feat: Ledger index (#1503)

Fixed #1052
This commit is contained in:
cyan317
2024-07-02 13:58:21 +01:00
committed by GitHub
parent d26c93a711
commit 66849432be
11 changed files with 502 additions and 0 deletions

View File

@@ -31,6 +31,7 @@ target_sources(
handlers/Ledger.cpp
handlers/LedgerData.cpp
handlers/LedgerEntry.cpp
handlers/LedgerIndex.cpp
handlers/LedgerRange.cpp
handlers/NFTsByIssuer.cpp
handlers/NFTBuyOffers.cpp

View File

@@ -34,6 +34,9 @@
#include <charconv>
#include <cstdint>
#include <ctime>
#include <iomanip>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
@@ -51,6 +54,26 @@ Required::verify(boost::json::value const& value, std::string_view key)
return {};
}
[[nodiscard]] MaybeError
TimeFormatValidator::verify(boost::json::value const& value, std::string_view key) const
{
using boost::json::value_to;
if (not value.is_object() or not value.as_object().contains(key))
return {}; // ignore. field does not exist, let 'required' fail instead
if (not value.as_object().at(key).is_string())
return Error{Status{RippledError::rpcINVALID_PARAMS}};
std::tm time = {};
std::stringstream stream(value_to<std::string>(value.as_object().at(key)));
stream >> std::get_time(&time, format_.c_str());
if (stream.fail())
return Error{Status{RippledError::rpcINVALID_PARAMS}};
return {};
}
[[nodiscard]] MaybeError
CustomValidator::verify(boost::json::value const& value, std::string_view key) const
{

View File

@@ -30,8 +30,11 @@
#include <xrpl/protocol/ErrorCodes.h>
#include <cstdint>
#include <ctime>
#include <functional>
#include <initializer_list>
#include <iomanip>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
@@ -288,6 +291,33 @@ public:
}
};
/**
* @brief Validate that value can be converted to time according to the given format.
*/
class TimeFormatValidator final {
std::string format_;
public:
/**
* @brief Construct the validator storing format value.
*
* @param format The format to use for time conversion
*/
explicit TimeFormatValidator(std::string format) : format_{std::move(format)}
{
}
/**
* @brief Verify that the JSON value is valid formatted time.
*
* @param value The JSON value representing the outer object
* @param key The key used to retrieve the tested value from the outer object
* @return `RippledError::rpcINVALID_PARAMS` if validation failed; otherwise no error is returned
*/
[[nodiscard]] MaybeError
verify(boost::json::value const& value, std::string_view key) const;
};
/**
* @brief Validates that the value is equal to the one passed in.
*/

View File

