feat: ETLng MPT support (#2154)

This commit is contained in:
Alex Kremer
2025-05-27 13:05:03 +01:00
committed by GitHub
parent 3e83b54332
commit 28062496eb
15 changed files with 380 additions and 36 deletions

View File

@@ -39,6 +39,11 @@
#include "etlng/LoadBalancerInterface.hpp" #include "etlng/LoadBalancerInterface.hpp"
#include "etlng/impl/LedgerPublisher.hpp" #include "etlng/impl/LedgerPublisher.hpp"
#include "etlng/impl/TaskManagerProvider.hpp" #include "etlng/impl/TaskManagerProvider.hpp"
#include "etlng/impl/ext/Cache.hpp"
#include "etlng/impl/ext/Core.hpp"
#include "etlng/impl/ext/MPT.hpp"
#include "etlng/impl/ext/NFT.hpp"
#include "etlng/impl/ext/Successor.hpp"
#include "feed/SubscriptionManagerInterface.hpp" #include "feed/SubscriptionManagerInterface.hpp"
#include "util/Assert.hpp" #include "util/Assert.hpp"
#include "util/Constants.hpp" #include "util/Constants.hpp"
@@ -96,7 +101,8 @@ ETLService::makeETLService(
etlng::impl::CacheExt{cacheUpdater}, etlng::impl::CacheExt{cacheUpdater},
etlng::impl::CoreExt{backend}, etlng::impl::CoreExt{backend},
etlng::impl::SuccessorExt{backend, backend->cache()}, etlng::impl::SuccessorExt{backend, backend->cache()},
etlng::impl::NFTExt{backend} etlng::impl::NFTExt{backend},
etlng::impl::MPTExt{backend}
), ),
amendmentBlockHandler amendmentBlockHandler
); );

View File

@@ -15,6 +15,7 @@ target_sources(
impl/TaskManager.cpp impl/TaskManager.cpp
impl/ext/Cache.cpp impl/ext/Cache.cpp
impl/ext/Core.cpp impl/ext/Core.cpp
impl/ext/MPT.cpp
impl/ext/NFT.cpp impl/ext/NFT.cpp
impl/ext/Successor.cpp impl/ext/Successor.cpp
) )

View File

