mirror of
https://github.com/XRPLF/clio.git
synced 2026-08-22 21:20:52 +00:00
feat: Add ETL Indexing for mpt_issuance_history (#3104)
## Summary Adds live ETL indexing for mptoken_issuance_history by extracting MPT issuance references from transaction metadata and transaction fields, then writing both MPT transaction index shapes during the existing MPT ETL pass. Successful transactions are indexed from affected MPT ledger objects; failed transactions are indexed only when they carry an attached MPT issuance reference. ## Changes - Adds `getMPTokenIssuanceTxsFromTx` to extract issuance transaction index records from `tesSUCCESS` transactions. - Scans `AffectedNodes` generically for `MPToken` and `MPTokenIssuance` ledger objects instead of relying on a transaction-type allowlist. - Reconstructs issuance IDs for `MPTokenIssuance` objects with `makeMptID(sequence, issuer)`, avoiding the hashed ledger-key trap. - Deduplicates distinct issuances per transaction and attaches the transaction’s affected accounts for account-level fanout. - Extends `MPTExt` so live and initial ETL data writes: - existing MPT holder index data - `mptoken_issuance_transactions` - `account_mptoken_issuance_transactions` - Adds a Prometheus counter for MPT issuance transaction index rows written by ETL. - Logs unexpectedly large per-transaction fanout. ## Tests Adds unit coverage for: - failed transactions producing no records - `MPTokenIssuanceCreate`, `Destroy`, `Set`, and `MPTokenAuthorize` - generic transaction-type coverage through `Payment`, `Clawback`, `OfferCreate`, and `AMMDeposit` - ID reconstruction from issuance nodes - multi-issuance fanout and deduplication - affected-account propagation - ETL extension writes to both backend index paths while preserving existing holder indexing --------- Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
This commit is contained in:
@@ -211,6 +211,9 @@ struct MPTokenIssuanceTransactionsData {
|
||||
std::uint32_t ledgerSequence{};
|
||||
std::uint32_t transactionIndex{};
|
||||
xrpl::uint256 txHash;
|
||||
|
||||
bool
|
||||
operator==(MPTokenIssuanceTransactionsData const&) const = default;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
#include "data/DBHelpers.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
|
||||
#include <boost/container/flat_set.hpp>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STBase.h>
|
||||
#include <xrpl/protocol/STIssue.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
@@ -44,6 +49,112 @@ getMPTHolderFromTx(xrpl::TxMeta const& txMeta, xrpl::STTx const&)
|
||||
return holders;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using MPTokenIssuanceIDs = boost::container::flat_set<xrpl::uint192>;
|
||||
|
||||
/**
|
||||
* @brief Derive the MPTokenIssuanceID from an affected node in transaction metadata.
|
||||
*
|
||||
* @param node An entry of the metadata's AffectedNodes array.
|
||||
* @return The 192-bit issuance ID if the node is an MPTokenIssuance or MPToken object.
|
||||
*/
|
||||
std::optional<xrpl::uint192>
|
||||
getMPTokenIssuanceIDFromNode(xrpl::STObject const& node)
|
||||
{
|
||||
auto const entryType = node.getFieldU16(xrpl::sfLedgerEntryType);
|
||||
if (entryType != xrpl::ltMPTOKEN && entryType != xrpl::ltMPTOKEN_ISSUANCE)
|
||||
return std::nullopt;
|
||||
|
||||
auto const& fieldsName =
|
||||
node.getFName() == xrpl::sfCreatedNode ? xrpl::sfNewFields : xrpl::sfFinalFields;
|
||||
if (not node.isFieldPresent(fieldsName))
|
||||
return std::nullopt;
|
||||
|
||||
auto const& fields = node.peekAtField(fieldsName).downcast<xrpl::STObject>();
|
||||
|
||||
if (entryType == xrpl::ltMPTOKEN) {
|
||||
if (not fields.isFieldPresent(xrpl::sfMPTokenIssuanceID))
|
||||
return std::nullopt;
|
||||
|
||||
return fields[xrpl::sfMPTokenIssuanceID];
|
||||
}
|
||||
|
||||
// MPTokenIssuance objects carry no sfMPTokenIssuanceID, and the node's ledger key is a
|
||||
// one-way hash that does not embed the ID, so reconstruct it from sfSequence and sfIssuer
|
||||
if (not fields.isFieldPresent(xrpl::sfSequence) || not fields.isFieldPresent(xrpl::sfIssuer))
|
||||
return std::nullopt;
|
||||
|
||||
return xrpl::makeMptID(
|
||||
fields.getFieldU32(xrpl::sfSequence), fields.getAccountID(xrpl::sfIssuer)
|
||||
);
|
||||
}
|
||||
|
||||
void
|
||||
addMPTokenIssuanceIDsFromTx(MPTokenIssuanceIDs& issuanceIDs, xrpl::STTx const& sttx)
|
||||
{
|
||||
if (sttx.isFieldPresent(xrpl::sfMPTokenIssuanceID))
|
||||
issuanceIDs.insert(sttx.getFieldH192(xrpl::sfMPTokenIssuanceID));
|
||||
|
||||
for (xrpl::STBase const& field : sttx) {
|
||||
switch (field.getSType()) {
|
||||
case xrpl::STI_AMOUNT: {
|
||||
auto const& amount = field.downcast<xrpl::STAmount>();
|
||||
if (amount.holds<xrpl::MPTIssue>())
|
||||
issuanceIDs.insert(amount.get<xrpl::MPTIssue>().getMptID());
|
||||
break;
|
||||
}
|
||||
case xrpl::STI_ISSUE: {
|
||||
auto const& issue = field.downcast<xrpl::STIssue>();
|
||||
if (issue.holds<xrpl::MPTIssue>())
|
||||
issuanceIDs.insert(issue.value().get<xrpl::MPTIssue>().getMptID());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<MPTokenIssuanceTransactionsData>
|
||||
getMPTokenIssuanceTxsFromTx(xrpl::TxMeta const& txMeta, xrpl::STTx const& sttx)
|
||||
{
|
||||
// Collect each distinct issuance only once per transaction; the same set of affected accounts
|
||||
// is attached to every record produced below.
|
||||
MPTokenIssuanceIDs issuanceIDs;
|
||||
|
||||
if (txMeta.getResultTER() == xrpl::tesSUCCESS) {
|
||||
for (auto const& node : txMeta.getNodes()) {
|
||||
if (auto const issuanceID = getMPTokenIssuanceIDFromNode(node); issuanceID.has_value())
|
||||
issuanceIDs.insert(*issuanceID);
|
||||
}
|
||||
}
|
||||
|
||||
addMPTokenIssuanceIDsFromTx(issuanceIDs, sttx);
|
||||
|
||||
if (issuanceIDs.empty())
|
||||
return {};
|
||||
|
||||
auto const accounts = txMeta.getAffectedAccounts();
|
||||
|
||||
std::vector<MPTokenIssuanceTransactionsData> result;
|
||||
result.reserve(issuanceIDs.size());
|
||||
for (auto const& issuanceID : issuanceIDs) {
|
||||
result.push_back(
|
||||
MPTokenIssuanceTransactionsData{
|
||||
.mptIssuanceID = issuanceID,
|
||||
.accounts = accounts,
|
||||
.ledgerSequence = txMeta.getLgrSeq(),
|
||||
.transactionIndex = txMeta.getIndex(),
|
||||
.txHash = sttx.getTransactionID()
|
||||
}
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<MPTHolderData>
|
||||
getMPTHolderFromObj(std::string const& key, std::string const& blob)
|
||||
{
|
||||
@@ -58,7 +169,7 @@ getMPTHolderFromObj(std::string const& key, std::string const& blob)
|
||||
);
|
||||
|
||||
if (sle.getFieldU16(xrpl::sfLedgerEntryType) != xrpl::ltMPTOKEN)
|
||||
return {};
|
||||
return std::nullopt;
|
||||
|
||||
auto const mptIssuanceID = sle[xrpl::sfMPTokenIssuanceID];
|
||||
auto const holder = sle.getAccountID(xrpl::sfAccount);
|
||||
|
||||
@@ -33,4 +33,22 @@ getMPTHolderFromTx(xrpl::TxMeta const& txMeta, xrpl::STTx const& sttx);
|
||||
std::optional<MPTHolderData>
|
||||
getMPTHolderFromObj(std::string const& key, std::string const& blob);
|
||||
|
||||
/**
|
||||
* @brief Pull MPT issuance transaction index data from a transaction.
|
||||
*
|
||||
* @note This scans the transaction's metadata for affected MPTokenIssuance/MPToken ledger objects
|
||||
* and transaction fields for attached MPTokenIssuanceID/MPT issue references. It produces one
|
||||
* record per distinct issuance, each carrying the full set of affected accounts. Transaction fields
|
||||
* are scanned so failed transactions that carry an issuance reference are indexed even when
|
||||
* metadata has no affected MPT objects. Used by live ETL and reused by the historical backfill
|
||||
* migrator.
|
||||
*
|
||||
* @param txMeta Transaction metadata.
|
||||
* @param sttx The transaction.
|
||||
* @return One record per distinct MPT issuance referenced by metadata or transaction fields; empty
|
||||
* if no MPT issuance reference is found.
|
||||
*/
|
||||
std::vector<MPTokenIssuanceTransactionsData>
|
||||
getMPTokenIssuanceTxsFromTx(xrpl::TxMeta const& txMeta, xrpl::STTx const& sttx);
|
||||
|
||||
} // namespace etl
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
#include <xrpl/basics/strHex.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
@@ -25,7 +27,7 @@ MPTExt::onLedgerData(model::LedgerData const& data)
|
||||
{
|
||||
LOG(log_.trace()) << "got TXS cnt = " << data.transactions.size()
|
||||
<< "; OBJS size = " << data.objects.size();
|
||||
writeMPTHoldersFromTransactions(data);
|
||||
writeMPTDataFromTransactions(data);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -40,21 +42,50 @@ void
|
||||
MPTExt::onInitialData(model::LedgerData const& data)
|
||||
{
|
||||
LOG(log_.trace()) << "got initial TXS cnt = " << data.transactions.size();
|
||||
writeMPTHoldersFromTransactions(data);
|
||||
writeMPTDataFromTransactions(data);
|
||||
}
|
||||
|
||||
void
|
||||
MPTExt::writeMPTHoldersFromTransactions(model::LedgerData const& data)
|
||||
MPTExt::writeMPTDataFromTransactions(model::LedgerData const& data)
|
||||
{
|
||||
std::vector<MPTHolderData> holders;
|
||||
std::vector<MPTokenIssuanceTransactionsData> issuanceTxs;
|
||||
std::size_t indexRowsWritten = 0;
|
||||
static constexpr std::size_t kIndexRowsPerTxWarningThreshold = 1000;
|
||||
|
||||
for (auto const& tx : data.transactions) {
|
||||
auto const mptHolders = getMPTHolderFromTx(tx.meta, tx.sttx);
|
||||
holders.append_range(mptHolders);
|
||||
|
||||
auto txIndexData = getMPTokenIssuanceTxsFromTx(tx.meta, tx.sttx);
|
||||
|
||||
std::size_t txIndexRows = 0;
|
||||
for (auto const& record : txIndexData)
|
||||
txIndexRows += 1 + record.accounts.size();
|
||||
|
||||
if (txIndexRows > kIndexRowsPerTxWarningThreshold) {
|
||||
LOG(log_.warn()) << "MPT issuance tx index fanout of " << txIndexRows
|
||||
<< " rows exceeds the expected bound of "
|
||||
<< kIndexRowsPerTxWarningThreshold << " for tx "
|
||||
<< xrpl::strHex(tx.id);
|
||||
}
|
||||
|
||||
indexRowsWritten += txIndexRows;
|
||||
issuanceTxs.insert(
|
||||
issuanceTxs.end(),
|
||||
std::make_move_iterator(txIndexData.begin()),
|
||||
std::make_move_iterator(txIndexData.end())
|
||||
);
|
||||
}
|
||||
|
||||
if (not holders.empty())
|
||||
backend_->writeMPTHolders(holders);
|
||||
|
||||
if (not issuanceTxs.empty()) {
|
||||
backend_->writeMPTokenIssuanceTransactions(issuanceTxs);
|
||||
backend_->writeAccountMPTokenIssuanceTransactions(issuanceTxs);
|
||||
issuanceTxIndexRowsWritten_.get() += static_cast<std::uint64_t>(indexRowsWritten);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace etl::impl
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "etl/Models.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "util/prometheus/Counter.hpp"
|
||||
#include "util/prometheus/Prometheus.hpp"
|
||||
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -10,6 +12,7 @@
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace etl::impl {
|
||||
@@ -18,6 +21,13 @@ class MPTExt {
|
||||
std::shared_ptr<BackendInterface> backend_;
|
||||
util::Logger log_{"ETL"};
|
||||
|
||||
std::reference_wrapper<util::prometheus::CounterInt> issuanceTxIndexRowsWritten_ =
|
||||
PrometheusService::counterInt(
|
||||
"etl_mpt_issuance_tx_index_rows_written_total",
|
||||
{},
|
||||
"Total number of MPT issuance transaction index rows written by ETL"
|
||||
);
|
||||
|
||||
public:
|
||||
explicit MPTExt(std::shared_ptr<BackendInterface> backend);
|
||||
|
||||
@@ -32,7 +42,7 @@ public:
|
||||
|
||||
private:
|
||||
void
|
||||
writeMPTHoldersFromTransactions(model::LedgerData const& data);
|
||||
writeMPTDataFromTransactions(model::LedgerData const& data);
|
||||
};
|
||||
|
||||
} // namespace etl::impl
|
||||
|
||||
@@ -7,6 +7,7 @@ target_sources(
|
||||
util/BinaryTestObject.cpp
|
||||
util/CallWithTimeout.cpp
|
||||
util/LoggerFixtures.cpp
|
||||
util/MPTokenTestObjects.cpp
|
||||
util/MockAssert.cpp
|
||||
util/StringUtils.cpp
|
||||
util/TestHttpClient.cpp
|
||||
|
||||
54
tests/common/util/MPTokenTestObjects.cpp
Normal file
54
tests/common/util/MPTokenTestObjects.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "util/MPTokenTestObjects.hpp"
|
||||
|
||||
#include "util/TestObject.hpp"
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace util {
|
||||
|
||||
xrpl::STObject
|
||||
createMPTokenNode(
|
||||
xrpl::SField const& nodeType,
|
||||
xrpl::uint192 const& issuanceID,
|
||||
std::string_view holder
|
||||
)
|
||||
{
|
||||
auto const& fieldsName =
|
||||
nodeType == xrpl::sfCreatedNode ? xrpl::sfNewFields : xrpl::sfFinalFields;
|
||||
|
||||
xrpl::STObject fields(fieldsName);
|
||||
fields.setAccountID(xrpl::sfAccount, ::getAccountIdWithString(holder));
|
||||
fields[xrpl::sfMPTokenIssuanceID] = issuanceID;
|
||||
|
||||
xrpl::STObject node(nodeType);
|
||||
node.setFieldU16(xrpl::sfLedgerEntryType, xrpl::ltMPTOKEN);
|
||||
node.setFieldH256(xrpl::sfLedgerIndex, xrpl::uint256{});
|
||||
node.set(std::move(fields));
|
||||
return node;
|
||||
}
|
||||
|
||||
xrpl::STObject
|
||||
createMPTokenIssuanceNode(xrpl::SField const& nodeType, std::uint32_t seq, std::string_view issuer)
|
||||
{
|
||||
auto const& fieldsName =
|
||||
nodeType == xrpl::sfCreatedNode ? xrpl::sfNewFields : xrpl::sfFinalFields;
|
||||
|
||||
xrpl::STObject fields(fieldsName);
|
||||
fields.setFieldU32(xrpl::sfSequence, seq);
|
||||
fields.setAccountID(xrpl::sfIssuer, ::getAccountIdWithString(issuer));
|
||||
|
||||
xrpl::STObject node(nodeType);
|
||||
node.setFieldU16(xrpl::sfLedgerEntryType, xrpl::ltMPTOKEN_ISSUANCE);
|
||||
node.setFieldH256(xrpl::sfLedgerIndex, xrpl::uint256{});
|
||||
node.set(std::move(fields));
|
||||
return node;
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
22
tests/common/util/MPTokenTestObjects.hpp
Normal file
22
tests/common/util/MPTokenTestObjects.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
namespace util {
|
||||
|
||||
[[nodiscard]] xrpl::STObject
|
||||
createMPTokenNode(
|
||||
xrpl::SField const& nodeType,
|
||||
xrpl::uint192 const& issuanceID,
|
||||
std::string_view holder
|
||||
);
|
||||
|
||||
[[nodiscard]] xrpl::STObject
|
||||
createMPTokenIssuanceNode(xrpl::SField const& nodeType, std::uint32_t seq, std::string_view issuer);
|
||||
|
||||
} // namespace util
|
||||
@@ -48,6 +48,7 @@ target_sources(
|
||||
etl/LoadBalancerTests.cpp
|
||||
etl/LoadingTests.cpp
|
||||
etl/MonitorTests.cpp
|
||||
etl/MPTHelpersTests.cpp
|
||||
etl/NetworkValidatedLedgersTests.cpp
|
||||
etl/NFTHelpersTests.cpp
|
||||
etl/RegistryTests.cpp
|
||||
|
||||
453
tests/unit/etl/MPTHelpersTests.cpp
Normal file
453
tests/unit/etl/MPTHelpersTests.cpp
Normal file
@@ -0,0 +1,453 @@
|
||||
#include "data/DBHelpers.hpp"
|
||||
#include "etl/MPTHelpers.hpp"
|
||||
#include "util/MPTokenTestObjects.hpp"
|
||||
#include "util/TestObject.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTAmount.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STIssue.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kAccount = "rM2AGCCCRb373FRuD8wHyUwUsh2dV4BW5Q";
|
||||
constexpr auto kAccount2 = "rnd1nHuzceyQDqnLH8urWNr4QBKt4v7WVk";
|
||||
constexpr auto kIssuer = "rK1EX542EgA9m948JrJRaEzwLVEhqWvnr9";
|
||||
constexpr auto kTX = "13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8";
|
||||
constexpr std::uint32_t kIssuanceSeq = 7;
|
||||
constexpr std::uint32_t kLedgerSeq = 99;
|
||||
constexpr std::uint32_t kTxIndex = 4;
|
||||
|
||||
xrpl::Slice const kSlice("test", 4);
|
||||
|
||||
xrpl::uint192
|
||||
defaultIssuanceID()
|
||||
{
|
||||
return xrpl::makeMptID(kIssuanceSeq, getAccountIdWithString(kIssuer));
|
||||
}
|
||||
|
||||
xrpl::STObject
|
||||
createAccountRootNode(std::string_view account)
|
||||
{
|
||||
xrpl::STObject fields(xrpl::sfFinalFields);
|
||||
fields.setAccountID(xrpl::sfAccount, getAccountIdWithString(account));
|
||||
|
||||
xrpl::STObject node(xrpl::sfModifiedNode);
|
||||
node.setFieldU16(xrpl::sfLedgerEntryType, xrpl::ltACCOUNT_ROOT);
|
||||
node.setFieldH256(xrpl::sfLedgerIndex, xrpl::uint256{});
|
||||
node.set(std::move(fields));
|
||||
return node;
|
||||
}
|
||||
|
||||
xrpl::STObject
|
||||
createMPTokenNodeWithoutIssuanceID()
|
||||
{
|
||||
xrpl::STObject fields(xrpl::sfFinalFields);
|
||||
fields.setAccountID(xrpl::sfAccount, getAccountIdWithString(kAccount));
|
||||
|
||||
xrpl::STObject node(xrpl::sfModifiedNode);
|
||||
node.setFieldU16(xrpl::sfLedgerEntryType, xrpl::ltMPTOKEN);
|
||||
node.setFieldH256(xrpl::sfLedgerIndex, xrpl::uint256{});
|
||||
node.set(std::move(fields));
|
||||
return node;
|
||||
}
|
||||
|
||||
xrpl::STObject
|
||||
createMPTokenIssuanceNodeWithoutIssuer()
|
||||
{
|
||||
xrpl::STObject fields(xrpl::sfFinalFields);
|
||||
fields.setFieldU32(xrpl::sfSequence, kIssuanceSeq);
|
||||
|
||||
xrpl::STObject node(xrpl::sfModifiedNode);
|
||||
node.setFieldU16(xrpl::sfLedgerEntryType, xrpl::ltMPTOKEN_ISSUANCE);
|
||||
node.setFieldH256(xrpl::sfLedgerIndex, xrpl::uint256{});
|
||||
node.set(std::move(fields));
|
||||
return node;
|
||||
}
|
||||
|
||||
xrpl::TxMeta
|
||||
createTxMeta(std::vector<xrpl::STObject> nodes, int result = xrpl::tesSUCCESS)
|
||||
{
|
||||
xrpl::STObject metaObj(xrpl::sfTransactionMetaData);
|
||||
metaObj.setFieldU8(xrpl::sfTransactionResult, result);
|
||||
metaObj.setFieldU32(xrpl::sfTransactionIndex, kTxIndex);
|
||||
|
||||
xrpl::STArray affectedNodes(xrpl::sfAffectedNodes);
|
||||
for (auto& node : nodes)
|
||||
affectedNodes.push_back(std::move(node));
|
||||
metaObj.setFieldArray(xrpl::sfAffectedNodes, affectedNodes);
|
||||
|
||||
return xrpl::TxMeta{xrpl::uint256(kTX), kLedgerSeq, metaObj.getSerializer().peekData()};
|
||||
}
|
||||
|
||||
xrpl::STTx
|
||||
createTx(xrpl::TxType type)
|
||||
{
|
||||
xrpl::STObject obj(xrpl::sfTransaction);
|
||||
obj.setFieldU16(xrpl::sfTransactionType, type);
|
||||
obj.setAccountID(xrpl::sfAccount, getAccountIdWithString(kAccount));
|
||||
obj.setFieldAmount(xrpl::sfFee, xrpl::STAmount(10, false));
|
||||
obj.setFieldU32(xrpl::sfSequence, 1);
|
||||
obj.setFieldVL(xrpl::sfSigningPubKey, kSlice);
|
||||
|
||||
// Satisfy the per-type required fields of the SOTemplate
|
||||
switch (type) {
|
||||
case xrpl::ttPAYMENT:
|
||||
obj.setFieldAmount(xrpl::sfAmount, xrpl::STAmount(100, false));
|
||||
obj.setAccountID(xrpl::sfDestination, getAccountIdWithString(kAccount2));
|
||||
break;
|
||||
case xrpl::ttCLAWBACK:
|
||||
obj.setFieldAmount(xrpl::sfAmount, xrpl::STAmount(100, false));
|
||||
break;
|
||||
case xrpl::ttOFFER_CREATE:
|
||||
obj.setFieldAmount(xrpl::sfTakerPays, xrpl::STAmount(100, false));
|
||||
obj.setFieldAmount(xrpl::sfTakerGets, xrpl::STAmount(200, false));
|
||||
break;
|
||||
case xrpl::ttAMM_DEPOSIT:
|
||||
obj.setFieldIssue(xrpl::sfAsset, xrpl::STIssue{xrpl::sfAsset, xrpl::xrpIssue()});
|
||||
obj.setFieldIssue(xrpl::sfAsset2, xrpl::STIssue{xrpl::sfAsset2, xrpl::xrpIssue()});
|
||||
break;
|
||||
case xrpl::ttMPTOKEN_ISSUANCE_DESTROY:
|
||||
case xrpl::ttMPTOKEN_ISSUANCE_SET:
|
||||
obj[xrpl::sfMPTokenIssuanceID] = defaultIssuanceID();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
auto const serialized = obj.getSerializer();
|
||||
return xrpl::STTx{xrpl::SerialIter{serialized.slice()}};
|
||||
}
|
||||
|
||||
xrpl::STTx
|
||||
createPaymentTxWithMPTAmount(xrpl::uint192 const& issuanceID)
|
||||
{
|
||||
xrpl::STObject obj(xrpl::sfTransaction);
|
||||
obj.setFieldU16(xrpl::sfTransactionType, xrpl::ttPAYMENT);
|
||||
obj.setAccountID(xrpl::sfAccount, getAccountIdWithString(kAccount));
|
||||
obj.setFieldAmount(
|
||||
xrpl::sfAmount, xrpl::STAmount(xrpl::MPTAmount{100}, xrpl::MPTIssue{issuanceID})
|
||||
);
|
||||
obj.setFieldAmount(xrpl::sfFee, xrpl::STAmount(10, false));
|
||||
obj.setAccountID(xrpl::sfDestination, getAccountIdWithString(kAccount2));
|
||||
obj.setFieldU32(xrpl::sfSequence, 1);
|
||||
obj.setFieldVL(xrpl::sfSigningPubKey, kSlice);
|
||||
|
||||
auto const serialized = obj.getSerializer();
|
||||
return xrpl::STTx{xrpl::SerialIter{serialized.slice()}};
|
||||
}
|
||||
|
||||
xrpl::STTx
|
||||
createAMMDepositTxWithMPTIssue(xrpl::uint192 const& issuanceID)
|
||||
{
|
||||
xrpl::STObject obj(xrpl::sfTransaction);
|
||||
obj.setFieldU16(xrpl::sfTransactionType, xrpl::ttAMM_DEPOSIT);
|
||||
obj.setAccountID(xrpl::sfAccount, getAccountIdWithString(kAccount));
|
||||
obj.setFieldAmount(xrpl::sfFee, xrpl::STAmount(10, false));
|
||||
obj.setFieldU32(xrpl::sfSequence, 1);
|
||||
obj.setFieldVL(xrpl::sfSigningPubKey, kSlice);
|
||||
obj.setFieldIssue(xrpl::sfAsset, xrpl::STIssue{xrpl::sfAsset, xrpl::MPTIssue{issuanceID}});
|
||||
obj.setFieldIssue(xrpl::sfAsset2, xrpl::STIssue{xrpl::sfAsset2, xrpl::xrpIssue()});
|
||||
|
||||
auto const serialized = obj.getSerializer();
|
||||
return xrpl::STTx{xrpl::SerialIter{serialized.slice()}};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct MPTHelpersTest : virtual public ::testing::Test {
|
||||
protected:
|
||||
static void
|
||||
verifyCommonFields(
|
||||
MPTokenIssuanceTransactionsData const& data,
|
||||
xrpl::STTx const& sttx,
|
||||
xrpl::TxMeta const& txMeta
|
||||
)
|
||||
{
|
||||
EXPECT_EQ(data.accounts, txMeta.getAffectedAccounts());
|
||||
EXPECT_EQ(data.ledgerSequence, txMeta.getLgrSeq());
|
||||
EXPECT_EQ(data.transactionIndex, txMeta.getIndex());
|
||||
EXPECT_EQ(data.txHash, sttx.getTransactionID());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(MPTHelpersTest, FailedTxWithoutIssuanceReferenceProducesNoRecords)
|
||||
{
|
||||
std::vector<xrpl::STObject> nodes;
|
||||
auto const txMeta = createTxMeta(std::move(nodes), xrpl::tecINCOMPLETE);
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, createTx(xrpl::ttPAYMENT));
|
||||
|
||||
EXPECT_TRUE(records.empty());
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, FailedTxIgnoresMPTAffectedNodes)
|
||||
{
|
||||
std::vector<xrpl::STObject> nodes;
|
||||
nodes.push_back(util::createMPTokenIssuanceNode(xrpl::sfModifiedNode, kIssuanceSeq, kIssuer));
|
||||
nodes.push_back(util::createMPTokenNode(xrpl::sfCreatedNode, defaultIssuanceID(), kAccount));
|
||||
auto const txMeta = createTxMeta(std::move(nodes), xrpl::tecINCOMPLETE);
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, createTx(xrpl::ttPAYMENT));
|
||||
|
||||
EXPECT_TRUE(records.empty());
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, FailedTxWithTopLevelIssuanceIDProducesRecord)
|
||||
{
|
||||
auto const txMeta = createTxMeta({}, xrpl::tecINCOMPLETE);
|
||||
auto const sttx = createTx(xrpl::ttMPTOKEN_ISSUANCE_SET);
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, sttx);
|
||||
|
||||
ASSERT_EQ(records.size(), 1);
|
||||
EXPECT_EQ(records[0].mptIssuanceID, defaultIssuanceID());
|
||||
verifyCommonFields(records[0], sttx, txMeta);
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, FailedTxWithMPTAmountProducesRecord)
|
||||
{
|
||||
auto const txMeta = createTxMeta({}, xrpl::tecINCOMPLETE);
|
||||
auto const sttx = createPaymentTxWithMPTAmount(defaultIssuanceID());
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, sttx);
|
||||
|
||||
ASSERT_EQ(records.size(), 1);
|
||||
EXPECT_EQ(records[0].mptIssuanceID, defaultIssuanceID());
|
||||
verifyCommonFields(records[0], sttx, txMeta);
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, FailedTxWithMPTIssueProducesRecord)
|
||||
{
|
||||
auto const txMeta = createTxMeta({}, xrpl::tecINCOMPLETE);
|
||||
auto const sttx = createAMMDepositTxWithMPTIssue(defaultIssuanceID());
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, sttx);
|
||||
|
||||
ASSERT_EQ(records.size(), 1);
|
||||
EXPECT_EQ(records[0].mptIssuanceID, defaultIssuanceID());
|
||||
verifyCommonFields(records[0], sttx, txMeta);
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, IssuanceCreateProducesRecordWithReconstructedID)
|
||||
{
|
||||
auto const tx = createMPTIssuanceCreateTxWithMetadata(kIssuer, 2, kIssuanceSeq);
|
||||
xrpl::TxMeta const txMeta(xrpl::uint256(kTX), 1, tx.metadata);
|
||||
auto const sttx = xrpl::STTx(xrpl::SerialIter{tx.transaction.data(), tx.transaction.size()});
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, sttx);
|
||||
|
||||
ASSERT_EQ(records.size(), 1);
|
||||
EXPECT_EQ(records[0].mptIssuanceID, defaultIssuanceID());
|
||||
verifyCommonFields(records[0], sttx, txMeta);
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, IssuanceDestroyProducesRecordFromDeletedNode)
|
||||
{
|
||||
std::vector<xrpl::STObject> nodes;
|
||||
nodes.push_back(util::createMPTokenIssuanceNode(xrpl::sfDeletedNode, kIssuanceSeq, kIssuer));
|
||||
auto const txMeta = createTxMeta(std::move(nodes));
|
||||
auto const sttx = createTx(xrpl::ttMPTOKEN_ISSUANCE_DESTROY);
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, sttx);
|
||||
|
||||
ASSERT_EQ(records.size(), 1);
|
||||
EXPECT_EQ(records[0].mptIssuanceID, defaultIssuanceID());
|
||||
verifyCommonFields(records[0], sttx, txMeta);
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, IssuanceSetProducesRecordFromModifiedNode)
|
||||
{
|
||||
std::vector<xrpl::STObject> nodes;
|
||||
nodes.push_back(util::createMPTokenIssuanceNode(xrpl::sfModifiedNode, kIssuanceSeq, kIssuer));
|
||||
auto const txMeta = createTxMeta(std::move(nodes));
|
||||
auto const sttx = createTx(xrpl::ttMPTOKEN_ISSUANCE_SET);
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, sttx);
|
||||
|
||||
ASSERT_EQ(records.size(), 1);
|
||||
EXPECT_EQ(records[0].mptIssuanceID, defaultIssuanceID());
|
||||
verifyCommonFields(records[0], sttx, txMeta);
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, AuthorizeProducesRecordFromMPTokenNode)
|
||||
{
|
||||
auto const issuanceID = defaultIssuanceID();
|
||||
auto const tx = createMPTokenAuthorizeTxWithMetadata(kAccount, issuanceID, 2, 3);
|
||||
xrpl::TxMeta const txMeta(xrpl::uint256(kTX), 1, tx.metadata);
|
||||
auto const sttx = xrpl::STTx(xrpl::SerialIter{tx.transaction.data(), tx.transaction.size()});
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, sttx);
|
||||
|
||||
ASSERT_EQ(records.size(), 1);
|
||||
EXPECT_EQ(records[0].mptIssuanceID, issuanceID);
|
||||
verifyCommonFields(records[0], sttx, txMeta);
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, DedupsAcrossMPTokenAndIssuanceNodes)
|
||||
{
|
||||
// Both nodes resolve to the same issuance ID: the MPToken node carries it directly while the
|
||||
// MPTokenIssuance node requires reconstruction via makeMptID.
|
||||
std::vector<xrpl::STObject> nodes;
|
||||
nodes.push_back(util::createMPTokenNode(xrpl::sfCreatedNode, defaultIssuanceID(), kAccount));
|
||||
nodes.push_back(util::createMPTokenIssuanceNode(xrpl::sfModifiedNode, kIssuanceSeq, kIssuer));
|
||||
auto const txMeta = createTxMeta(std::move(nodes));
|
||||
auto const sttx = createTx(xrpl::ttPAYMENT);
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, sttx);
|
||||
|
||||
ASSERT_EQ(records.size(), 1);
|
||||
EXPECT_EQ(records[0].mptIssuanceID, defaultIssuanceID());
|
||||
EXPECT_TRUE(records[0].accounts.contains(getAccountIdWithString(kAccount)));
|
||||
EXPECT_TRUE(records[0].accounts.contains(getAccountIdWithString(kIssuer)));
|
||||
verifyCommonFields(records[0], sttx, txMeta);
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, MultipleIssuancesFanOutAndDedup)
|
||||
{
|
||||
auto const issuanceA = xrpl::makeMptID(1, getAccountIdWithString(kIssuer));
|
||||
auto const issuanceB = xrpl::makeMptID(2, getAccountIdWithString(kIssuer));
|
||||
ASSERT_LT(issuanceA, issuanceB);
|
||||
|
||||
// issuanceA is touched twice and should produce only one index record.
|
||||
std::vector<xrpl::STObject> nodes;
|
||||
nodes.push_back(util::createMPTokenNode(xrpl::sfCreatedNode, issuanceB, kAccount));
|
||||
nodes.push_back(util::createMPTokenNode(xrpl::sfModifiedNode, issuanceA, kAccount2));
|
||||
nodes.push_back(util::createMPTokenNode(xrpl::sfDeletedNode, issuanceA, kAccount));
|
||||
auto const txMeta = createTxMeta(std::move(nodes));
|
||||
auto const sttx = createTx(xrpl::ttPAYMENT);
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, sttx);
|
||||
|
||||
ASSERT_EQ(records.size(), 2);
|
||||
EXPECT_EQ(records[0].mptIssuanceID, issuanceA);
|
||||
EXPECT_EQ(records[1].mptIssuanceID, issuanceB);
|
||||
for (auto const& record : records) {
|
||||
EXPECT_EQ(record.accounts, txMeta.getAffectedAccounts());
|
||||
verifyCommonFields(record, sttx, txMeta);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, IndexesMPTNodesRegardlessOfTransactionType)
|
||||
{
|
||||
constexpr xrpl::TxType kTypes[] = {
|
||||
xrpl::ttPAYMENT, xrpl::ttCLAWBACK, xrpl::ttOFFER_CREATE, xrpl::ttAMM_DEPOSIT
|
||||
};
|
||||
|
||||
for (auto const type : kTypes) {
|
||||
std::vector<xrpl::STObject> nodes;
|
||||
nodes.push_back(
|
||||
util::createMPTokenNode(xrpl::sfModifiedNode, defaultIssuanceID(), kAccount)
|
||||
);
|
||||
auto const txMeta = createTxMeta(std::move(nodes));
|
||||
auto const sttx = createTx(type);
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, sttx);
|
||||
|
||||
ASSERT_EQ(records.size(), 1) << "TransactionType " << type;
|
||||
EXPECT_EQ(records[0].mptIssuanceID, defaultIssuanceID());
|
||||
verifyCommonFields(records[0], sttx, txMeta);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, MPTNodeWithoutFieldsProducesNoRecords)
|
||||
{
|
||||
// A ModifiedNode carrying no FinalFields must be skipped without crashing.
|
||||
xrpl::STObject node(xrpl::sfModifiedNode);
|
||||
node.setFieldU16(xrpl::sfLedgerEntryType, xrpl::ltMPTOKEN_ISSUANCE);
|
||||
node.setFieldH256(xrpl::sfLedgerIndex, xrpl::uint256{});
|
||||
|
||||
std::vector<xrpl::STObject> nodes;
|
||||
nodes.push_back(std::move(node));
|
||||
auto const txMeta = createTxMeta(std::move(nodes));
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, createTx(xrpl::ttPAYMENT));
|
||||
|
||||
EXPECT_TRUE(records.empty());
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, MPTNodesMissingRequiredFieldsProduceNoRecords)
|
||||
{
|
||||
std::vector<xrpl::STObject> nodes;
|
||||
nodes.push_back(createMPTokenNodeWithoutIssuanceID());
|
||||
nodes.push_back(createMPTokenIssuanceNodeWithoutIssuer());
|
||||
auto const txMeta = createTxMeta(std::move(nodes));
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, createTx(xrpl::ttPAYMENT));
|
||||
|
||||
EXPECT_TRUE(records.empty());
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, NoMPTNodesProducesNoRecords)
|
||||
{
|
||||
std::vector<xrpl::STObject> nodes;
|
||||
nodes.push_back(createAccountRootNode(kAccount));
|
||||
auto const txMeta = createTxMeta(std::move(nodes));
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, createTx(xrpl::ttPAYMENT));
|
||||
|
||||
EXPECT_TRUE(records.empty());
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, RecordCarriesAllAffectedAccounts)
|
||||
{
|
||||
std::vector<xrpl::STObject> nodes;
|
||||
nodes.push_back(util::createMPTokenNode(xrpl::sfCreatedNode, defaultIssuanceID(), kAccount));
|
||||
nodes.push_back(util::createMPTokenIssuanceNode(xrpl::sfModifiedNode, kIssuanceSeq, kIssuer));
|
||||
nodes.push_back(createAccountRootNode(kAccount2));
|
||||
auto const txMeta = createTxMeta(std::move(nodes));
|
||||
auto const sttx = createTx(xrpl::ttPAYMENT);
|
||||
|
||||
auto const records = etl::getMPTokenIssuanceTxsFromTx(txMeta, sttx);
|
||||
|
||||
ASSERT_EQ(records.size(), 1);
|
||||
EXPECT_EQ(records[0].accounts, txMeta.getAffectedAccounts());
|
||||
EXPECT_EQ(records[0].accounts.size(), 3);
|
||||
EXPECT_TRUE(records[0].accounts.contains(getAccountIdWithString(kAccount)));
|
||||
EXPECT_TRUE(records[0].accounts.contains(getAccountIdWithString(kAccount2)));
|
||||
EXPECT_TRUE(records[0].accounts.contains(getAccountIdWithString(kIssuer)));
|
||||
}
|
||||
|
||||
TEST_F(MPTHelpersTest, ExtractionIsDeterministic)
|
||||
{
|
||||
auto const issuanceA = xrpl::makeMptID(1, getAccountIdWithString(kIssuer));
|
||||
|
||||
std::vector<xrpl::STObject> nodes;
|
||||
nodes.push_back(util::createMPTokenNode(xrpl::sfCreatedNode, issuanceA, kAccount));
|
||||
nodes.push_back(util::createMPTokenIssuanceNode(xrpl::sfModifiedNode, kIssuanceSeq, kIssuer));
|
||||
auto const txMeta = createTxMeta(std::move(nodes));
|
||||
auto const sttx = createTx(xrpl::ttPAYMENT);
|
||||
|
||||
auto const first = etl::getMPTokenIssuanceTxsFromTx(txMeta, sttx);
|
||||
auto const second = etl::getMPTokenIssuanceTxsFromTx(txMeta, sttx);
|
||||
|
||||
ASSERT_EQ(first.size(), 2);
|
||||
ASSERT_EQ(first.size(), second.size());
|
||||
for (std::size_t i = 0; i < first.size(); ++i) {
|
||||
EXPECT_EQ(first[i].mptIssuanceID, second[i].mptIssuanceID);
|
||||
EXPECT_EQ(first[i].accounts, second[i].accounts);
|
||||
EXPECT_EQ(first[i].ledgerSequence, second[i].ledgerSequence);
|
||||
EXPECT_EQ(first[i].transactionIndex, second[i].transactionIndex);
|
||||
EXPECT_EQ(first[i].txHash, second[i].txHash);
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,21 @@
|
||||
#include "etl/impl/ext/MPT.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "util/BinaryTestObject.hpp"
|
||||
#include "util/MPTokenTestObjects.hpp"
|
||||
#include "util/MockBackendTestFixture.hpp"
|
||||
#include "util/MockPrometheus.hpp"
|
||||
#include "util/TestObject.hpp"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
@@ -21,8 +27,10 @@
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -68,6 +76,125 @@ constinit auto const kHash = "6005B465CBBF7FA8E41AC0C0CD38491026D9411FCB7BA46E2A
|
||||
constinit auto const kHash2 = "6005B465CBBF7FA8E41AC0C0CD38491026D9411FCB7BA46E2AEBB3AF7654261C";
|
||||
constinit auto const kHash3 = "6005B465CBBF7FA8E41AC0C0CD38491026D9411FCB7BA46E2AEBB3AF7654261D";
|
||||
|
||||
// The issuance ID carried by the ltMPTOKEN CreatedNode in kTxnMeta.
|
||||
constinit auto const kIssuanceID = "002DBD1817E0AF9FDE4F9978B8FCD8A5063630B5737DA605";
|
||||
|
||||
constinit auto const kAccount = "rM2AGCCCRb373FRuD8wHyUwUsh2dV4BW5Q";
|
||||
constinit auto const kAccount2 = "rnd1nHuzceyQDqnLH8urWNr4QBKt4v7WVk";
|
||||
constexpr auto kHighFanoutAccountCount = 1001u;
|
||||
|
||||
xrpl::AccountID
|
||||
accountIDFromSeed(std::uint32_t seed)
|
||||
{
|
||||
std::array<unsigned char, xrpl::AccountID::size()> bytes{};
|
||||
bytes[16] = static_cast<unsigned char>(seed >> 24);
|
||||
bytes[17] = static_cast<unsigned char>(seed >> 16);
|
||||
bytes[18] = static_cast<unsigned char>(seed >> 8);
|
||||
bytes[19] = static_cast<unsigned char>(seed);
|
||||
return xrpl::AccountID::fromVoid(bytes.data());
|
||||
}
|
||||
|
||||
xrpl::STObject
|
||||
createAccountRootNode(xrpl::AccountID const& account)
|
||||
{
|
||||
xrpl::STObject fields(xrpl::sfFinalFields);
|
||||
fields.setAccountID(xrpl::sfAccount, account);
|
||||
|
||||
xrpl::STObject node(xrpl::sfModifiedNode);
|
||||
node.setFieldU16(xrpl::sfLedgerEntryType, xrpl::ltACCOUNT_ROOT);
|
||||
node.setFieldH256(xrpl::sfLedgerIndex, xrpl::uint256{});
|
||||
node.set(std::move(fields));
|
||||
return node;
|
||||
}
|
||||
|
||||
// One Payment transaction touching two distinct issuances with three affected accounts.
|
||||
etl::model::Transaction
|
||||
createMultiIssuanceTransaction()
|
||||
{
|
||||
xrpl::Slice const slice("test", 4);
|
||||
xrpl::STObject tx(xrpl::sfTransaction);
|
||||
tx.setFieldU16(xrpl::sfTransactionType, xrpl::ttPAYMENT);
|
||||
tx.setAccountID(xrpl::sfAccount, getAccountIdWithString(kAccount));
|
||||
tx.setFieldAmount(xrpl::sfAmount, xrpl::STAmount(100, false));
|
||||
tx.setFieldAmount(xrpl::sfFee, xrpl::STAmount(10, false));
|
||||
tx.setAccountID(xrpl::sfDestination, getAccountIdWithString(kAccount2));
|
||||
tx.setFieldU32(xrpl::sfSequence, 1);
|
||||
tx.setFieldVL(xrpl::sfSigningPubKey, slice);
|
||||
|
||||
auto const serialized = tx.getSerializer();
|
||||
auto const sttx = xrpl::STTx{xrpl::SerialIter{serialized.slice()}};
|
||||
|
||||
auto const issuanceA = xrpl::makeMptID(1, getAccountIdWithString(kHolderAccount));
|
||||
auto const issuanceB = xrpl::makeMptID(2, getAccountIdWithString(kHolderAccount));
|
||||
|
||||
xrpl::STObject metaObj(xrpl::sfTransactionMetaData);
|
||||
metaObj.setFieldU8(xrpl::sfTransactionResult, xrpl::tesSUCCESS);
|
||||
metaObj.setFieldU32(xrpl::sfTransactionIndex, 0);
|
||||
|
||||
xrpl::STArray affectedNodes(xrpl::sfAffectedNodes);
|
||||
affectedNodes.push_back(util::createMPTokenNode(xrpl::sfModifiedNode, issuanceA, kAccount));
|
||||
affectedNodes.push_back(util::createMPTokenNode(xrpl::sfModifiedNode, issuanceB, kAccount2));
|
||||
affectedNodes.push_back(
|
||||
util::createMPTokenIssuanceNode(xrpl::sfModifiedNode, 1, kHolderAccount)
|
||||
); // issuanceA again
|
||||
metaObj.setFieldArray(xrpl::sfAffectedNodes, affectedNodes);
|
||||
|
||||
auto const txMeta =
|
||||
xrpl::TxMeta{sttx.getTransactionID(), kSeq, metaObj.getSerializer().peekData()};
|
||||
|
||||
return etl::model::Transaction{
|
||||
.raw = "",
|
||||
.metaRaw = "",
|
||||
.sttx = sttx,
|
||||
.meta = txMeta,
|
||||
.id = sttx.getTransactionID(),
|
||||
.key = "0000000000000000000000000000000000000000000000000000000000000002",
|
||||
.type = sttx.getTxnType()
|
||||
};
|
||||
}
|
||||
|
||||
etl::model::Transaction
|
||||
createHighFanoutIssuanceTransaction()
|
||||
{
|
||||
xrpl::Slice const slice("test", 4);
|
||||
xrpl::STObject tx(xrpl::sfTransaction);
|
||||
tx.setFieldU16(xrpl::sfTransactionType, xrpl::ttPAYMENT);
|
||||
tx.setAccountID(xrpl::sfAccount, getAccountIdWithString(kAccount));
|
||||
tx.setFieldAmount(xrpl::sfAmount, xrpl::STAmount(100, false));
|
||||
tx.setFieldAmount(xrpl::sfFee, xrpl::STAmount(10, false));
|
||||
tx.setAccountID(xrpl::sfDestination, getAccountIdWithString(kAccount2));
|
||||
tx.setFieldU32(xrpl::sfSequence, 1);
|
||||
tx.setFieldVL(xrpl::sfSigningPubKey, slice);
|
||||
|
||||
auto const serialized = tx.getSerializer();
|
||||
auto const sttx = xrpl::STTx{xrpl::SerialIter{serialized.slice()}};
|
||||
|
||||
xrpl::STObject metaObj(xrpl::sfTransactionMetaData);
|
||||
metaObj.setFieldU8(xrpl::sfTransactionResult, xrpl::tesSUCCESS);
|
||||
metaObj.setFieldU32(xrpl::sfTransactionIndex, 0);
|
||||
|
||||
xrpl::STArray affectedNodes(xrpl::sfAffectedNodes);
|
||||
affectedNodes.push_back(
|
||||
util::createMPTokenNode(xrpl::sfModifiedNode, xrpl::uint192{kMptIssuanceID}, kHolderAccount)
|
||||
);
|
||||
for (std::uint32_t i = 0; i < kHighFanoutAccountCount; ++i)
|
||||
affectedNodes.push_back(createAccountRootNode(accountIDFromSeed(i + 1)));
|
||||
metaObj.setFieldArray(xrpl::sfAffectedNodes, affectedNodes);
|
||||
|
||||
auto const txMeta =
|
||||
xrpl::TxMeta{sttx.getTransactionID(), kSeq, metaObj.getSerializer().peekData()};
|
||||
|
||||
return etl::model::Transaction{
|
||||
.raw = "",
|
||||
.metaRaw = "",
|
||||
.sttx = sttx,
|
||||
.meta = txMeta,
|
||||
.id = sttx.getTransactionID(),
|
||||
.key = "0000000000000000000000000000000000000000000000000000000000000003",
|
||||
.type = sttx.getTxnType()
|
||||
};
|
||||
}
|
||||
|
||||
auto
|
||||
createTransactionFromObjects(
|
||||
xrpl::STObject const& txObj,
|
||||
@@ -92,21 +219,6 @@ createTransactionFromObjects(
|
||||
};
|
||||
}
|
||||
|
||||
xrpl::STObject
|
||||
createNewMPTokenNode(std::string_view holder)
|
||||
{
|
||||
xrpl::STObject newFields(xrpl::sfNewFields);
|
||||
newFields.setFieldU16(xrpl::sfLedgerEntryType, xrpl::ltMPTOKEN);
|
||||
newFields[xrpl::sfMPTokenIssuanceID] = xrpl::uint192{kMptIssuanceID};
|
||||
newFields.setAccountID(xrpl::sfAccount, getAccountIdWithString(holder));
|
||||
|
||||
xrpl::STObject createdNode(xrpl::sfCreatedNode);
|
||||
createdNode.setFieldU16(xrpl::sfLedgerEntryType, xrpl::ltMPTOKEN);
|
||||
createdNode.setFieldH256(xrpl::sfLedgerIndex, xrpl::uint256{});
|
||||
createdNode.set(std::move(newFields));
|
||||
return createdNode;
|
||||
}
|
||||
|
||||
xrpl::STObject
|
||||
createPaymentMetaWithNewMPTokens(xrpl::TER result = xrpl::tesSUCCESS)
|
||||
{
|
||||
@@ -115,8 +227,12 @@ createPaymentMetaWithNewMPTokens(xrpl::TER result = xrpl::tesSUCCESS)
|
||||
metaObj.setFieldU32(xrpl::sfTransactionIndex, 0);
|
||||
|
||||
xrpl::STArray affectedNodes(xrpl::sfAffectedNodes);
|
||||
affectedNodes.push_back(createNewMPTokenNode(kHolderAccount));
|
||||
affectedNodes.push_back(createNewMPTokenNode(kHolderAccount2));
|
||||
affectedNodes.push_back(
|
||||
util::createMPTokenNode(xrpl::sfCreatedNode, xrpl::uint192{kMptIssuanceID}, kHolderAccount)
|
||||
);
|
||||
affectedNodes.push_back(
|
||||
util::createMPTokenNode(xrpl::sfCreatedNode, xrpl::uint192{kMptIssuanceID}, kHolderAccount2)
|
||||
);
|
||||
metaObj.setFieldArray(xrpl::sfAffectedNodes, affectedNodes);
|
||||
|
||||
return metaObj;
|
||||
@@ -168,6 +284,7 @@ createTestDataWithoutMPToken()
|
||||
auto
|
||||
createTestData()
|
||||
{
|
||||
// Only the AUTHORIZE transaction carries metadata with MPT affected nodes.
|
||||
auto transactions = std::vector{
|
||||
util::createTransaction(
|
||||
xrpl::TxType::ttMPTOKEN_ISSUANCE_CREATE
|
||||
@@ -191,13 +308,33 @@ createTestData()
|
||||
};
|
||||
}
|
||||
|
||||
// Same AUTHORIZE fixture as kTxnMeta, with a distinct transaction index.
|
||||
etl::model::Transaction
|
||||
createAuthorizeTransactionWithIndex(std::string const& hashStr, std::uint32_t txIndex)
|
||||
{
|
||||
auto tx =
|
||||
util::createTransaction(xrpl::TxType::ttMPTOKEN_AUTHORIZE, hashStr, kTxnMeta, kTxnHex);
|
||||
|
||||
auto const metaBlob = xrpl::strUnHex(kTxnMeta);
|
||||
EXPECT_TRUE(metaBlob.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
xrpl::SerialIter sitMeta{xrpl::makeSlice(*metaBlob)};
|
||||
xrpl::STObject metaObj{sitMeta, xrpl::sfMetadata};
|
||||
metaObj.setFieldU32(xrpl::sfTransactionIndex, txIndex);
|
||||
|
||||
xrpl::uint256 hash;
|
||||
EXPECT_TRUE(hash.parseHex(hashStr));
|
||||
tx.meta = xrpl::TxMeta{hash, kSeq, metaObj.getSerializer().peekData()};
|
||||
return tx;
|
||||
}
|
||||
|
||||
auto
|
||||
createMultipleHoldersTestData()
|
||||
{
|
||||
auto transactions = std::vector{
|
||||
util::createTransaction(xrpl::TxType::ttMPTOKEN_AUTHORIZE, kHash, kTxnMeta, kTxnHex),
|
||||
util::createTransaction(xrpl::TxType::ttMPTOKEN_AUTHORIZE, kHash2, kTxnMeta, kTxnHex),
|
||||
util::createTransaction(xrpl::TxType::ttMPTOKEN_AUTHORIZE, kHash3, kTxnMeta, kTxnHex)
|
||||
createAuthorizeTransactionWithIndex(kHash, 0),
|
||||
createAuthorizeTransactionWithIndex(kHash2, 1),
|
||||
createAuthorizeTransactionWithIndex(kHash3, 2)
|
||||
};
|
||||
|
||||
auto const header = createLedgerHeader(kLedgerHash, kSeq);
|
||||
@@ -227,7 +364,20 @@ TEST_F(MPTExtTests, OnLedgerDataFiltersAndWritesMPTs)
|
||||
EXPECT_EQ(holders.size(), 1); // Only metadata creating an MPToken is written
|
||||
});
|
||||
|
||||
std::vector<MPTokenIssuanceTransactionsData> issuanceTxs;
|
||||
std::vector<MPTokenIssuanceTransactionsData> accountIssuanceTxs;
|
||||
EXPECT_CALL(*backend_, writeMPTokenIssuanceTransactions).WillOnce(SaveArg<0>(&issuanceTxs));
|
||||
EXPECT_CALL(*backend_, writeAccountMPTokenIssuanceTransactions)
|
||||
.WillOnce(SaveArg<0>(&accountIssuanceTxs));
|
||||
|
||||
ext_.onLedgerData(data);
|
||||
|
||||
// Only the AUTHORIZE fixture touches an MPT object.
|
||||
ASSERT_EQ(issuanceTxs.size(), 1);
|
||||
EXPECT_EQ(issuanceTxs[0].mptIssuanceID, xrpl::uint192(kIssuanceID));
|
||||
EXPECT_FALSE(issuanceTxs[0].accounts.empty());
|
||||
EXPECT_TRUE(issuanceTxs[0].accounts.contains(getAccountIdWithString(kHolderAccount)));
|
||||
EXPECT_EQ(issuanceTxs, accountIssuanceTxs); // same vector goes to both tables
|
||||
}
|
||||
|
||||
TEST_F(MPTExtTests, OnInitialDataFiltersAndWritesMPTs)
|
||||
@@ -238,7 +388,20 @@ TEST_F(MPTExtTests, OnInitialDataFiltersAndWritesMPTs)
|
||||
EXPECT_EQ(holders.size(), 1); // Only metadata creating an MPToken is written
|
||||
});
|
||||
|
||||
std::vector<MPTokenIssuanceTransactionsData> issuanceTxs;
|
||||
std::vector<MPTokenIssuanceTransactionsData> accountIssuanceTxs;
|
||||
EXPECT_CALL(*backend_, writeMPTokenIssuanceTransactions).WillOnce(SaveArg<0>(&issuanceTxs));
|
||||
EXPECT_CALL(*backend_, writeAccountMPTokenIssuanceTransactions)
|
||||
.WillOnce(SaveArg<0>(&accountIssuanceTxs));
|
||||
|
||||
ext_.onInitialData(data);
|
||||
|
||||
// Only the AUTHORIZE fixture touches an MPT object.
|
||||
ASSERT_EQ(issuanceTxs.size(), 1);
|
||||
EXPECT_EQ(issuanceTxs[0].mptIssuanceID, xrpl::uint192(kIssuanceID));
|
||||
EXPECT_FALSE(issuanceTxs[0].accounts.empty());
|
||||
EXPECT_TRUE(issuanceTxs[0].accounts.contains(getAccountIdWithString(kHolderAccount)));
|
||||
EXPECT_EQ(issuanceTxs, accountIssuanceTxs); // same vector goes to both tables
|
||||
}
|
||||
|
||||
TEST_F(MPTExtTests, OnInitialObjectWritesMPT)
|
||||
@@ -257,16 +420,131 @@ TEST_F(MPTExtTests, OnInitialDataWithMultipleHolders)
|
||||
auto const data = createMultipleHoldersTestData();
|
||||
|
||||
EXPECT_CALL(*backend_, writeMPTHolders).WillOnce([](auto const& holders) {
|
||||
EXPECT_EQ(holders.size(), 3); // Expect all three AUTHORIZE transactions
|
||||
EXPECT_EQ(holders.size(), 3); // All three AUTHORIZE transactions
|
||||
|
||||
auto const expectedAccount =
|
||||
rpc::accountFromStringStrict(kHolderAccount); // Expect all three to be the same
|
||||
rpc::accountFromStringStrict(kHolderAccount); // Same holder in each fixture
|
||||
EXPECT_TRUE(std::ranges::all_of(holders, [&expectedAccount](auto const& data) {
|
||||
return data.holder == expectedAccount;
|
||||
}));
|
||||
});
|
||||
|
||||
std::vector<MPTokenIssuanceTransactionsData> issuanceTxs;
|
||||
std::vector<MPTokenIssuanceTransactionsData> accountIssuanceTxs;
|
||||
EXPECT_CALL(*backend_, writeMPTokenIssuanceTransactions).WillOnce(SaveArg<0>(&issuanceTxs));
|
||||
EXPECT_CALL(*backend_, writeAccountMPTokenIssuanceTransactions)
|
||||
.WillOnce(SaveArg<0>(&accountIssuanceTxs));
|
||||
|
||||
ext_.onInitialData(data);
|
||||
|
||||
// One record per AUTHORIZE transaction; each has a distinct transaction index.
|
||||
ASSERT_EQ(issuanceTxs.size(), 3);
|
||||
EXPECT_TRUE(std::ranges::all_of(issuanceTxs, [](auto const& record) {
|
||||
return record.mptIssuanceID == xrpl::uint192(kIssuanceID);
|
||||
}));
|
||||
std::vector<std::uint32_t> indices;
|
||||
std::ranges::transform(issuanceTxs, std::back_inserter(indices), [](auto const& record) {
|
||||
return record.transactionIndex;
|
||||
});
|
||||
EXPECT_THAT(indices, UnorderedElementsAre(0, 1, 2));
|
||||
EXPECT_EQ(issuanceTxs, accountIssuanceTxs);
|
||||
}
|
||||
|
||||
TEST_F(MPTExtTests, NoMPTTransactionsWritesNothing)
|
||||
{
|
||||
auto transactions = std::vector{
|
||||
util::createTransaction(xrpl::TxType::ttAMM_CREATE),
|
||||
util::createTransaction(xrpl::TxType::ttAMM_CREATE)
|
||||
};
|
||||
|
||||
auto const header = createLedgerHeader(kLedgerHash, kSeq);
|
||||
auto const data = etl::model::LedgerData{
|
||||
.transactions = std::move(transactions),
|
||||
.objects = {},
|
||||
.successors = {},
|
||||
.edgeKeys = {},
|
||||
.header = header,
|
||||
.rawHeader = {},
|
||||
.seq = kSeq
|
||||
};
|
||||
|
||||
EXPECT_CALL(*backend_, writeMPTHolders).Times(0);
|
||||
EXPECT_CALL(*backend_, writeMPTokenIssuanceTransactions).Times(0);
|
||||
EXPECT_CALL(*backend_, writeAccountMPTokenIssuanceTransactions).Times(0);
|
||||
|
||||
ext_.onLedgerData(data);
|
||||
}
|
||||
|
||||
TEST_F(MPTExtTests, OnLedgerDataDedupsMultiIssuanceFanout)
|
||||
{
|
||||
auto transactions = std::vector{createMultiIssuanceTransaction()};
|
||||
|
||||
auto const header = createLedgerHeader(kLedgerHash, kSeq);
|
||||
auto const data = etl::model::LedgerData{
|
||||
.transactions = std::move(transactions),
|
||||
.objects = {},
|
||||
.successors = {},
|
||||
.edgeKeys = {},
|
||||
.header = header,
|
||||
.rawHeader = {},
|
||||
.seq = kSeq
|
||||
};
|
||||
|
||||
std::vector<MPTokenIssuanceTransactionsData> issuanceTxs;
|
||||
std::vector<MPTokenIssuanceTransactionsData> accountIssuanceTxs;
|
||||
EXPECT_CALL(*backend_, writeMPTokenIssuanceTransactions).WillOnce(SaveArg<0>(&issuanceTxs));
|
||||
EXPECT_CALL(*backend_, writeAccountMPTokenIssuanceTransactions)
|
||||
.WillOnce(SaveArg<0>(&accountIssuanceTxs));
|
||||
|
||||
ext_.onLedgerData(data);
|
||||
|
||||
// issuanceA is touched twice, issuanceB once; each record carries the full account set.
|
||||
auto const issuanceA = xrpl::makeMptID(1, getAccountIdWithString(kHolderAccount));
|
||||
auto const issuanceB = xrpl::makeMptID(2, getAccountIdWithString(kHolderAccount));
|
||||
|
||||
ASSERT_EQ(issuanceTxs.size(), 2);
|
||||
EXPECT_EQ(issuanceTxs[0].mptIssuanceID, issuanceA);
|
||||
EXPECT_EQ(issuanceTxs[1].mptIssuanceID, issuanceB);
|
||||
for (auto const& record : issuanceTxs) {
|
||||
EXPECT_EQ(record.accounts.size(), 3);
|
||||
EXPECT_TRUE(record.accounts.contains(getAccountIdWithString(kAccount)));
|
||||
EXPECT_TRUE(record.accounts.contains(getAccountIdWithString(kAccount2)));
|
||||
EXPECT_TRUE(record.accounts.contains(getAccountIdWithString(kHolderAccount)));
|
||||
EXPECT_EQ(record.ledgerSequence, kSeq);
|
||||
}
|
||||
EXPECT_EQ(issuanceTxs, accountIssuanceTxs);
|
||||
}
|
||||
|
||||
TEST_F(MPTExtTests, OnLedgerDataWritesHighFanoutIssuanceIndexWithoutHolders)
|
||||
{
|
||||
auto transactions = std::vector{createHighFanoutIssuanceTransaction()};
|
||||
|
||||
auto const header = createLedgerHeader(kLedgerHash, kSeq);
|
||||
auto const data = etl::model::LedgerData{
|
||||
.transactions = std::move(transactions),
|
||||
.objects = {},
|
||||
.successors = {},
|
||||
.edgeKeys = {},
|
||||
.header = header,
|
||||
.rawHeader = {},
|
||||
.seq = kSeq
|
||||
};
|
||||
|
||||
EXPECT_CALL(*backend_, writeMPTHolders).Times(0);
|
||||
|
||||
std::vector<MPTokenIssuanceTransactionsData> issuanceTxs;
|
||||
std::vector<MPTokenIssuanceTransactionsData> accountIssuanceTxs;
|
||||
EXPECT_CALL(*backend_, writeMPTokenIssuanceTransactions).WillOnce(SaveArg<0>(&issuanceTxs));
|
||||
EXPECT_CALL(*backend_, writeAccountMPTokenIssuanceTransactions)
|
||||
.WillOnce(SaveArg<0>(&accountIssuanceTxs));
|
||||
|
||||
ext_.onLedgerData(data);
|
||||
|
||||
ASSERT_EQ(issuanceTxs.size(), 1);
|
||||
EXPECT_EQ(issuanceTxs[0].mptIssuanceID, xrpl::uint192{kMptIssuanceID});
|
||||
EXPECT_GT(issuanceTxs[0].accounts.size(), kHighFanoutAccountCount);
|
||||
EXPECT_EQ(issuanceTxs[0].ledgerSequence, kSeq);
|
||||
EXPECT_EQ(issuanceTxs, accountIssuanceTxs);
|
||||
}
|
||||
|
||||
TEST_F(MPTExtTests, OnInitialDataDoesNotWriteFailedMPTokenCreations)
|
||||
|
||||
Reference in New Issue
Block a user