@@ -43,6 +43,7 @@
#include "rpc/handlers/Ledger.hpp"
#include "rpc/handlers/LedgerData.hpp"
#include "rpc/handlers/LedgerEntry.hpp"
#include "rpc/handlers/LedgerIndex.hpp"
#include "rpc/handlers/LedgerRange.hpp"
#include "rpc/handlers/NFTBuyOffers.hpp"
#include "rpc/handlers/NFTHistory.hpp"
@@ -94,6 +95,7 @@ ProductionHandlerProvider::ProductionHandlerProvider(
{"ledger", {LedgerHandler{backend}}},
{"ledger_data", {LedgerDataHandler{backend}}},
{"ledger_entry", {LedgerEntryHandler{backend}}},
{"ledger_index", {LedgerIndexHandler{backend}, true}}, // clio only
{"ledger_range", {LedgerRangeHandler{backend}}},
{"nfts_by_issuer", {NFTsByIssuerHandler{backend}, true}}, // clio only
{"nft_history", {NFTHistoryHandler{backend}, true}}, // clio only

View File

@@ -0,0 +1,119 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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 "rpc/handlers/LedgerIndex.hpp"
#include "rpc/JS.hpp"
#include "rpc/common/Types.hpp"
#include "util/Assert.hpp"
#include <boost/json/conversion.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <fmt/core.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <ctime>
#include <numeric>
#include <ranges>
#include <sstream>
#include <string>
#include <utility>
namespace rpc {
LedgerIndexHandler::Result
LedgerIndexHandler::process(LedgerIndexHandler::Input input, Context const& ctx) const
{
auto const range = sharedPtrBackend_->fetchLedgerRange();
auto const [minIndex, maxIndex] = *range;
auto const fillOutputByIndex = [&](std::uint32_t index) {
auto const ledger = sharedPtrBackend_->fetchLedgerBySequence(index, ctx.yield);
return Output{
.ledgerIndex = index,
.ledgerHash = ripple::strHex(ledger->hash),
.closeTimeIso = ripple::to_string_iso(ledger->closeTime)
};
};
// if no date is provided, return the latest ledger
if (!input.date)
return fillOutputByIndex(maxIndex);
auto const convertISOTimeStrToTicks = [](std::string const& isoTimeStr) {
std::tm time = {};
std::stringstream ss(isoTimeStr);
ss >> std::get_time(&time, DATE_FORMAT);
return std::chrono::system_clock::from_time_t(std::mktime(&time)).time_since_epoch().count();
};
auto const ticks = convertISOTimeStrToTicks(*input.date);
auto const earlierThan = [&](std::uint32_t ledgerIndex) {
auto const header = sharedPtrBackend_->fetchLedgerBySequence(ledgerIndex, ctx.yield);
auto const ledgerTime =
std::chrono::system_clock::time_point{header->closeTime.time_since_epoch() + ripple::epoch_offset};
return ticks < ledgerTime.time_since_epoch().count();
};
// If the given date is earlier than the first valid ledger, return lgrNotFound
if (earlierThan(minIndex))
return Error{Status{RippledError::rpcLGR_NOT_FOUND, "ledgerNotInRange"}};
auto const view = std::ranges::iota_view{minIndex, maxIndex + 1};
auto const greaterEqLedgerIter = std::ranges::lower_bound(
view, ticks, [&](std::uint32_t ledgerIndex, std::int64_t) { return not earlierThan(ledgerIndex); }
);
if (greaterEqLedgerIter != view.end())
return fillOutputByIndex(std::max(static_cast<std::uint32_t>(*greaterEqLedgerIter) - 1, minIndex));
return fillOutputByIndex(maxIndex);
}
LedgerIndexHandler::Input
tag_invoke(boost::json::value_to_tag<LedgerIndexHandler::Input>, boost::json::value const& jv)
{
auto input = LedgerIndexHandler::Input{};
if (jv.as_object().contains(JS(date)))
input.date = jv.at(JS(date)).as_string();
return input;
}
void
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, LedgerIndexHandler::Output const& output)
{
jv = boost::json::object{
{JS(ledger_index), output.ledgerIndex},
{JS(ledger_hash), output.ledgerHash},
{JS(close_time_iso), output.closeTimeIso},
{JS(validated), true},
};
}
} // namespace rpc

View File

@@ -0,0 +1,119 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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 "rpc/JS.hpp"
#include "rpc/common/Specs.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/common/Validators.hpp"
#include <boost/json/conversion.hpp>
#include <boost/json/value.hpp>
#include <xrpl/protocol/jss.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
namespace rpc {
/**
* @brief The ledger_index method fetches the lastest closed ledger before the given date.
*
*/
class LedgerIndexHandler {
std::shared_ptr<BackendInterface> sharedPtrBackend_;
static constexpr auto DATE_FORMAT = "%Y-%m-%dT%TZ";
public:
/**
* @brief A struct to hold the output data of the command
*/
struct Output {
uint32_t ledgerIndex{};
std::string ledgerHash;
std::string closeTimeIso;
};
/**
* @brief A struct to hold the input data for the command
*/
struct Input {
std::optional<std::string> date;
};
using Result = HandlerReturnType<Output>;
/**
* @brief Construct a new LedgerIndexHandler object
*
* @param sharedPtrBackend The backend to use
*/
LedgerIndexHandler(std::shared_ptr<BackendInterface> const& sharedPtrBackend) : sharedPtrBackend_(sharedPtrBackend)
{
}
/**
* @brief Returns the API specification for the command
*
* @param apiVersion The api version to return the spec for
* @return The spec for the given apiVersion
*/
static RpcSpecConstRef
spec([[maybe_unused]] uint32_t apiVersion)
{
static auto const rpcSpec = RpcSpec{
{JS(date), validation::Type<std::string>{}, validation::TimeFormatValidator{DATE_FORMAT}},
};
return rpcSpec;
}
/**
* @brief Process the LedgerIndex command
*
* @param input The input data for the command
* @param ctx The context of the request
* @return The result of the operation
*/
Result
process(Input input, Context const& ctx) const;
private:
/**
* @brief Convert the Output to a JSON object
*
* @param [out] jv The JSON object to convert to
* @param output The output to convert
*/
friend void
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output);
/**
* @brief Convert a JSON object to Input type
*
* @param jv The JSON object to convert
* @return Input parsed from the JSON object
*/
friend Input
tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv);
};
} // namespace rpc

View File