@@ -36,14 +36,14 @@ CacheExt::CacheExt(std::shared_ptr<CacheUpdaterInterface> cacheUpdater) : cacheU
} }
void void
CacheExt::onLedgerData(model::LedgerData const& data) const CacheExt::onLedgerData(model::LedgerData const& data)
{ {
cacheUpdater_->update(data);
LOG(log_.trace()) << "got data. objects cnt = " << data.objects.size(); LOG(log_.trace()) << "got data. objects cnt = " << data.objects.size();
cacheUpdater_->update(data);
} }
void void
CacheExt::onInitialData(model::LedgerData const& data) const CacheExt::onInitialData(model::LedgerData const& data)
{ {
LOG(log_.trace()) << "got initial data. objects cnt = " << data.objects.size(); LOG(log_.trace()) << "got initial data. objects cnt = " << data.objects.size();
cacheUpdater_->update(data); cacheUpdater_->update(data);
@@ -52,7 +52,6 @@ CacheExt::onInitialData(model::LedgerData const& data) const
void void
CacheExt::onInitialObjects(uint32_t seq, std::vector<model::Object> const& objs, [[maybe_unused]] std::string lastKey) CacheExt::onInitialObjects(uint32_t seq, std::vector<model::Object> const& objs, [[maybe_unused]] std::string lastKey)
const
{ {
LOG(log_.trace()) << "got initial objects cnt = " << objs.size(); LOG(log_.trace()) << "got initial objects cnt = " << objs.size();
cacheUpdater_->update(seq, objs); cacheUpdater_->update(seq, objs);

View File

@@ -40,13 +40,13 @@ public:
CacheExt(std::shared_ptr<CacheUpdaterInterface> cacheUpdater); CacheExt(std::shared_ptr<CacheUpdaterInterface> cacheUpdater);
void void
onLedgerData(model::LedgerData const& data) const; onLedgerData(model::LedgerData const& data);
void void
onInitialData(model::LedgerData const& data) const; onInitialData(model::LedgerData const& data);
void void
onInitialObjects(uint32_t seq, std::vector<model::Object> const& objs, [[maybe_unused]] std::string lastKey) const; onInitialObjects(uint32_t seq, std::vector<model::Object> const& objs, [[maybe_unused]] std::string lastKey);
// We want cache updates through ETL if we are a potential writer but currently are not writing to DB // We want cache updates through ETL if we are a potential writer but currently are not writing to DB
static bool static bool

View File

@@ -34,7 +34,7 @@ CoreExt::CoreExt(std::shared_ptr<BackendInterface> backend) : backend_(std::move
} }
void void
CoreExt::onLedgerData(model::LedgerData const& data) const CoreExt::onLedgerData(model::LedgerData const& data)
{ {
LOG(log_.debug()) << "Loading ledger data for " << data.seq; LOG(log_.debug()) << "Loading ledger data for " << data.seq;
backend_->writeLedger(data.header, auto{data.rawHeader}); backend_->writeLedger(data.header, auto{data.rawHeader});
@@ -42,7 +42,7 @@ CoreExt::onLedgerData(model::LedgerData const& data) const
} }
void void
CoreExt::onInitialData(model::LedgerData const& data) const CoreExt::onInitialData(model::LedgerData const& data)
{ {
LOG(log_.info()) << "Loading initial ledger data for " << data.seq; LOG(log_.info()) << "Loading initial ledger data for " << data.seq;
backend_->writeLedger(data.header, auto{data.rawHeader}); backend_->writeLedger(data.header, auto{data.rawHeader});
@@ -50,21 +50,21 @@ CoreExt::onInitialData(model::LedgerData const& data) const
} }
void void
CoreExt::onInitialObject(uint32_t seq, model::Object const& obj) const CoreExt::onInitialObject(uint32_t seq, model::Object const& obj)
{ {
LOG(log_.trace()) << "got initial OBJ = " << obj.key << " for seq " << seq; LOG(log_.trace()) << "got initial OBJ = " << obj.key << " for seq " << seq;
backend_->writeLedgerObject(auto{obj.keyRaw}, seq, auto{obj.dataRaw}); backend_->writeLedgerObject(auto{obj.keyRaw}, seq, auto{obj.dataRaw});
} }
void void
CoreExt::onObject(uint32_t seq, model::Object const& obj) const CoreExt::onObject(uint32_t seq, model::Object const& obj)
{ {
LOG(log_.trace()) << "got OBJ = " << obj.key << " for seq " << seq; LOG(log_.trace()) << "got OBJ = " << obj.key << " for seq " << seq;
backend_->writeLedgerObject(auto{obj.keyRaw}, seq, auto{obj.dataRaw}); backend_->writeLedgerObject(auto{obj.keyRaw}, seq, auto{obj.dataRaw});
} }
void void
CoreExt::insertTransactions(model::LedgerData const& data) const CoreExt::insertTransactions(model::LedgerData const& data)
{ {
for (auto const& txn : data.transactions) { for (auto const& txn : data.transactions) {
LOG(log_.trace()) << "Inserting transaction = " << txn.sttx.getTransactionID(); LOG(log_.trace()) << "Inserting transaction = " << txn.sttx.getTransactionID();

View File

@@ -39,20 +39,20 @@ public:
CoreExt(std::shared_ptr<BackendInterface> backend); CoreExt(std::shared_ptr<BackendInterface> backend);
void void
onLedgerData(model::LedgerData const& data) const; onLedgerData(model::LedgerData const& data);
void void
onInitialData(model::LedgerData const& data) const; onInitialData(model::LedgerData const& data);
void void
onInitialObject(uint32_t seq, model::Object const& obj) const; onInitialObject(uint32_t seq, model::Object const& obj);
void void
onObject(uint32_t seq, model::Object const& obj) const; onObject(uint32_t seq, model::Object const& obj);
private: private:
void void
insertTransactions(model::LedgerData const& data) const; insertTransactions(model::LedgerData const& data);
}; };
} // namespace etlng::impl } // namespace etlng::impl

View File

@@ -0,0 +1,81 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "etlng/impl/ext/MPT.hpp"
#include "data/BackendInterface.hpp"
#include "data/DBHelpers.hpp"
#include "etl/MPTHelpers.hpp"
#include "etlng/Models.hpp"
#include "util/log/Logger.hpp"
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TxMeta.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
namespace etlng::impl {
MPTExt::MPTExt(std::shared_ptr<BackendInterface> backend) : backend_(std::move(backend))
{
}
void
MPTExt::onLedgerData(model::LedgerData const& data)
{
LOG(log_.trace()) << "got TXS cnt = " << data.transactions.size() << "; OBJS size = " << data.objects.size();
writeMPTHoldersFromTransactions(data);
}
void
MPTExt::onInitialObject(uint32_t, model::Object const& obj)
{
LOG(log_.trace()) << "got initial object with key: " << ripple::strHex(obj.key);
if (auto const mptHolder = etl::getMPTHolderFromObj(obj.keyRaw, obj.dataRaw); mptHolder.has_value())
backend_->writeMPTHolders({*mptHolder});
}
void
MPTExt::onInitialData(model::LedgerData const& data)
{
LOG(log_.trace()) << "got initial TXS cnt = " << data.transactions.size();
writeMPTHoldersFromTransactions(data);
}
void
MPTExt::writeMPTHoldersFromTransactions(model::LedgerData const& data)
{
std::vector<MPTHolderData> holders;
for (auto const& tx : data.transactions) {
if (auto const mptHolder = etl::getMPTHolderFromTx(tx.meta, tx.sttx); mptHolder.has_value())
holders.push_back(*mptHolder);
}
if (not holders.empty())
backend_->writeMPTHolders(holders);
}
} // namespace etlng::impl

View File

@@ -0,0 +1,57 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "data/BackendInterface.hpp"
#include "etlng/Models.hpp"
#include "util/log/Logger.hpp"
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TxMeta.h>
#include <cstdint>
#include <memory>
namespace etlng::impl {
class MPTExt {
std::shared_ptr<BackendInterface> backend_;
util::Logger log_{"ETL"};
public:
explicit MPTExt(std::shared_ptr<BackendInterface> backend);
void
onLedgerData(model::LedgerData const& data);
void
onInitialObject(uint32_t seq, model::Object const& obj);
void
onInitialData(model::LedgerData const& data);
private:
void
writeMPTHoldersFromTransactions(model::LedgerData const& data);
};
} // namespace etlng::impl

View File

@@ -37,27 +37,28 @@ NFTExt::NFTExt(std::shared_ptr<BackendInterface> backend) : backend_(std::move(b
} }
void void
NFTExt::onLedgerData(model::LedgerData const& data) const NFTExt::onLedgerData(model::LedgerData const& data)
{ {
LOG(log_.trace()) << "got TXS cnt = " << data.transactions.size() << "; OBJS size = " << data.objects.size();
writeNFTs(data); writeNFTs(data);
} }
void void
NFTExt::onInitialObject(uint32_t seq, model::Object const& obj) const NFTExt::onInitialObject(uint32_t seq, model::Object const& obj)
{ {
LOG(log_.trace()) << "got initial object with key = " << obj.key; LOG(log_.trace()) << "got initial object with key = " << obj.key;
backend_->writeNFTs(etl::getNFTDataFromObj(seq, obj.keyRaw, obj.dataRaw)); backend_->writeNFTs(etl::getNFTDataFromObj(seq, obj.keyRaw, obj.dataRaw));
} }
void void
NFTExt::onInitialData(model::LedgerData const& data) const NFTExt::onInitialData(model::LedgerData const& data)
{ {
LOG(log_.trace()) << "got initial TXS cnt = " << data.transactions.size(); LOG(log_.trace()) << "got initial TXS cnt = " << data.transactions.size();
writeNFTs(data); writeNFTs(data);
} }
void void
NFTExt::writeNFTs(model::LedgerData const& data) const NFTExt::writeNFTs(model::LedgerData const& data)
{ {
std::vector<NFTsData> nfts; std::vector<NFTsData> nfts;
std::vector<NFTTransactionsData> nftTxs; std::vector<NFTTransactionsData> nftTxs;

View File

@@ -36,17 +36,17 @@ public:
NFTExt(std::shared_ptr<BackendInterface> backend); NFTExt(std::shared_ptr<BackendInterface> backend);
void void
onLedgerData(model::LedgerData const& data) const; onLedgerData(model::LedgerData const& data);
void void
onInitialObject(uint32_t seq, model::Object const& obj) const; onInitialObject(uint32_t seq, model::Object const& obj);
void void
onInitialData(model::LedgerData const& data) const; onInitialData(model::LedgerData const& data);
private: private:
void void
writeNFTs(model::LedgerData const& data) const; writeNFTs(model::LedgerData const& data);
}; };
} // namespace etlng::impl } // namespace etlng::impl

View File

@@ -56,18 +56,18 @@ constinit auto const kRAW_HEADER =
namespace util { namespace util {
std::pair<std::string, std::string> std::pair<std::string, std::string>
createNftTxAndMetaBlobs(std::string metaStr, std::string txnStr) createTxAndMetaBlobs(std::string metaStr, std::string txnStr)
{ {
return {hexStringToBinaryString(metaStr), hexStringToBinaryString(txnStr)}; return {hexStringToBinaryString(metaStr), hexStringToBinaryString(txnStr)};
} }
std::pair<ripple::STTx, ripple::TxMeta> std::pair<ripple::STTx, ripple::TxMeta>
createNftTxAndMeta(std::string hashStr, std::string metaStr, std::string txnStr) createTxAndMeta(std::string hashStr, std::string metaStr, std::string txnStr)
{ {
ripple::uint256 hash; ripple::uint256 hash;
EXPECT_TRUE(hash.parseHex(hashStr)); EXPECT_TRUE(hash.parseHex(hashStr));
auto const [metaBlob, txnBlob] = createNftTxAndMetaBlobs(metaStr, txnStr); auto const [metaBlob, txnBlob] = createTxAndMetaBlobs(metaStr, txnStr);
ripple::SerialIter it{txnBlob.data(), txnBlob.size()}; ripple::SerialIter it{txnBlob.data(), txnBlob.size()};
return {ripple::STTx{it}, ripple::TxMeta{hash, kSEQ, metaBlob}}; return {ripple::STTx{it}, ripple::TxMeta{hash, kSEQ, metaBlob}};
@@ -76,7 +76,7 @@ createNftTxAndMeta(std::string hashStr, std::string metaStr, std::string txnStr)
etlng::model::Transaction etlng::model::Transaction
createTransaction(ripple::TxType type, std::string hashStr, std::string metaStr, std::string txnStr) createTransaction(ripple::TxType type, std::string hashStr, std::string metaStr, std::string txnStr)
{ {
auto const [sttx, meta] = createNftTxAndMeta(hashStr, metaStr, txnStr); auto const [sttx, meta] = createTxAndMeta(hashStr, metaStr, txnStr);
return { return {
.raw = "", .raw = "",
.metaRaw = "", .metaRaw = "",
@@ -161,6 +161,28 @@ createObjectWithTwoNFTs()
}; };
} }
etlng::model::Object
createObjectWithMPT()
{
constexpr auto kACCOUNT = "rM2AGCCCRb373FRuD8wHyUwUsh2dV4BW5Q";
auto const account = getAccountIdWithString(kACCOUNT);
auto const mptokenObject = createMpTokenObject(kACCOUNT, ripple::makeMptID(2, getAccountIdWithString(kACCOUNT)));
return {
.key = {},
.keyRaw = std::string(reinterpret_cast<char const*>(account.data()), ripple::AccountID::size()),
.data = {},
.dataRaw = std::string(
static_cast<char const*>(mptokenObject.getSerializer().getDataPtr()),
mptokenObject.getSerializer().getDataLength()
),
.successor = "",
.predecessor = "",
.type = etlng::model::Object::ModType::Created,
};
}
etlng::model::BookSuccessor etlng::model::BookSuccessor
createSuccessor() createSuccessor()
{ {
@@ -184,7 +206,7 @@ createDataAndDiff()
{ {
auto original = org::xrpl::rpc::v1::TransactionAndMetadata(); auto original = org::xrpl::rpc::v1::TransactionAndMetadata();
auto const [metaRaw, txRaw] = createNftTxAndMetaBlobs(); auto const [metaRaw, txRaw] = createTxAndMetaBlobs();
original.set_transaction_blob(txRaw); original.set_transaction_blob(txRaw);
original.set_metadata_blob(metaRaw); original.set_metadata_blob(metaRaw);
for (int i = 0; i < 10; ++i) { for (int i = 0; i < 10; ++i) {
@@ -230,7 +252,7 @@ createData()
{ {
auto original = org::xrpl::rpc::v1::TransactionAndMetadata(); auto original = org::xrpl::rpc::v1::TransactionAndMetadata();
auto const [metaRaw, txRaw] = createNftTxAndMetaBlobs(); auto const [metaRaw, txRaw] = createTxAndMetaBlobs();
original.set_transaction_blob(txRaw); original.set_transaction_blob(txRaw);
original.set_metadata_blob(metaRaw); original.set_metadata_blob(metaRaw);
for (int i = 0; i < 10; ++i) { for (int i = 0; i < 10; ++i) {

View File

@@ -144,10 +144,10 @@ static constexpr auto kDEFAULT_HASH = "6C7F69A6D25A13AC4A2E9145999F45D4674F93990
static constexpr auto kDEFAULT_OBJ_KEY = "B00AA769C00726371689ED66A7CF57C2502F1BF4BDFF2ACADF67A2A7B5E8960D"; static constexpr auto kDEFAULT_OBJ_KEY = "B00AA769C00726371689ED66A7CF57C2502F1BF4BDFF2ACADF67A2A7B5E8960D";
[[maybe_unused, nodiscard]] std::pair<std::string, std::string> [[maybe_unused, nodiscard]] std::pair<std::string, std::string>
createNftTxAndMetaBlobs(std::string metaStr = kDEFAULT_TXN_META, std::string txnStr = kDEFAULT_TXN_HEX); createTxAndMetaBlobs(std::string metaStr = kDEFAULT_TXN_META, std::string txnStr = kDEFAULT_TXN_HEX);
[[maybe_unused, nodiscard]] std::pair<ripple::STTx, ripple::TxMeta> [[maybe_unused, nodiscard]] std::pair<ripple::STTx, ripple::TxMeta>
createNftTxAndMeta( createTxAndMeta(
std::string hashStr = kDEFAULT_HASH, std::string hashStr = kDEFAULT_HASH,
std::string metaStr = kDEFAULT_TXN_META, std::string metaStr = kDEFAULT_TXN_META,
std::string txnStr = kDEFAULT_TXN_HEX std::string txnStr = kDEFAULT_TXN_HEX
@@ -176,6 +176,9 @@ createObjectWithBookBase(
[[maybe_unused, nodiscard]] etlng::model::Object [[maybe_unused, nodiscard]] etlng::model::Object
createObjectWithTwoNFTs(); createObjectWithTwoNFTs();
[[maybe_unused, nodiscard]] etlng::model::Object
createObjectWithMPT();
[[maybe_unused, nodiscard]] etlng::model::BookSuccessor [[maybe_unused, nodiscard]] etlng::model::BookSuccessor
createSuccessor(); createSuccessor();

View File

@@ -55,6 +55,7 @@ target_sources(
etlng/SourceImplTests.cpp etlng/SourceImplTests.cpp
etlng/ext/CoreTests.cpp etlng/ext/CoreTests.cpp
etlng/ext/CacheTests.cpp etlng/ext/CacheTests.cpp
etlng/ext/MPTTests.cpp
etlng/ext/NFTTests.cpp etlng/ext/NFTTests.cpp
etlng/ext/SuccessorTests.cpp etlng/ext/SuccessorTests.cpp
# Feed # Feed

View File

@@ -198,7 +198,7 @@ TEST_F(ExtractionNgTests, OneTransaction)
auto expected = util::createTransaction(ripple::TxType::ttNFTOKEN_CREATE_OFFER); auto expected = util::createTransaction(ripple::TxType::ttNFTOKEN_CREATE_OFFER);
auto original = org::xrpl::rpc::v1::TransactionAndMetadata(); auto original = org::xrpl::rpc::v1::TransactionAndMetadata();
auto [metaRaw, txRaw] = util::createNftTxAndMetaBlobs(); auto [metaRaw, txRaw] = util::createTxAndMetaBlobs();
original.set_transaction_blob(txRaw); original.set_transaction_blob(txRaw);
original.set_metadata_blob(metaRaw); original.set_metadata_blob(metaRaw);
@@ -216,7 +216,7 @@ TEST_F(ExtractionNgTests, MultipleTransactions)
auto expected = util::createTransaction(ripple::TxType::ttNFTOKEN_CREATE_OFFER); auto expected = util::createTransaction(ripple::TxType::ttNFTOKEN_CREATE_OFFER);
auto original = org::xrpl::rpc::v1::TransactionAndMetadata(); auto original = org::xrpl::rpc::v1::TransactionAndMetadata();
auto [metaRaw, txRaw] = util::createNftTxAndMetaBlobs(); auto [metaRaw, txRaw] = util::createTxAndMetaBlobs();
original.set_transaction_blob(txRaw); original.set_transaction_blob(txRaw);
original.set_metadata_blob(metaRaw); original.set_metadata_blob(metaRaw);

View File

@@ -0,0 +1,173 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "etlng/Models.hpp"
#include "etlng/impl/ext/MPT.hpp"
#include "rpc/RPCHelpers.hpp"
#include "util/BinaryTestObject.hpp"
#include "util/MockBackendTestFixture.hpp"
#include "util/MockPrometheus.hpp"
#include "util/TestObject.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/TxMeta.h>
#include <xrpl/protocol/serialize.h>
#include <algorithm>
#include <utility>
#include <vector>
using namespace etlng;
using namespace etlng::impl;
using namespace data;
using namespace testing;
namespace {
constinit auto const kSEQ = 123u;
constinit auto const kLEDGER_HASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constinit auto const kHOLDER_ACCOUNT = "rK1EX542EgA9m948JrJRaEzwLVEhqWvnr9";
constinit auto const kTXN_HEX =
"120039220000000024002DBD1A201B002DBDA36840000000000000017321EDECF25C029811CAD07AFD616EB75E3803E44D0D59A6826AC25FE3"
"4A43626D2D157440244262E760314164843026CE2F100D0BFEB0DD6F75026FEB3F75FCAA943F5C874FF0411BC82A85DE504B434B5EC3C6A692"
"3CC37A1C2ABD3E98EFFC8240B9D0018114CEF330DB51154D8DEE249CC3D6DFD04B91F648EE0115002DBD1817E0AF9FDE4F9978B8FCD8A50636"
"30B5737DA605";
constinit auto const kTXN_META =
"201C00000002F8E311007F562668E165750018E0AE5808C131BAF4C26441D2BCF76F8628774DFDF098B7250BE88114CEF330DB51154D8DEE24"
"9CC3D6DFD04B91F648EE0115002DBD1817E0AF9FDE4F9978B8FCD8A5063630B5737DA605E1E1E511006425002DBD2F55E85C182A243C7CBF0E"
"F7B8B3E0C8AE68E3DE6616DE1EFE168CD913CA6520444D568F18252475DFAC9D5DE5423DFA08842F398F346DEB2BD546C526D26BF81E345CE7"
"2200000000588F18252475DFAC9D5DE5423DFA08842F398F346DEB2BD546C526D26BF81E345C8214CEF330DB51154D8DEE249CC3D6DFD04B91"
"F648EEE1E1E511006125002DBD2F55E85C182A243C7CBF0EF7B8B3E0C8AE68E3DE6616DE1EFE168CD913CA6520444D56F7D3073515F1C71F2A"
"D00941BA714A3FBE3D91AEAFCD6345B5389004AD707E95E624002DBD1A2D00000001624000000005F5E0FFE1E7220000000024002DBD1B2D00"
"000002624000000005F5E0FE8114CEF330DB51154D8DEE249CC3D6DFD04B91F648EEE1E1F1031000";
constinit auto const kHASH = "6005B465CBBF7FA8E41AC0C0CD38491026D9411FCB7BA46E2AEBB3AF7654261B";
constinit auto const kHASH2 = "6005B465CBBF7FA8E41AC0C0CD38491026D9411FCB7BA46E2AEBB3AF7654261C";
constinit auto const kHASH3 = "6005B465CBBF7FA8E41AC0C0CD38491026D9411FCB7BA46E2AEBB3AF7654261D";
auto
createTestData()
{
auto transactions = std::vector{
util::createTransaction(ripple::TxType::ttMPTOKEN_ISSUANCE_CREATE), // not AUTHORIZE so will not be written
util::createTransaction(ripple::TxType::ttMPTOKEN_AUTHORIZE, kHASH, kTXN_META, kTXN_HEX),
util::createTransaction(ripple::TxType::ttAMM_CREATE), // not MPT - will be filtered
util::createTransaction(ripple::TxType::ttMPTOKEN_ISSUANCE_CREATE), // not unique - will be filtered
};
auto const header = createLedgerHeader(kLEDGER_HASH, kSEQ);
return etlng::model::LedgerData{
.transactions = std::move(transactions),
.objects = {},
.successors = {},
.edgeKeys = {},
.header = header,
.rawHeader = {},
.seq = kSEQ
};
}
auto
createMultipleHoldersTestData()
{
auto transactions = std::vector{
util::createTransaction(ripple::TxType::ttMPTOKEN_AUTHORIZE, kHASH, kTXN_META, kTXN_HEX),
util::createTransaction(ripple::TxType::ttMPTOKEN_AUTHORIZE, kHASH2, kTXN_META, kTXN_HEX),
util::createTransaction(ripple::TxType::ttMPTOKEN_AUTHORIZE, kHASH3, kTXN_META, kTXN_HEX)
};
auto const header = createLedgerHeader(kLEDGER_HASH, kSEQ);
return etlng::model::LedgerData{
.transactions = std::move(transactions),
.objects = {},
.successors = {},
.edgeKeys = {},
.header = header,
.rawHeader = {},
.seq = kSEQ
};
}
} // namespace
struct MPTExtTests : util::prometheus::WithPrometheus, MockBackendTest {
protected:
MPTExt ext_{backend_};
};
TEST_F(MPTExtTests, OnLedgerDataFiltersAndWritesMPTs)
{
auto const data = createTestData();
EXPECT_CALL(*backend_, writeMPTHolders).WillOnce([](auto const& holders) {
EXPECT_EQ(holders.size(), 1); // Only AUTHORIZE is written in the end
});
ext_.onLedgerData(data);
}
TEST_F(MPTExtTests, OnInitialDataFiltersAndWritesMPTs)
{
auto const data = createTestData();
EXPECT_CALL(*backend_, writeMPTHolders).WillOnce([](auto const& holders) {
EXPECT_EQ(holders.size(), 1); // Only AUTHORIZE is written in the end
});
ext_.onInitialData(data);
}
TEST_F(MPTExtTests, OnInitialObjectWritesMPT)
{
auto const data = util::createObjectWithMPT();
EXPECT_CALL(*backend_, writeMPTHolders).WillOnce([](auto const& holders) { EXPECT_EQ(holders.size(), 1); });
ext_.onInitialObject(kSEQ, data);
}
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
auto const expectedAccount = rpc::accountFromStringStrict(kHOLDER_ACCOUNT); // Expect all three to be the same
EXPECT_TRUE(std::ranges::all_of(holders, [&expectedAccount](auto const& data) {
return data.holder == expectedAccount;
}));
});
ext_.onInitialData(data);
}