mirror of
https://github.com/XRPLF/clio.git
synced 2026-07-25 08:00:18 +00:00
## 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>
593 lines
21 KiB
C++
593 lines
21 KiB
C++
#include "data/DBHelpers.hpp"
|
|
#include "etl/Models.hpp"
|
|
#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>
|
|
#include <xrpl/protocol/Serializer.h>
|
|
#include <xrpl/protocol/TER.h>
|
|
#include <xrpl/protocol/TxFormats.h>
|
|
#include <xrpl/protocol/TxMeta.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <iterator>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
using namespace etl;
|
|
using namespace etl::impl;
|
|
using namespace data;
|
|
using namespace testing;
|
|
|
|
namespace {
|
|
|
|
constinit auto const kSeq = 123u;
|
|
constinit auto const kLedgerHash =
|
|
"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
|
|
constinit auto const kHolderAccount = "rK1EX542EgA9m948JrJRaEzwLVEhqWvnr9";
|
|
constinit auto const kHolderAccount2 = "rnd1nHuzceyQDqnLH8urWNr4QBKt4v7WVk";
|
|
constinit auto const kMptIssuanceID = "000004C463C52827307480341125DA0577DEFC38405B0E3E";
|
|
|
|
constinit auto const kTxnHex =
|
|
"120039220000000024002DBD1A201B002DBDA36840000000000000017321EDECF25C029811CAD07AFD616EB75E3803"
|
|
"E44D0D59A6826AC25FE3"
|
|
"4A43626D2D157440244262E760314164843026CE2F100D0BFEB0DD6F75026FEB3F75FCAA943F5C874FF0411BC82A85"
|
|
"DE504B434B5EC3C6A692"
|
|
"3CC37A1C2ABD3E98EFFC8240B9D0018114CEF330DB51154D8DEE249CC3D6DFD04B91F648EE0115002DBD1817E0AF9F"
|
|
"DE4F9978B8FCD8A50636"
|
|
"30B5737DA605";
|
|
|
|
constinit auto const kTxnMeta =
|
|
"201C00000002F8E311007F562668E165750018E0AE5808C131BAF4C26441D2BCF76F8628774DFDF098B7250BE88114"
|
|
"CEF330DB51154D8DEE24"
|
|
"9CC3D6DFD04B91F648EE0115002DBD1817E0AF9FDE4F9978B8FCD8A5063630B5737DA605E1E1E511006425002DBD2F"
|
|
"55E85C182A243C7CBF0E"
|
|
"F7B8B3E0C8AE68E3DE6616DE1EFE168CD913CA6520444D568F18252475DFAC9D5DE5423DFA08842F398F346DEB2BD5"
|
|
"46C526D26BF81E345CE7"
|
|
"2200000000588F18252475DFAC9D5DE5423DFA08842F398F346DEB2BD546C526D26BF81E345C8214CEF330DB51154D"
|
|
"8DEE249CC3D6DFD04B91"
|
|
"F648EEE1E1E511006125002DBD2F55E85C182A243C7CBF0EF7B8B3E0C8AE68E3DE6616DE1EFE168CD913CA6520444D"
|
|
"56F7D3073515F1C71F2A"
|
|
"D00941BA714A3FBE3D91AEAFCD6345B5389004AD707E95E624002DBD1A2D00000001624000000005F5E0FFE1E72200"
|
|
"00000024002DBD1B2D00"
|
|
"000002624000000005F5E0FE8114CEF330DB51154D8DEE249CC3D6DFD04B91F648EEE1E1F1031000";
|
|
|
|
constinit auto const kHash = "6005B465CBBF7FA8E41AC0C0CD38491026D9411FCB7BA46E2AEBB3AF7654261B";
|
|
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,
|
|
xrpl::STObject const& metaObj,
|
|
xrpl::TxType type
|
|
)
|
|
{
|
|
auto const txBlob = txObj.getSerializer().peekData();
|
|
auto const metaBlob = metaObj.getSerializer().peekData();
|
|
|
|
xrpl::SerialIter txIter{txBlob.data(), txBlob.size()};
|
|
xrpl::uint256 const id{kHash};
|
|
|
|
return etl::model::Transaction{
|
|
.raw = "",
|
|
.metaRaw = "",
|
|
.sttx = xrpl::STTx{txIter},
|
|
.meta = xrpl::TxMeta{id, kSeq, metaBlob},
|
|
.id = id,
|
|
.key = std::string{kHash},
|
|
.type = type
|
|
};
|
|
}
|
|
|
|
xrpl::STObject
|
|
createPaymentMetaWithNewMPTokens(xrpl::TER result = xrpl::tesSUCCESS)
|
|
{
|
|
xrpl::STObject metaObj(xrpl::sfTransactionMetaData);
|
|
metaObj.setFieldU8(xrpl::sfTransactionResult, TERtoInt(result));
|
|
metaObj.setFieldU32(xrpl::sfTransactionIndex, 0);
|
|
|
|
xrpl::STArray affectedNodes(xrpl::sfAffectedNodes);
|
|
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;
|
|
}
|
|
|
|
auto
|
|
createPaymentWithMultipleHoldersTestData(xrpl::TER result = xrpl::tesSUCCESS)
|
|
{
|
|
auto transactions = std::vector{createTransactionFromObjects(
|
|
createPaymentTransactionObject(kHolderAccount, kHolderAccount2, 1, 1, 1),
|
|
createPaymentMetaWithNewMPTokens(result),
|
|
xrpl::TxType::ttPAYMENT
|
|
)};
|
|
|
|
auto const header = createLedgerHeader(kLedgerHash, kSeq);
|
|
return etl::model::LedgerData{
|
|
.transactions = std::move(transactions),
|
|
.objects = {},
|
|
.successors = {},
|
|
.edgeKeys = {},
|
|
.header = header,
|
|
.rawHeader = {},
|
|
.seq = kSeq
|
|
};
|
|
}
|
|
|
|
auto
|
|
createTestDataWithoutMPToken()
|
|
{
|
|
auto transactions = std::vector{
|
|
util::createTransaction(
|
|
xrpl::TxType::ttMPTOKEN_ISSUANCE_CREATE
|
|
), // metadata does not create an MPT holder
|
|
util::createTransaction(xrpl::TxType::ttAMM_CREATE), // metadata is not MPT
|
|
};
|
|
|
|
auto const header = createLedgerHeader(kLedgerHash, kSeq);
|
|
return etl::model::LedgerData{
|
|
.transactions = std::move(transactions),
|
|
.objects = {},
|
|
.successors = {},
|
|
.edgeKeys = {},
|
|
.header = header,
|
|
.rawHeader = {},
|
|
.seq = kSeq
|
|
};
|
|
}
|
|
|
|
auto
|
|
createTestData()
|
|
{
|
|
// Only the AUTHORIZE transaction carries metadata with MPT affected nodes.
|
|
auto transactions = std::vector{
|
|
util::createTransaction(
|
|
xrpl::TxType::ttMPTOKEN_ISSUANCE_CREATE
|
|
), // metadata does not create an MPT holder
|
|
util::createTransaction(xrpl::TxType::ttMPTOKEN_AUTHORIZE, kHash, kTxnMeta, kTxnHex),
|
|
util::createTransaction(xrpl::TxType::ttAMM_CREATE), // metadata is not MPT
|
|
util::createTransaction(
|
|
xrpl::TxType::ttMPTOKEN_ISSUANCE_CREATE
|
|
), // metadata does not create an MPT holder
|
|
};
|
|
|
|
auto const header = createLedgerHeader(kLedgerHash, kSeq);
|
|
return etl::model::LedgerData{
|
|
.transactions = std::move(transactions),
|
|
.objects = {},
|
|
.successors = {},
|
|
.edgeKeys = {},
|
|
.header = header,
|
|
.rawHeader = {},
|
|
.seq = kSeq
|
|
};
|
|
}
|
|
|
|
// 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{
|
|
createAuthorizeTransactionWithIndex(kHash, 0),
|
|
createAuthorizeTransactionWithIndex(kHash2, 1),
|
|
createAuthorizeTransactionWithIndex(kHash3, 2)
|
|
};
|
|
|
|
auto const header = createLedgerHeader(kLedgerHash, kSeq);
|
|
return etl::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 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)
|
|
{
|
|
auto const data = createTestData();
|
|
|
|
EXPECT_CALL(*backend_, writeMPTHolders).WillOnce([](auto const& holders) {
|
|
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)
|
|
{
|
|
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); // All three AUTHORIZE transactions
|
|
|
|
auto const expectedAccount =
|
|
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)
|
|
{
|
|
auto const data = createPaymentWithMultipleHoldersTestData(xrpl::tecINCOMPLETE);
|
|
|
|
EXPECT_CALL(*backend_, writeMPTHolders).Times(0);
|
|
|
|
ext_.onInitialData(data);
|
|
}
|
|
|
|
TEST_F(MPTExtTests, OnInitialDataDoesNotWriteWithoutCreatedMPToken)
|
|
{
|
|
auto const data = createTestDataWithoutMPToken();
|
|
|
|
EXPECT_CALL(*backend_, writeMPTHolders).Times(0);
|
|
|
|
ext_.onInitialData(data);
|
|
}
|
|
|
|
TEST_F(MPTExtTests, OnInitialDataWritesAllMPTsCreatedByPayment)
|
|
{
|
|
auto const data = createPaymentWithMultipleHoldersTestData();
|
|
auto const expectedMptID = xrpl::uint192{kMptIssuanceID};
|
|
auto const expectedAccount = getAccountIdWithString(kHolderAccount);
|
|
auto const expectedAccount2 = getAccountIdWithString(kHolderAccount2);
|
|
|
|
EXPECT_CALL(*backend_, writeMPTHolders).WillOnce([&](auto const& holders) {
|
|
EXPECT_THAT(
|
|
holders,
|
|
UnorderedElementsAre(
|
|
AllOf(
|
|
Field(&MPTHolderData::mptID, expectedMptID),
|
|
Field(&MPTHolderData::holder, expectedAccount)
|
|
),
|
|
AllOf(
|
|
Field(&MPTHolderData::mptID, expectedMptID),
|
|
Field(&MPTHolderData::holder, expectedAccount2)
|
|
)
|
|
)
|
|
);
|
|
});
|
|
|
|
ext_.onInitialData(data);
|
|
}
|