@@ -93,6 +93,21 @@ CreateLedgerHeader(std::string_view ledgerHash, ripple::LedgerIndex seq, std::op
return ledgerHeader;
}
ripple::LedgerHeader
CreateLedgerHeaderWithUnixTime(std::string_view ledgerHash, ripple::LedgerIndex seq, uint64_t closeTimeUnixStamp)
{
using namespace std::chrono;
auto ledgerHeader = ripple::LedgerHeader();
ledgerHeader.hash = ripple::uint256{ledgerHash};
ledgerHeader.seq = seq;
auto const closeTime = closeTimeUnixStamp - seconds{rippleEpochStart}.count();
ledgerHeader.closeTime = ripple::NetClock::time_point{seconds{closeTime}};
return ledgerHeader;
}
ripple::STObject
CreateLegacyFeeSettingLedgerObject(
uint64_t base,

View File

@@ -65,6 +65,12 @@ GetAccountKey(ripple::AccountID const& acc);
[[nodiscard]] ripple::LedgerHeader
CreateLedgerHeader(std::string_view ledgerHash, ripple::LedgerIndex seq, std::optional<uint32_t> age = std::nullopt);
/*
* Create a simple ledgerHeader object with hash, seq and unix timestamp
*/
[[nodiscard]] ripple::LedgerHeader
CreateLedgerHeaderWithUnixTime(std::string_view ledgerHash, ripple::LedgerIndex seq, uint64_t closeTimeUnixStamp);
/*
* Create a Legacy (pre XRPFees amendment) FeeSetting ledger object
*/

View File

@@ -72,6 +72,7 @@ target_sources(
rpc/handlers/GetAggregatePriceTests.cpp
rpc/handlers/LedgerDataTests.cpp
rpc/handlers/LedgerEntryTests.cpp
rpc/handlers/LedgerIndexTests.cpp
rpc/handlers/LedgerRangeTests.cpp
rpc/handlers/LedgerTests.cpp
rpc/handlers/NFTBuyOffersTests.cpp

View File

@@ -355,6 +355,42 @@ TEST_F(RPCBaseTest, WithCustomError)
ASSERT_EQ(err.error(), ripple::rpcALREADY_MULTISIG);
}
TEST_F(RPCBaseTest, TimeFormatValidator)
{
auto const spec = RpcSpec{
{"date", TimeFormatValidator{"%Y-%m-%dT%H:%M:%SZ"}},
};
auto passingInput = json::parse(R"({ "date": "2023-01-01T00:00:00Z" })");
EXPECT_TRUE(spec.process(passingInput));
passingInput = json::parse("123");
EXPECT_TRUE(spec.process(passingInput));
// key not exists
passingInput = json::parse(R"({ "date1": "2023-01-01T00:00:00Z" })");
EXPECT_TRUE(spec.process(passingInput));
auto failingInput = json::parse(R"({ "date": "2023-01-01-00:00:00" })");
auto err = spec.process(failingInput);
EXPECT_FALSE(err);
EXPECT_EQ(err.error(), ripple::rpcINVALID_PARAMS);
failingInput = json::parse(R"({ "date": "01-01-2024T00:00:00" })");
EXPECT_FALSE(spec.process(failingInput));
failingInput = json::parse(R"({ "date": "2024-01-01T29:00:00" })");
EXPECT_FALSE(spec.process(failingInput));
failingInput = json::parse(R"({ "date": "" })");
EXPECT_FALSE(spec.process(failingInput));
failingInput = json::parse(R"({ "date": 1 })");
err = spec.process(failingInput);
EXPECT_FALSE(err);
EXPECT_EQ(err.error(), ripple::rpcINVALID_PARAMS);
}
TEST_F(RPCBaseTest, CustomValidator)
{
auto customFormatCheck = CustomValidator{[](json::value const& value, std::string_view /* key */) -> MaybeError {

View File

@@ -0,0 +1,150 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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 "rpc/Errors.hpp"
#include "rpc/common/AnyHandler.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/handlers/LedgerIndex.hpp"
#include "util/HandlerBaseTestFixture.hpp"
#include "util/NameGenerator.hpp"
#include "util/TestObject.hpp"
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
#include <fmt/core.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
constexpr static auto RANGEMIN = 10;
constexpr static auto RANGEMAX = 30;
constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
using namespace rpc;
namespace json = boost::json;
using namespace testing;
class RPCLedgerIndexTest : public HandlerBaseTestStrict {};
TEST_F(RPCLedgerIndexTest, DateStrNotValid)
{
auto const handler = AnyHandler{LedgerIndexHandler{backend}};
auto const req = json::parse(R"({"date": "not_a_number"})");
runSpawn([&](auto yield) {
auto const output = handler.process(req, Context{yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "Invalid parameters.");
});
}
TEST_F(RPCLedgerIndexTest, NoDateGiven)
{
backend->setRange(RANGEMIN, RANGEMAX);
auto const ledgerHeader = CreateLedgerHeader(LEDGERHASH, RANGEMAX, 5);
EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillOnce(Return(ledgerHeader));
auto const handler = AnyHandler{LedgerIndexHandler{backend}};
auto const req = json::parse(R"({})");
runSpawn([&](auto yield) {
auto const output = handler.process(req, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(output.result->at("ledger_index").as_uint64(), RANGEMAX);
EXPECT_EQ(output.result->at("ledger_hash").as_string(), LEDGERHASH);
EXPECT_TRUE(output.result->as_object().contains("close_time_iso"));
});
}
TEST_F(RPCLedgerIndexTest, EarlierThanMinLedger)
{
backend->setRange(RANGEMIN, RANGEMAX);
auto const handler = AnyHandler{LedgerIndexHandler{backend}};
auto const req = json::parse(R"({"date": "2024-06-25T12:23:05Z"})");
auto const ledgerHeader =
CreateLedgerHeaderWithUnixTime(LEDGERHASH, RANGEMIN, 1719318190); //"2024-06-25T12:23:10Z"
EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMIN, _)).WillOnce(Return(ledgerHeader));
runSpawn([&](auto yield) {
auto const output = handler.process(req, Context{yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
});
}
struct LedgerIndexTestsCaseBundle {
std::string testName;
std::string json;
std::uint32_t expectedLedgerIndex;
std::string closeTimeIso;
};
class LedgerIndexTests : public RPCLedgerIndexTest, public WithParamInterface<LedgerIndexTestsCaseBundle> {
public:
static auto
generateTestValuesForParametersTest()
{
// start from 2024-06-25T12:23:10Z to 2024-06-25T12:23:50Z with step 2
return std::vector<LedgerIndexTestsCaseBundle>{
{"LaterThanMaxLedger", R"({"date": "2024-06-25T12:23:55Z"})", RANGEMAX, "2024-06-25T12:23:50Z"},
{"GreaterThanMinLedger", R"({"date": "2024-06-25T12:23:11Z"})", RANGEMIN, "2024-06-25T12:23:10Z"},
{"IsMinLedger", R"({"date": "2024-06-25T12:23:10Z"})", RANGEMIN, "2024-06-25T12:23:10Z"},
{"IsMaxLedger", R"({"date": "2024-06-25T12:23:50Z"})", RANGEMAX, "2024-06-25T12:23:50Z"},
{"IsMidLedger", R"({"date": "2024-06-25T12:23:30Z"})", 20, "2024-06-25T12:23:30Z"},
{"BetweenLedgers", R"({"date": "2024-06-25T12:23:29Z"})", 19, "2024-06-25T12:23:28Z"}
};
}
};
INSTANTIATE_TEST_CASE_P(
RPCLedgerIndexTestsGroup,
LedgerIndexTests,
ValuesIn(LedgerIndexTests::generateTestValuesForParametersTest()),
tests::util::NameGenerator
);
TEST_P(LedgerIndexTests, SearchFromLedgerRange)
{
auto const testBundle = GetParam();
auto const jv = json::parse(testBundle.json).as_object();
backend->setRange(RANGEMIN, RANGEMAX);
// start from 1719318190 , which is the unix time for 2024-06-25T12:23:10Z to 2024-06-25T12:23:50Z with
// step 2
for (uint32_t i = RANGEMIN; i <= RANGEMAX; i++) {
auto const ledgerHeader = CreateLedgerHeaderWithUnixTime(LEDGERHASH, i, 1719318190 + 2 * (i - RANGEMIN));
ON_CALL(*backend, fetchLedgerBySequence(i, _)).WillByDefault(Return(ledgerHeader));
EXPECT_CALL(*backend, fetchLedgerBySequence(i, _))
.Times(i == testBundle.expectedLedgerIndex ? (i == RANGEMIN ? Exactly(3) : Exactly(2)) : AtMost(1));
}
auto const handler = AnyHandler{LedgerIndexHandler{backend}};
auto const req = json::parse(testBundle.json);
runSpawn([&](auto yield) {
auto const output = handler.process(req, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(output.result->at("ledger_index").as_uint64(), testBundle.expectedLedgerIndex);
EXPECT_EQ(output.result->at("ledger_hash").as_string(), LEDGERHASH);
EXPECT_EQ(output.result->at("close_time_iso").as_string(), testBundle.closeTimeIso);
});
}