mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-11 06:08:02 +00:00
Compare commits
3 Commits
gregtatcam
...
dangell7/b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6519117097 | ||
|
|
1d9e0def40 | ||
|
|
ff7411078f |
@@ -30,6 +30,7 @@ Version 3.5.0 is not yet released.
|
||||
|
||||
- `subscribe`, `unsubscribe`: Added an optional `mpt_issuances` request field, an array of MPT issuance IDs (hex strings). Subscribers receive a message with `type` `mptTransaction` for each validated transaction whose metadata affects a subscribed issuance; the message has the same fields as the `transactions` stream. MPT issuance subscriptions count toward the per-connection subscription limit. An empty array, a non-array value, or an invalid ID returns `invalidParams`. ([#5671](https://github.com/XRPLF/rippled/pull/5671))
|
||||
- `ledger_entry`: Add full support for checks, NFT offers, payment channels, and signer lists. ([#6319](https://github.com/XRPLF/rippled/pull/6319))
|
||||
- `tx`, `ledger`, `subscribe` (`transactions` stream): With the `BatchV2` amendment enabled, a `Batch` transaction that applies with `tesSUCCESS` is followed in the ledger by a `BatchResult` pseudo-transaction, placed after the batch's inner transactions. Its `Account` is `rrrrrrrrrrrrrrrrrrrrrhoLvTp`, `ParentBatchID` is the hash of the `Batch`, and `BatchResults` lists every inner transaction the batch attempted, in order, each as a `BatchResult` object with `TransactionHash` and `EngineResultCode`. `EngineResultCode` is a signed integer, so `tem`, `tef` and `ter` codes are negative. An inner transaction missing from `BatchResults` was never attempted; one with a `tes` or `tec` code is in the ledger unless the whole batch was rolled back. Its metadata carries `TransactionResult` `tesSUCCESS` and an empty `AffectedNodes`, and like other pseudo-transactions it cannot be submitted. `server_definitions`: `BatchResult` is added to `TRANSACTION_TYPES` and `TRANSACTION_FORMATS`, and `BatchResults`, `BatchResult` and `EngineResultCode` to `FIELDS`. ([#8532](https://github.com/XRPLF/rippled/pull/8532))
|
||||
|
||||
### Bugfixes in 3.5.0
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// Add new amendments to the top of this list.
|
||||
// Keep it sorted in reverse chronological order.
|
||||
|
||||
XRPL_FEATURE(BatchV2, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Cosign, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -245,6 +245,7 @@ TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset
|
||||
TYPED_SFIELD(sfLoanScale, INT32, 1)
|
||||
TYPED_SFIELD(sfRemainingOwnerCountDelta, INT32, 2)
|
||||
TYPED_SFIELD(sfVMReturnCode, INT32, 3)
|
||||
TYPED_SFIELD(sfEngineResultCode, INT32, 4)
|
||||
|
||||
// currency amount (common)
|
||||
TYPED_SFIELD(sfAmount, AMOUNT, 1)
|
||||
@@ -425,6 +426,7 @@ UNTYPED_SFIELD(sfBook, OBJECT, 36)
|
||||
UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::kSmdDefault, SField::kNotSigning)
|
||||
UNTYPED_SFIELD(sfSponsorSignature, OBJECT, 38, SField::kSmdDefault, SField::kNotSigning)
|
||||
UNTYPED_SFIELD(sfProposedTransaction, OBJECT, 39)
|
||||
UNTYPED_SFIELD(sfBatchResult, OBJECT, 40)
|
||||
|
||||
// array of objects (common)
|
||||
// ARRAY/1 is reserved for end of array
|
||||
@@ -457,3 +459,4 @@ UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28)
|
||||
UNTYPED_SFIELD(sfPermissions, ARRAY, 29)
|
||||
UNTYPED_SFIELD(sfRawTransactions, ARRAY, 30)
|
||||
UNTYPED_SFIELD(sfBatchSigners, ARRAY, 31, SField::kSmdDefault, SField::kNotSigning)
|
||||
UNTYPED_SFIELD(sfBatchResults, ARRAY, 32)
|
||||
|
||||
@@ -1221,3 +1221,16 @@ TRANSACTION(ttUNL_MODIFY, 102, UNLModify,
|
||||
{sfLedgerSequence, SoeRequired},
|
||||
{sfUNLModifyValidator, SoeRequired},
|
||||
}))
|
||||
|
||||
/** This system-generated transaction type records the outcome of every inner
|
||||
transaction a Batch attempted. The ledger build inserts it after the Batch's
|
||||
inner transactions; it is never submitted or proposed.
|
||||
*/
|
||||
TRANSACTION(ttBATCH_RESULT, 103, BatchResult,
|
||||
({
|
||||
.amendment = featureBatchV2,
|
||||
}),
|
||||
({
|
||||
{sfParentBatchID, SoeRequired},
|
||||
{sfBatchResults, SoeRequired},
|
||||
}))
|
||||
|
||||
155
include/xrpl/protocol_autogen/transactions/BatchResult.h
Normal file
155
include/xrpl/protocol_autogen/transactions/BatchResult.h
Normal file
@@ -0,0 +1,155 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBase.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
class BatchResultBuilder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: BatchResult
|
||||
*
|
||||
* Type: ttBATCH_RESULT (103)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureBatchV2
|
||||
* Privileges: Privilege::NoPriv
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use BatchResultBuilder to construct new transactions.
|
||||
*/
|
||||
class BatchResult : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ttBATCH_RESULT;
|
||||
|
||||
/**
|
||||
* @brief Construct a BatchResult transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit BatchResult(std::shared_ptr<STTx const> tx)
|
||||
: TransactionBase(std::move(tx))
|
||||
{
|
||||
// Verify transaction type
|
||||
if (tx_->getTxnType() != txType)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for BatchResult");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
|
||||
/**
|
||||
* @brief Get sfParentBatchID (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT256::type::value_type
|
||||
getParentBatchID() const
|
||||
{
|
||||
return this->tx_->at(sfParentBatchID);
|
||||
}
|
||||
/**
|
||||
* @brief Get sfBatchResults (SoeRequired)
|
||||
* @note This is an untyped field.
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
STArray const&
|
||||
getBatchResults() const
|
||||
{
|
||||
return this->tx_->getFieldArray(sfBatchResults);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for BatchResult transactions.
|
||||
*
|
||||
* Provides a fluent interface for constructing transactions with method chaining.
|
||||
* Uses STObject internally for flexible transaction construction.
|
||||
* Inherits common field setters from TransactionBuilderBase.
|
||||
*/
|
||||
class BatchResultBuilder : public TransactionBuilderBase<BatchResultBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new BatchResultBuilder with required fields.
|
||||
* @param account The account initiating the transaction.
|
||||
* @param parentBatchID The sfParentBatchID field value.
|
||||
* @param batchResults The sfBatchResults field value.
|
||||
* @param sequence Optional sequence number for the transaction.
|
||||
* @param fee Optional fee for the transaction.
|
||||
*/
|
||||
BatchResultBuilder(SF_ACCOUNT::type::value_type account,
|
||||
std::decay_t<typename SF_UINT256::type::value_type> const& parentBatchID, STArray const& batchResults, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
|
||||
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
|
||||
)
|
||||
: TransactionBuilderBase<BatchResultBuilder>(ttBATCH_RESULT, account, sequence, fee)
|
||||
{
|
||||
setParentBatchID(parentBatchID);
|
||||
setBatchResults(batchResults);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a BatchResultBuilder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
BatchResultBuilder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ttBATCH_RESULT)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for BatchResultBuilder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transaction-specific field setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set sfParentBatchID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
BatchResultBuilder&
|
||||
setParentBatchID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
|
||||
{
|
||||
object_[sfParentBatchID] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfBatchResults (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
BatchResultBuilder&
|
||||
setBatchResults(STArray const& value)
|
||||
{
|
||||
object_.setFieldArray(sfBatchResults, value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the BatchResult wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
BatchResult
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return BatchResult{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
@@ -59,5 +59,6 @@ private:
|
||||
using EnableAmendment = Change;
|
||||
using SetFee = Change;
|
||||
using UNLModify = Change;
|
||||
using BatchResult = Change;
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -146,6 +146,10 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfTxnSignature, SoeOptional},
|
||||
{sfSigners, SoeOptional}});
|
||||
|
||||
add(sfBatchResult.jsonName,
|
||||
sfBatchResult.getCode(),
|
||||
{{sfTransactionHash, SoeRequired}, {sfEngineResultCode, SoeRequired}});
|
||||
|
||||
add(sfBook.jsonName,
|
||||
sfBook.getCode(),
|
||||
{
|
||||
|
||||
@@ -895,7 +895,7 @@ isPseudoTx(STObject const& tx)
|
||||
|
||||
auto const tt = safeCast<TxType>(*t);
|
||||
|
||||
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY;
|
||||
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY || tt == ttBATCH_RESULT;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -8,19 +8,25 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -198,11 +204,22 @@ apply(
|
||||
});
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct BatchInnerResult
|
||||
{
|
||||
UInt256 id;
|
||||
TER ter;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static bool
|
||||
applyBatchTransactions(
|
||||
ServiceRegistry& registry,
|
||||
OpenView& batchView,
|
||||
STTx const& batchTxn,
|
||||
std::vector<BatchInnerResult>& results,
|
||||
beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
@@ -239,6 +256,7 @@ applyBatchTransactions(
|
||||
for (auto const& stx : batchTxn.getBatchTransactions())
|
||||
{
|
||||
auto const result = applyOneTransaction(*stx);
|
||||
results.push_back({.id = stx->getTransactionID(), .ter = result.ter});
|
||||
XRPL_ASSERT(
|
||||
result.applied == (isTesSuccess(result.ter) || isTecClaim(result.ter)),
|
||||
"Outer Batch failure, inner transaction should not be applied");
|
||||
@@ -263,6 +281,46 @@ applyBatchTransactions(
|
||||
return applied != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the BatchResult record listing every inner the Batch attempted.
|
||||
*
|
||||
* It goes into the view the outer Batch applied to, after the batch view was
|
||||
* committed or dropped, so it survives a rollback and takes the index after
|
||||
* the last applied inner.
|
||||
*/
|
||||
static void
|
||||
insertBatchResult(
|
||||
OpenView& view,
|
||||
UInt256 const& parentBatchId,
|
||||
std::vector<BatchInnerResult> const& results)
|
||||
{
|
||||
XRPL_ASSERT(!view.open(), "xrpl::insertBatchResult : closed ledger view");
|
||||
|
||||
STArray entries(sfBatchResults, results.size());
|
||||
for (auto const& result : results)
|
||||
{
|
||||
auto entry = STObject::makeInnerObject(sfBatchResult);
|
||||
entry.setFieldH256(sfTransactionHash, result.id);
|
||||
entry.setFieldI32(sfEngineResultCode, TERtoInt(result.ter));
|
||||
entries.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
STTx const record(ttBATCH_RESULT, [&](STObject& obj) {
|
||||
obj.setAccountID(sfAccount, AccountID());
|
||||
obj.setFieldH256(sfParentBatchID, parentBatchId);
|
||||
obj.setFieldArray(sfBatchResults, entries);
|
||||
});
|
||||
|
||||
auto const txID = record.getTransactionID();
|
||||
auto const sTx = std::make_shared<Serializer>();
|
||||
record.add(*sTx);
|
||||
|
||||
auto const sMeta = std::make_shared<Serializer>();
|
||||
TxMeta(txID, view.seq()).addRaw(*sMeta, tesSUCCESS, view.txCount());
|
||||
|
||||
view.rawTxInsert(txID, sTx, sMeta);
|
||||
}
|
||||
|
||||
ApplyTransactionResult
|
||||
applyTransaction(
|
||||
ServiceRegistry& registry,
|
||||
@@ -291,9 +349,13 @@ applyTransaction(
|
||||
if (isTesSuccess(result.ter) && txn.getTxnType() == ttBATCH)
|
||||
{
|
||||
OpenView wholeBatchView(kBatchView, view);
|
||||
std::vector<BatchInnerResult> innerResults;
|
||||
|
||||
if (applyBatchTransactions(registry, wholeBatchView, txn, j))
|
||||
if (applyBatchTransactions(registry, wholeBatchView, txn, innerResults, j))
|
||||
wholeBatchView.apply(view);
|
||||
|
||||
if (view.rules().enabled(featureBatchV2))
|
||||
insertBatchResult(view, txn.getTransactionID(), innerResults);
|
||||
}
|
||||
|
||||
return ApplyTransactionResult::Success;
|
||||
|
||||
@@ -42,6 +42,13 @@ Transactor::invokePreflight<Change>(PreflightContext const& ctx)
|
||||
preflight0(ctx, ctx.rules.enabled(featureLendingProtocol) ? tfEnableAmendmentMask : 0))
|
||||
return ret;
|
||||
|
||||
// The ledger build inserts BatchResult directly; it is never applied as a transaction.
|
||||
if (ctx.tx.getTxnType() == ttBATCH_RESULT)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Change: BatchResult cannot be applied";
|
||||
return temINVALID;
|
||||
}
|
||||
|
||||
auto account = ctx.tx.getAccountID(sfAccount);
|
||||
if (account != beast::kZero)
|
||||
{
|
||||
|
||||
@@ -68,9 +68,12 @@
|
||||
#include <xrpl/tx/transactors/payment/Payment.h>
|
||||
#include <xrpl/tx/transactors/system/Batch.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -97,12 +100,40 @@ class Batch_test : public beast::unit_test::Suite
|
||||
std::string txHash;
|
||||
};
|
||||
|
||||
static bool
|
||||
isBatchResult(json::Value const& txn)
|
||||
{
|
||||
return txn[sfTransactionType.jsonName] == "BatchResult";
|
||||
}
|
||||
|
||||
static std::uint32_t
|
||||
txIndex(json::Value const& txn)
|
||||
{
|
||||
return txn[jss::metaData][sfTransactionIndex.jsonName].asUInt();
|
||||
}
|
||||
|
||||
// Index counts only the ledger's non-BatchResult transactions, as in a ledger without BatchV2.
|
||||
static json::Value
|
||||
getTxByIndex(json::Value const& jrr, int const index)
|
||||
{
|
||||
std::vector<json::Value> txns;
|
||||
for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions])
|
||||
{
|
||||
if (txn[jss::metaData][sfTransactionIndex.jsonName] == index)
|
||||
if (!isBatchResult(txn))
|
||||
txns.push_back(txn);
|
||||
}
|
||||
std::ranges::sort(txns, {}, &txIndex);
|
||||
if (index < 0 || std::cmp_greater_equal(index, txns.size()))
|
||||
return {};
|
||||
return txns[index];
|
||||
}
|
||||
|
||||
static json::Value
|
||||
getBatchResult(json::Value const& jrr, std::string const& batchID)
|
||||
{
|
||||
for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions])
|
||||
{
|
||||
if (isBatchResult(txn) && txn[sfParentBatchID.jsonName] == batchID)
|
||||
return txn;
|
||||
}
|
||||
return {};
|
||||
@@ -118,12 +149,118 @@ class Batch_test : public beast::unit_test::Suite
|
||||
return env.rpc("json", "ledger", to_string(params));
|
||||
}
|
||||
|
||||
// Checks batchID's BatchResult against the Batch's mode and the inner leaves in the ledger.
|
||||
void
|
||||
validateBatchResult(jtx::Env& env, json::Value const& jrr, std::string const& batchID)
|
||||
{
|
||||
auto const record = getBatchResult(jrr, batchID);
|
||||
if (!BEAST_EXPECT(record.isObject()))
|
||||
return;
|
||||
|
||||
// The record follows the Batch's last leaf, changes nothing and has no account.
|
||||
std::uint32_t last = 0;
|
||||
std::size_t leaves = 0;
|
||||
for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions])
|
||||
{
|
||||
bool const isLeaf = txn[jss::metaData][sfParentBatchID.jsonName] == batchID;
|
||||
if (isLeaf)
|
||||
++leaves;
|
||||
if (isLeaf || txn[jss::hash] == batchID)
|
||||
last = std::max(last, txIndex(txn));
|
||||
}
|
||||
BEAST_EXPECT(txIndex(record) == last + 1);
|
||||
BEAST_EXPECT(record[jss::metaData][sfTransactionResult.jsonName] == "tesSUCCESS");
|
||||
BEAST_EXPECT(record[jss::metaData][sfAffectedNodes.jsonName].size() == 0);
|
||||
BEAST_EXPECT(record[sfAccount.jsonName] == toBase58(AccountID()));
|
||||
|
||||
UInt256 outerID;
|
||||
UInt256 recordID;
|
||||
if (!BEAST_EXPECT(
|
||||
outerID.parseHex(batchID) && recordID.parseHex(record[jss::hash].asString())))
|
||||
return;
|
||||
auto const& ledger = *env.closed();
|
||||
auto const outer = ledger.txRead(outerID).first;
|
||||
auto const recordTx = ledger.txRead(recordID).first;
|
||||
if (!BEAST_EXPECT(outer && recordTx))
|
||||
return;
|
||||
|
||||
// Entries are the first inners in sfRawTransactions order.
|
||||
auto const innerIDs = outer->getBatchTransactionIDs();
|
||||
std::vector<TER> results;
|
||||
for (auto const& entry : recordTx->getFieldArray(sfBatchResults))
|
||||
{
|
||||
if (!BEAST_EXPECT(results.size() < innerIDs.size()))
|
||||
return;
|
||||
BEAST_EXPECT(entry.getFieldH256(sfTransactionHash) == innerIDs[results.size()]);
|
||||
results.push_back(TER::fromInt(entry.getFieldI32(sfEngineResultCode)));
|
||||
}
|
||||
|
||||
// The Batch stops after the first non-tes under tfAllOrNothing and tfUntilFailure,
|
||||
// after the first tes under tfOnlyOne, and never under tfIndependent.
|
||||
auto const mode = outer->getFlags();
|
||||
auto const attempted = [&](auto stopsOn) {
|
||||
auto const stop = std::ranges::find_if(results, stopsOn);
|
||||
return stop == results.end() ? innerIDs.size()
|
||||
: std::ranges::distance(results.begin(), stop) + 1;
|
||||
};
|
||||
auto const isTes = [](TER ter) { return isTesSuccess(ter); };
|
||||
auto expected = innerIDs.size();
|
||||
if ((mode & (tfAllOrNothing | tfUntilFailure)) != 0u)
|
||||
{
|
||||
expected = attempted(std::not_fn(isTes));
|
||||
}
|
||||
else if ((mode & tfOnlyOne) != 0u)
|
||||
{
|
||||
expected = attempted(isTes);
|
||||
}
|
||||
BEAST_EXPECT(std::cmp_equal(results.size(), expected));
|
||||
|
||||
// An entry has a leaf iff it is tes or tec and the Batch was not rolled back.
|
||||
bool const rolledBack =
|
||||
(mode & tfAllOrNothing) != 0u && !std::ranges::all_of(results, isTes);
|
||||
std::size_t applied = 0;
|
||||
for (std::size_t i = 0; i < results.size(); ++i)
|
||||
{
|
||||
auto const [tx, meta] = ledger.txRead(innerIDs[i]);
|
||||
bool const isApplied =
|
||||
!rolledBack && (isTesSuccess(results[i]) || isTecClaim(results[i]));
|
||||
if (!BEAST_EXPECT((tx != nullptr) == isApplied) || !isApplied)
|
||||
continue;
|
||||
++applied;
|
||||
BEAST_EXPECT(meta->getFieldH256(sfParentBatchID) == outerID);
|
||||
BEAST_EXPECT(
|
||||
std::cmp_equal(meta->getFieldU8(sfTransactionResult), TERtoInt(results[i])));
|
||||
}
|
||||
BEAST_EXPECT(leaves == applied);
|
||||
}
|
||||
|
||||
void
|
||||
validateClosedLedger(jtx::Env& env, std::vector<TestLedgerData> const& ledgerResults)
|
||||
{
|
||||
auto const jrr = getLastLedger(env);
|
||||
auto const transactions = jrr[jss::result][jss::ledger][jss::transactions];
|
||||
BEAST_EXPECT(transactions.size() == ledgerResults.size());
|
||||
auto const records = std::ranges::count_if(transactions, isBatchResult);
|
||||
BEAST_EXPECT(transactions.size() - records == ledgerResults.size());
|
||||
|
||||
// Each applied Batch has one BatchResult under BatchV2 and none without it.
|
||||
bool const recordsEnabled = env.closed()->rules().enabled(featureBatchV2);
|
||||
std::size_t outers = 0;
|
||||
for (TestLedgerData const& ledgerResult : ledgerResults)
|
||||
{
|
||||
if (ledgerResult.txType != "Batch" || ledgerResult.result != "tesSUCCESS")
|
||||
continue;
|
||||
++outers;
|
||||
if (recordsEnabled)
|
||||
{
|
||||
validateBatchResult(env, jrr, ledgerResult.txHash);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(getBatchResult(jrr, ledgerResult.txHash).isNull());
|
||||
}
|
||||
}
|
||||
BEAST_EXPECT(std::cmp_equal(records, recordsEnabled ? outers : 0));
|
||||
|
||||
for (TestLedgerData const& ledgerResult : ledgerResults)
|
||||
{
|
||||
auto const txn = getTxByIndex(jrr, ledgerResult.index);
|
||||
@@ -5940,6 +6077,249 @@ class Batch_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testBatchResult(FeatureBitset features)
|
||||
{
|
||||
testcase("batch result");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
bool const enabled = features[featureBatchV2];
|
||||
|
||||
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();
|
||||
|
||||
// The closed ledger's BatchResult for batchID lists exactly these inners and results.
|
||||
auto const expectRecord = [&](std::string const& batchID,
|
||||
std::vector<std::pair<std::string, TER>> const& expected) {
|
||||
auto const record = getBatchResult(getLastLedger(env), batchID);
|
||||
if (!enabled)
|
||||
{
|
||||
BEAST_EXPECT(record.isNull());
|
||||
return;
|
||||
}
|
||||
if (!BEAST_EXPECT(record.isObject()))
|
||||
return;
|
||||
auto const& entries = record[sfBatchResults.jsonName];
|
||||
if (!BEAST_EXPECT(entries.size() == expected.size()))
|
||||
return;
|
||||
for (std::size_t i = 0; i < expected.size(); ++i)
|
||||
{
|
||||
auto const& entry = entries[i][sfBatchResult.jsonName];
|
||||
BEAST_EXPECT(entry[sfTransactionHash.jsonName] == expected[i].first);
|
||||
BEAST_EXPECT(
|
||||
entry[sfEngineResultCode.jsonName].asInt() == TERtoInt(expected[i].second));
|
||||
}
|
||||
};
|
||||
|
||||
// tfAllOrNothing rolled back: both inners recorded, neither in the ledger.
|
||||
{
|
||||
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();
|
||||
|
||||
validateClosedLedger(
|
||||
env,
|
||||
{{.index = 0,
|
||||
.txType = "Batch",
|
||||
.result = "tesSUCCESS",
|
||||
.txHash = batchID,
|
||||
.batchID = std::nullopt}});
|
||||
expectRecord(batchID, {{txIDs[0], tesSUCCESS}, {txIDs[1], tefNO_AUTH_REQUIRED}});
|
||||
|
||||
// The record is retrievable by hash like any ledger transaction.
|
||||
if (enabled)
|
||||
{
|
||||
auto const record = getBatchResult(getLastLedger(env), batchID);
|
||||
auto const jrr = env.rpc("tx", record[jss::hash].asString());
|
||||
BEAST_EXPECT(jrr[jss::result][sfTransactionType.jsonName] == "BatchResult");
|
||||
BEAST_EXPECT(jrr[jss::result][sfParentBatchID.jsonName] == batchID);
|
||||
}
|
||||
}
|
||||
|
||||
// tfIndependent, all succeed: the record follows both inners.
|
||||
{
|
||||
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, tfIndependent),
|
||||
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
|
||||
batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
|
||||
env.close();
|
||||
|
||||
validateClosedLedger(
|
||||
env,
|
||||
{{.index = 0,
|
||||
.txType = "Batch",
|
||||
.result = "tesSUCCESS",
|
||||
.txHash = batchID,
|
||||
.batchID = std::nullopt},
|
||||
{.index = 1,
|
||||
.txType = "Payment",
|
||||
.result = "tesSUCCESS",
|
||||
.txHash = txIDs[0],
|
||||
.batchID = batchID},
|
||||
{.index = 2,
|
||||
.txType = "Payment",
|
||||
.result = "tesSUCCESS",
|
||||
.txHash = txIDs[1],
|
||||
.batchID = batchID}});
|
||||
expectRecord(batchID, {{txIDs[0], tesSUCCESS}, {txIDs[1], tesSUCCESS}});
|
||||
}
|
||||
|
||||
// tfIndependent with a tec inner: the tec is recorded and its leaf is in the ledger.
|
||||
{
|
||||
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, tfIndependent),
|
||||
// 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));
|
||||
env.close();
|
||||
|
||||
validateClosedLedger(
|
||||
env,
|
||||
{{.index = 0,
|
||||
.txType = "Batch",
|
||||
.result = "tesSUCCESS",
|
||||
.txHash = batchID,
|
||||
.batchID = std::nullopt},
|
||||
{.index = 1,
|
||||
.txType = "Payment",
|
||||
.result = "tecUNFUNDED_PAYMENT",
|
||||
.txHash = txIDs[0],
|
||||
.batchID = batchID},
|
||||
{.index = 2,
|
||||
.txType = "Payment",
|
||||
.result = "tesSUCCESS",
|
||||
.txHash = txIDs[1],
|
||||
.batchID = batchID}});
|
||||
expectRecord(batchID, {{txIDs[0], tecUNFUNDED_PAYMENT}, {txIDs[1], tesSUCCESS}});
|
||||
}
|
||||
|
||||
// tfUntilFailure stops at the failing second inner; the third is never attempted.
|
||||
{
|
||||
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, tfUntilFailure),
|
||||
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),
|
||||
batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
|
||||
env.close();
|
||||
|
||||
validateClosedLedger(
|
||||
env,
|
||||
{{.index = 0,
|
||||
.txType = "Batch",
|
||||
.result = "tesSUCCESS",
|
||||
.txHash = batchID,
|
||||
.batchID = std::nullopt},
|
||||
{.index = 1,
|
||||
.txType = "Payment",
|
||||
.result = "tesSUCCESS",
|
||||
.txHash = txIDs[0],
|
||||
.batchID = batchID}});
|
||||
expectRecord(batchID, {{txIDs[0], tesSUCCESS}, {txIDs[1], tefNO_AUTH_REQUIRED}});
|
||||
}
|
||||
|
||||
// tfOnlyOne stops at the first success; the third is never attempted.
|
||||
{
|
||||
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();
|
||||
|
||||
validateClosedLedger(
|
||||
env,
|
||||
{{.index = 0,
|
||||
.txType = "Batch",
|
||||
.result = "tesSUCCESS",
|
||||
.txHash = batchID,
|
||||
.batchID = std::nullopt},
|
||||
{.index = 1,
|
||||
.txType = "Payment",
|
||||
.result = "tecUNFUNDED_PAYMENT",
|
||||
.txHash = txIDs[0],
|
||||
.batchID = batchID},
|
||||
{.index = 2,
|
||||
.txType = "Payment",
|
||||
.result = "tesSUCCESS",
|
||||
.txHash = txIDs[1],
|
||||
.batchID = batchID}});
|
||||
expectRecord(batchID, {{txIDs[0], tecUNFUNDED_PAYMENT}, {txIDs[1], tesSUCCESS}});
|
||||
}
|
||||
|
||||
// One result of each class an applied inner can return.
|
||||
{
|
||||
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),
|
||||
// temDST_IS_SRC: TrustSet::preclaim rejects a trust line to itself
|
||||
batch::Inner(trust(alice, alice["USD"](100)), seq + 3),
|
||||
// terPRE_SEQ: the account is at seq + 3
|
||||
batch::Inner(pay(alice, bob, XRP(1)), seq + 5));
|
||||
env.close();
|
||||
|
||||
validateClosedLedger(
|
||||
env,
|
||||
{{.index = 0,
|
||||
.txType = "Batch",
|
||||
.result = "tesSUCCESS",
|
||||
.txHash = batchID,
|
||||
.batchID = std::nullopt},
|
||||
{.index = 1,
|
||||
.txType = "Payment",
|
||||
.result = "tesSUCCESS",
|
||||
.txHash = txIDs[0],
|
||||
.batchID = batchID},
|
||||
{.index = 2,
|
||||
.txType = "Payment",
|
||||
.result = "tecUNFUNDED_PAYMENT",
|
||||
.txHash = txIDs[1],
|
||||
.batchID = batchID}});
|
||||
expectRecord(
|
||||
batchID,
|
||||
{{txIDs[0], tesSUCCESS},
|
||||
{txIDs[1], tecUNFUNDED_PAYMENT},
|
||||
{txIDs[2], temDST_IS_SRC},
|
||||
{txIDs[3], terPRE_SEQ}});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
@@ -5980,6 +6360,7 @@ class Batch_test : public beast::unit_test::Suite
|
||||
testOuterBinding(features);
|
||||
testUnsortedBatchSigners(features);
|
||||
testBatchSigCache(features);
|
||||
testBatchResult(features);
|
||||
}
|
||||
|
||||
public:
|
||||
@@ -5990,6 +6371,7 @@ public:
|
||||
|
||||
auto const sa = testableAmendments();
|
||||
testWithFeats(sa);
|
||||
testWithFeats(sa - featureBatchV2);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include <xrpl/protocol/RippleLedgerHash.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/server/Handoff.h>
|
||||
#include <xrpl/shamap/SHAMapItem.h>
|
||||
|
||||
@@ -116,7 +117,37 @@ struct LedgerReplay_test : public beast::unit_test::Suite
|
||||
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
expectReplayMatches(env);
|
||||
|
||||
// A rolled-back tfAllOrNothing batch and a tfIndependent batch in one ledger.
|
||||
auto const seq2 = env.seq(alice);
|
||||
env(batch::outer(alice, seq2, batchFee, tfAllOrNothing),
|
||||
batch::Inner(pay(alice, bob, XRP(1)), seq2 + 1),
|
||||
// tecUNFUNDED_PAYMENT rolls the whole batch back
|
||||
batch::Inner(pay(alice, bob, XRP(1000000)), seq2 + 2),
|
||||
Ter(tesSUCCESS));
|
||||
auto const seq3 = env.seq(alice);
|
||||
env(batch::outer(alice, seq3, batchFee, tfIndependent),
|
||||
batch::Inner(pay(alice, bob, XRP(1)), seq3 + 1),
|
||||
batch::Inner(pay(alice, bob, XRP(2)), seq3 + 2),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// Both batches left a BatchResult record, so the replay has to regenerate them.
|
||||
int records = 0;
|
||||
for (auto const& item : env.app().getLedgerMaster().getClosedLedger()->txs)
|
||||
{
|
||||
if (item.first->getTxnType() == ttBATCH_RESULT)
|
||||
++records;
|
||||
}
|
||||
BEAST_EXPECT(records == 2);
|
||||
expectReplayMatches(env);
|
||||
}
|
||||
|
||||
// Replaying the last closed ledger from its parent reproduces its hash.
|
||||
void
|
||||
expectReplayMatches(jtx::Env& env)
|
||||
{
|
||||
LedgerMaster& ledgerMaster = env.app().getLedgerMaster();
|
||||
auto const lastClosed = ledgerMaster.getClosedLedger();
|
||||
auto const lastClosedParent = ledgerMaster.getLedgerByHash(lastClosed->header().parentHash);
|
||||
|
||||
@@ -6,14 +6,18 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/tx/apply.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -51,6 +55,12 @@ struct PseudoTx_test : public beast::unit_test::Suite
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
});
|
||||
|
||||
res.emplace_back(ttBATCH_RESULT, [&](auto& obj) {
|
||||
obj.setAccountID(sfAccount, AccountID());
|
||||
obj.setFieldH256(sfParentBatchID, UInt256(3));
|
||||
obj.setFieldArray(sfBatchResults, STArray(sfBatchResults));
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -89,6 +99,26 @@ struct PseudoTx_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testBatchResultRejectedOnClosedView(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
Env env(*this, features);
|
||||
|
||||
// A BatchResult placed in a proposed transaction set is applied to a closed view.
|
||||
auto const pseudoTxs = getPseudoTxs(env.closed()->rules(), env.closed()->seq() + 1);
|
||||
auto const record = std::ranges::find_if(
|
||||
pseudoTxs, [](STTx const& stx) { return stx.getTxnType() == ttBATCH_RESULT; });
|
||||
if (!BEAST_EXPECT(record != pseudoTxs.end()))
|
||||
return;
|
||||
|
||||
OpenView closedView(&*env.closed());
|
||||
BEAST_EXPECT(!closedView.open());
|
||||
auto const result = xrpl::apply(env.app(), closedView, *record, TapNone, env.journal);
|
||||
BEAST_EXPECT(!result.applied && result.ter == temINVALID);
|
||||
BEAST_EXPECT(closedView.txCount() == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testAllowed()
|
||||
{
|
||||
@@ -109,6 +139,8 @@ struct PseudoTx_test : public beast::unit_test::Suite
|
||||
|
||||
testPrevented(all - featureXRPFees);
|
||||
testPrevented(all);
|
||||
testBatchResultRejectedOnClosedView(all);
|
||||
testBatchResultRejectedOnClosedView(all - featureBatchV2);
|
||||
testAllowed();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// Auto-generated unit tests for transaction BatchResult
|
||||
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <protocol_autogen/TestHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol_autogen/transactions/BatchResult.h>
|
||||
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
// 1 & 4) Set fields via builder setters, build, then read them back via
|
||||
// wrapper getters. After build(), validate() should succeed.
|
||||
TEST(TransactionsBatchResultTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testBatchResult"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 1;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const parentBatchIDValue = canonical_UINT256();
|
||||
auto const batchResultsValue = canonical_ARRAY();
|
||||
|
||||
BatchResultBuilder builder{
|
||||
accountValue,
|
||||
parentBatchIDValue,
|
||||
batchResultsValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(tx.validate(reason)) << reason;
|
||||
|
||||
// Verify signing was applied
|
||||
EXPECT_FALSE(tx.getSigningPubKey().empty());
|
||||
EXPECT_TRUE(tx.hasTxnSignature());
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(tx.getAccount(), accountValue);
|
||||
EXPECT_EQ(tx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(tx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
{
|
||||
auto const& expected = parentBatchIDValue;
|
||||
auto const actual = tx.getParentBatchID();
|
||||
expectEqualField(expected, actual, "sfParentBatchID");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = batchResultsValue;
|
||||
auto const actual = tx.getBatchResults();
|
||||
expectEqualField(expected, actual, "sfBatchResults");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
// and verify all fields match.
|
||||
TEST(TransactionsBatchResultTests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testBatchResultFromTx"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 2;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const parentBatchIDValue = canonical_UINT256();
|
||||
auto const batchResultsValue = canonical_ARRAY();
|
||||
|
||||
// Build an initial transaction
|
||||
BatchResultBuilder initialBuilder{
|
||||
accountValue,
|
||||
parentBatchIDValue,
|
||||
batchResultsValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
BatchResultBuilder builderFromTx{initialTx.getSTTx()};
|
||||
|
||||
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
|
||||
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
{
|
||||
auto const& expected = parentBatchIDValue;
|
||||
auto const actual = rebuiltTx.getParentBatchID();
|
||||
expectEqualField(expected, actual, "sfParentBatchID");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = batchResultsValue;
|
||||
auto const actual = rebuiltTx.getBatchResults();
|
||||
expectEqualField(expected, actual, "sfBatchResults");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsBatchResultTests, WrapperThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(BatchResult{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsBatchResultTests, BuilderThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(BatchResultBuilder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SystemParameters.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/tx/apply.h>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -243,6 +244,9 @@ buildLedger(
|
||||
// replay.
|
||||
if (tx.second->isFlag(tfInnerBatchTxn))
|
||||
continue;
|
||||
// Replaying the outer Batch regenerates its BatchResult record.
|
||||
if (tx.second->getTxnType() == ttBATCH_RESULT)
|
||||
continue;
|
||||
applyTransaction(app, accum, *tx.second, false, applyFlags, j);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user