mirror of
https://github.com/XRPLF/clio.git
synced 2025-12-06 17:27:58 +00:00
@@ -26,6 +26,7 @@ target_sources(
|
||||
handlers/BookOffers.cpp
|
||||
handlers/DepositAuthorized.cpp
|
||||
handlers/GatewayBalances.cpp
|
||||
handlers/GetAggregatePrice.cpp
|
||||
handlers/Ledger.cpp
|
||||
handlers/LedgerData.cpp
|
||||
handlers/LedgerEntry.cpp
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "rpc/handlers/BookOffers.hpp"
|
||||
#include "rpc/handlers/DepositAuthorized.hpp"
|
||||
#include "rpc/handlers/GatewayBalances.hpp"
|
||||
#include "rpc/handlers/GetAggregatePrice.hpp"
|
||||
#include "rpc/handlers/Ledger.hpp"
|
||||
#include "rpc/handlers/LedgerData.hpp"
|
||||
#include "rpc/handlers/LedgerEntry.hpp"
|
||||
@@ -85,6 +86,7 @@ ProductionHandlerProvider::ProductionHandlerProvider(
|
||||
{"book_offers", {BookOffersHandler{backend}}},
|
||||
{"deposit_authorized", {DepositAuthorizedHandler{backend}}},
|
||||
{"gateway_balances", {GatewayBalancesHandler{backend}}},
|
||||
{"get_aggregate_price", {GetAggregatePriceHandler{backend}}},
|
||||
{"ledger", {LedgerHandler{backend}}},
|
||||
{"ledger_data", {LedgerDataHandler{backend}}},
|
||||
{"ledger_entry", {LedgerEntryHandler{backend}}},
|
||||
|
||||
310
src/rpc/handlers/GetAggregatePrice.cpp
Normal file
310
src/rpc/handlers/GetAggregatePrice.cpp
Normal file
@@ -0,0 +1,310 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/GetAggregatePrice.hpp"
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/bimap.hpp>
|
||||
#include <boost/bimap/bimap.hpp>
|
||||
#include <boost/bimap/multiset_of.hpp>
|
||||
#include <boost/json/conversion.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <ripple/basics/Number.h>
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <ripple/protocol/AccountID.h>
|
||||
#include <ripple/protocol/ErrorCodes.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/Issue.h>
|
||||
#include <ripple/protocol/LedgerFormats.h>
|
||||
#include <ripple/protocol/LedgerHeader.h>
|
||||
#include <ripple/protocol/SField.h>
|
||||
#include <ripple/protocol/STAmount.h>
|
||||
#include <ripple/protocol/STLedgerEntry.h>
|
||||
#include <ripple/protocol/STObject.h>
|
||||
#include <ripple/protocol/Serializer.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <ripple/protocol/tokens.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
namespace rpc {
|
||||
|
||||
GetAggregatePriceHandler::Result
|
||||
GetAggregatePriceHandler::process(GetAggregatePriceHandler::Input input, Context const& ctx) const
|
||||
{
|
||||
auto const range = sharedPtrBackend_->fetchLedgerRange();
|
||||
auto const lgrInfoOrStatus = getLedgerInfoFromHashOrSeq(
|
||||
*sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, range->maxSequence
|
||||
);
|
||||
|
||||
if (auto const status = std::get_if<Status>(&lgrInfoOrStatus))
|
||||
return Error{*status};
|
||||
|
||||
auto const lgrInfo = std::get<ripple::LedgerHeader>(lgrInfoOrStatus);
|
||||
|
||||
// sorted descending by lastUpdateTime, ascending by AssetPrice
|
||||
using TimestampPricesBiMap = boost::bimaps::bimap<
|
||||
boost::bimaps::multiset_of<std::uint32_t, std::greater<std::uint32_t>>,
|
||||
boost::bimaps::multiset_of<ripple::STAmount>>;
|
||||
|
||||
TimestampPricesBiMap timestampPricesBiMap;
|
||||
|
||||
for (auto const& oracle : input.oracles) {
|
||||
auto const oracleIndex = ripple::keylet::oracle(oracle.account, oracle.documentId).key;
|
||||
|
||||
auto const oracleObject = sharedPtrBackend_->fetchLedgerObject(oracleIndex, lgrInfo.seq, ctx.yield);
|
||||
if (not oracleObject)
|
||||
continue;
|
||||
|
||||
ripple::STLedgerEntry const oracleSle{
|
||||
ripple::SerialIter{oracleObject->data(), oracleObject->size()}, oracleIndex
|
||||
};
|
||||
|
||||
tracebackOracleObject(ctx.yield, oracleSle, [&](auto const& node) {
|
||||
auto const& series = node.getFieldArray(ripple::sfPriceDataSeries);
|
||||
// Find the token pair entry with the price
|
||||
if (auto const iter = std::find_if(
|
||||
series.begin(),
|
||||
series.end(),
|
||||
[&](ripple::STObject const& o) -> bool {
|
||||
return o.getFieldCurrency(ripple::sfBaseAsset).getText() == input.baseAsset and
|
||||
o.getFieldCurrency(ripple::sfQuoteAsset).getText() == input.quoteAsset and
|
||||
o.isFieldPresent(ripple::sfAssetPrice);
|
||||
}
|
||||
);
|
||||
iter != series.end()) {
|
||||
auto const price = iter->getFieldU64(ripple::sfAssetPrice);
|
||||
// Asset price is after scale, so we need to get the negative of the scale
|
||||
auto const scale =
|
||||
iter->isFieldPresent(ripple::sfScale) ? -static_cast<int>(iter->getFieldU8(ripple::sfScale)) : 0;
|
||||
|
||||
timestampPricesBiMap.insert(TimestampPricesBiMap::value_type(
|
||||
node.getFieldU32(ripple::sfLastUpdateTime), ripple::STAmount{ripple::noIssue(), price, scale}
|
||||
));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (timestampPricesBiMap.empty())
|
||||
return Error{Status{ripple::rpcOBJECT_NOT_FOUND}};
|
||||
|
||||
auto const latestTime = timestampPricesBiMap.left.begin()->first;
|
||||
|
||||
Output out(latestTime, ripple::to_string(lgrInfo.hash), lgrInfo.seq);
|
||||
|
||||
if (input.timeThreshold) {
|
||||
auto const oldestTime = timestampPricesBiMap.left.rbegin()->first;
|
||||
auto const upperBound = latestTime > *input.timeThreshold ? (latestTime - *input.timeThreshold) : oldestTime;
|
||||
if (upperBound > oldestTime) {
|
||||
// Note : upperBound must not be greater than the latestTime, so timestampPricesBiMap can not be empty
|
||||
timestampPricesBiMap.left.erase(
|
||||
timestampPricesBiMap.left.upper_bound(upperBound), timestampPricesBiMap.left.end()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
auto const getStats = [](TimestampPricesBiMap::right_const_iterator begin,
|
||||
TimestampPricesBiMap::right_const_iterator end) -> Stats {
|
||||
ripple::STAmount avg{ripple::noIssue(), 0, 0};
|
||||
ripple::Number sd{0};
|
||||
std::uint16_t const size = std::distance(begin, end);
|
||||
avg = std::accumulate(begin, end, avg, [&](ripple::STAmount const& acc, auto const& it) {
|
||||
return acc + it.first;
|
||||
});
|
||||
avg = divide(avg, ripple::STAmount{ripple::noIssue(), size, 0}, ripple::noIssue());
|
||||
if (size > 1) {
|
||||
sd = std::accumulate(begin, end, sd, [&](ripple::Number const& acc, auto const& it) {
|
||||
return acc + (it.first - avg) * (it.first - avg);
|
||||
});
|
||||
sd = root2(sd / (size - 1));
|
||||
}
|
||||
return {avg, sd, size};
|
||||
};
|
||||
|
||||
out.extireStats = getStats(timestampPricesBiMap.right.begin(), timestampPricesBiMap.right.end());
|
||||
|
||||
auto const itAdvance = [&](auto it, int distance) {
|
||||
std::advance(it, distance);
|
||||
return it;
|
||||
};
|
||||
|
||||
// trim is [1,25], we can thin out the first and last trim% of the data
|
||||
if (input.trim) {
|
||||
auto const trimCount = timestampPricesBiMap.size() * (*input.trim) / 100;
|
||||
|
||||
out.trimStats = getStats(
|
||||
itAdvance(timestampPricesBiMap.right.begin(), trimCount),
|
||||
itAdvance(timestampPricesBiMap.right.end(), -trimCount)
|
||||
);
|
||||
}
|
||||
|
||||
auto const median = [&, size = out.extireStats.size]() {
|
||||
auto const middle = size / 2;
|
||||
if ((size % 2) == 0) {
|
||||
static ripple::STAmount two{ripple::noIssue(), 2, 0};
|
||||
auto it = itAdvance(timestampPricesBiMap.right.begin(), middle - 1);
|
||||
auto const& a1 = it->first;
|
||||
auto const& a2 = (++it)->first;
|
||||
return divide(a1 + a2, two, ripple::noIssue());
|
||||
}
|
||||
return itAdvance(timestampPricesBiMap.right.begin(), middle)->first;
|
||||
}();
|
||||
|
||||
out.median = median.getText();
|
||||
return out;
|
||||
}
|
||||
|
||||
void
|
||||
GetAggregatePriceHandler::tracebackOracleObject(
|
||||
boost::asio::yield_context yield,
|
||||
ripple::STObject const& oracleObject,
|
||||
std::function<bool(ripple::STObject const&)> const& callback
|
||||
) const
|
||||
{
|
||||
static auto constexpr HISTORY_MAX = 3;
|
||||
|
||||
std::optional<ripple::STObject> optOracleObject = oracleObject;
|
||||
std::optional<ripple::STObject> optCurrentObject = optOracleObject;
|
||||
|
||||
bool isNew = false;
|
||||
bool noOracleFound = false;
|
||||
auto history = 0;
|
||||
|
||||
while (true) {
|
||||
// No oracle found in metadata
|
||||
if (noOracleFound)
|
||||
return;
|
||||
|
||||
// Found the price pair or this is a new object, exit early
|
||||
if (callback(*optOracleObject) or isNew)
|
||||
return;
|
||||
|
||||
if (++history > HISTORY_MAX)
|
||||
return;
|
||||
|
||||
auto const prevTxIndex = optCurrentObject->getFieldH256(ripple::sfPreviousTxnID);
|
||||
|
||||
auto const prevTx = sharedPtrBackend_->fetchTransaction(prevTxIndex, yield);
|
||||
if (not prevTx)
|
||||
return;
|
||||
|
||||
noOracleFound = true;
|
||||
auto const [_, meta] = deserializeTxPlusMeta(*prevTx);
|
||||
|
||||
for (ripple::STObject const& node : meta->getFieldArray(ripple::sfAffectedNodes)) {
|
||||
if (node.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltORACLE) {
|
||||
continue;
|
||||
}
|
||||
noOracleFound = false;
|
||||
optCurrentObject = node;
|
||||
isNew = node.isFieldPresent(ripple::sfNewFields);
|
||||
// if a meta is for the new and this is the first
|
||||
// look-up then it's the meta for the tx that
|
||||
// created the current object; i.e. there is no
|
||||
// historical data
|
||||
if (isNew and history == 1)
|
||||
return;
|
||||
|
||||
optOracleObject = isNew ? dynamic_cast<ripple::STObject const&>(node.peekAtField(ripple::sfNewFields))
|
||||
: dynamic_cast<ripple::STObject const&>(node.peekAtField(ripple::sfFinalFields));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GetAggregatePriceHandler::Input
|
||||
tag_invoke(boost::json::value_to_tag<GetAggregatePriceHandler::Input>, boost::json::value const& jv)
|
||||
{
|
||||
auto input = GetAggregatePriceHandler::Input{};
|
||||
auto const& jsonObject = jv.as_object();
|
||||
|
||||
if (jsonObject.contains(JS(ledger_hash)))
|
||||
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
|
||||
|
||||
if (jsonObject.contains(JS(ledger_index))) {
|
||||
if (!jsonObject.at(JS(ledger_index)).is_string()) {
|
||||
input.ledgerIndex = jv.at(JS(ledger_index)).as_int64();
|
||||
} else if (jsonObject.at(JS(ledger_index)).as_string() != "validated") {
|
||||
input.ledgerIndex = std::stoi(boost::json::value_to<std::string>(jv.at(JS(ledger_index))));
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& oracle : jsonObject.at(JS(oracles)).as_array()) {
|
||||
input.oracles.push_back(GetAggregatePriceHandler::Oracle{
|
||||
.documentId = boost::json::value_to<std::uint64_t>(oracle.as_object().at(JS(oracle_document_id))),
|
||||
.account = *ripple::parseBase58<ripple::AccountID>(
|
||||
boost::json::value_to<std::string>(oracle.as_object().at(JS(account)))
|
||||
)
|
||||
});
|
||||
}
|
||||
input.baseAsset = boost::json::value_to<std::string>(jv.at(JS(base_asset)));
|
||||
input.quoteAsset = boost::json::value_to<std::string>(jv.at(JS(quote_asset)));
|
||||
|
||||
if (jsonObject.contains(JS(trim)))
|
||||
input.trim = jv.at(JS(trim)).as_int64();
|
||||
|
||||
if (jsonObject.contains(JS(time_threshold)))
|
||||
input.timeThreshold = jv.at(JS(time_threshold)).as_int64();
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
void
|
||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, GetAggregatePriceHandler::Output const& output)
|
||||
{
|
||||
jv = boost::json::object{
|
||||
{JS(ledger_hash), output.ledgerHash},
|
||||
{JS(ledger_index), output.ledgerIndex},
|
||||
{JS(validated), output.validated},
|
||||
{JS(time), output.time},
|
||||
{JS(entire_set),
|
||||
boost::json::object{
|
||||
{JS(mean), output.extireStats.avg.getText()},
|
||||
{JS(standard_deviation), ripple::to_string(output.extireStats.sd)},
|
||||
{JS(size), output.extireStats.size}
|
||||
}},
|
||||
{JS(median), output.median}
|
||||
};
|
||||
|
||||
if (output.trimStats) {
|
||||
jv.as_object()[JS(trimmed_set)] = boost::json::object{
|
||||
{JS(mean), output.trimStats->avg.getText()},
|
||||
{JS(standard_deviation), ripple::to_string(output.trimStats->sd)},
|
||||
{JS(size), output.trimStats->size}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rpc
|
||||
221
src/rpc/handlers/GetAggregatePrice.hpp
Normal file
221
src/rpc/handlers/GetAggregatePrice.hpp
Normal file
@@ -0,0 +1,221 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/common/Specs.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/common/Validators.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/conversion.hpp>
|
||||
#include <ripple/basics/Number.h>
|
||||
#include <ripple/protocol/AccountID.h>
|
||||
#include <ripple/protocol/ErrorCodes.h>
|
||||
#include <ripple/protocol/STAmount.h>
|
||||
#include <ripple/protocol/STObject.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
namespace rpc {
|
||||
|
||||
/**
|
||||
*@brief The get_aggregate_price method.
|
||||
*/
|
||||
class GetAggregatePriceHandler {
|
||||
std::shared_ptr<BackendInterface> sharedPtrBackend_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief A struct to hold the statistics
|
||||
*/
|
||||
struct Stats {
|
||||
ripple::STAmount avg;
|
||||
ripple::Number sd; // standard deviation
|
||||
uint32_t size{0};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A struct to hold the output data of the command
|
||||
*/
|
||||
struct Output {
|
||||
uint32_t time;
|
||||
Stats extireStats;
|
||||
std::optional<Stats> trimStats;
|
||||
std::string ledgerHash;
|
||||
uint32_t ledgerIndex;
|
||||
std::string median;
|
||||
bool validated = true;
|
||||
|
||||
/**
|
||||
* @brief Construct a new Output object
|
||||
* @param time The time of the latest oracle data
|
||||
* @param ledgerHash The hash of the ledger
|
||||
* @param ledgerIndex The index of the ledger
|
||||
*/
|
||||
Output(uint32_t time, std::string ledgerHash, uint32_t ledgerIndex)
|
||||
: time(time), ledgerHash(std::move(ledgerHash)), ledgerIndex(ledgerIndex)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A struct to hold the input oracle data
|
||||
*/
|
||||
struct Oracle {
|
||||
std::uint32_t documentId{0};
|
||||
ripple::AccountID account;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A struct to hold the input data for the command
|
||||
*/
|
||||
struct Input {
|
||||
std::optional<std::string> ledgerHash;
|
||||
std::optional<std::uint32_t> ledgerIndex;
|
||||
std::vector<Oracle> oracles; // valid range is 1-200
|
||||
std::string baseAsset;
|
||||
std::string quoteAsset;
|
||||
std::optional<std::uint32_t> timeThreshold;
|
||||
std::optional<std::uint8_t> trim; // valid range is 1-25
|
||||
};
|
||||
|
||||
using Result = HandlerReturnType<Output>;
|
||||
|
||||
/**
|
||||
* @brief Construct a new GetAggregatePrice handler object
|
||||
*
|
||||
* @param sharedPtrBackend The backend to use
|
||||
*/
|
||||
GetAggregatePriceHandler(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 constexpr ORACLES_MAX = 200;
|
||||
|
||||
static auto const oraclesValidator =
|
||||
validation::CustomValidator{[](boost::json::value const& value, std::string_view) -> MaybeError {
|
||||
if (!value.is_array() or value.as_array().empty() or value.as_array().size() > ORACLES_MAX)
|
||||
return Error{Status{RippledError::rpcORACLE_MALFORMED}};
|
||||
|
||||
for (auto oracle : value.as_array()) {
|
||||
if (!oracle.is_object() or !oracle.as_object().contains(JS(oracle_document_id)) or
|
||||
!oracle.as_object().contains(JS(account)))
|
||||
return Error{Status{RippledError::rpcORACLE_MALFORMED}};
|
||||
|
||||
auto maybeError =
|
||||
validation::Type<std::uint32_t>{}.verify(oracle.as_object(), JS(oracle_document_id));
|
||||
|
||||
if (!maybeError)
|
||||
return maybeError;
|
||||
|
||||
maybeError = validation::AccountBase58Validator.verify(oracle.as_object(), JS(account));
|
||||
|
||||
if (!maybeError)
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS}};
|
||||
};
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
static auto const rpcSpec = RpcSpec{
|
||||
{JS(ledger_hash), validation::Uint256HexStringValidator},
|
||||
{JS(ledger_index), validation::LedgerIndexValidator},
|
||||
// note: Rippled's base_asset and quote_asset can be non-string. It will eventually return
|
||||
// "rpcOBJECT_NOT_FOUND". Clio will return "rpcINVALID_PARAMS" if the base_asset or quote_asset is not a
|
||||
// string. User can clearly know there is a mistake in the input.
|
||||
{JS(base_asset), validation::Required{}, validation::Type<std::string>{}},
|
||||
{JS(quote_asset), validation::Required{}, validation::Type<std::string>{}},
|
||||
{JS(oracles), validation::Required{}, oraclesValidator},
|
||||
// note: Unlike `rippled`, Clio only supports UInt as input, no string, no `null`, etc.
|
||||
{JS(time_threshold), validation::Type<std::uint32_t>{}},
|
||||
{
|
||||
JS(trim),
|
||||
validation::Type<std::uint8_t>{},
|
||||
validation::Between<std::uint8_t>{1, 25},
|
||||
}
|
||||
};
|
||||
|
||||
return rpcSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Process the GetAggregatePrice 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 Calls callback on the oracle ledger entry
|
||||
If the oracle entry does not contains the price pair, search up to three previous metadata objects. Stops early if
|
||||
the callback returns true.
|
||||
*/
|
||||
void
|
||||
tracebackOracleObject(
|
||||
boost::asio::yield_context yield,
|
||||
ripple::STObject const& oracleObject,
|
||||
std::function<bool(ripple::STObject const&)> const& callback
|
||||
) const;
|
||||
|
||||
/**
|
||||
* @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
|
||||
@@ -61,12 +61,12 @@ target_sources(
|
||||
rpc/handlers/AccountOffersTests.cpp
|
||||
rpc/handlers/AccountTxTests.cpp
|
||||
rpc/handlers/AMMInfoTests.cpp
|
||||
# Backend
|
||||
rpc/handlers/BookChangesTests.cpp
|
||||
rpc/handlers/BookOffersTests.cpp
|
||||
rpc/handlers/DefaultProcessorTests.cpp
|
||||
rpc/handlers/DepositAuthorizedTests.cpp
|
||||
rpc/handlers/GatewayBalancesTests.cpp
|
||||
rpc/handlers/GetAggregatePriceTests.cpp
|
||||
rpc/handlers/LedgerDataTests.cpp
|
||||
rpc/handlers/LedgerEntryTests.cpp
|
||||
rpc/handlers/LedgerRangeTests.cpp
|
||||
|
||||
1276
unittests/rpc/handlers/GetAggregatePriceTests.cpp
Normal file
1276
unittests/rpc/handlers/GetAggregatePriceTests.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -823,6 +823,59 @@ CreateCreateNFTOfferTxWithMetadata(
|
||||
return ret;
|
||||
}
|
||||
|
||||
data::TransactionAndMetadata
|
||||
CreateOracleSetTxWithMetadata(
|
||||
std::string_view accountId,
|
||||
uint32_t seq,
|
||||
uint32_t fee,
|
||||
uint32_t docId,
|
||||
std::uint32_t lastUpdateTime,
|
||||
ripple::STArray priceDataSeries,
|
||||
std::string_view oracleIndex,
|
||||
bool created,
|
||||
std::string_view previousTxnId
|
||||
)
|
||||
{
|
||||
// tx
|
||||
ripple::STObject tx(ripple::sfTransaction);
|
||||
tx.setFieldU16(ripple::sfTransactionType, ripple::ttORACLE_SET);
|
||||
auto account = ripple::parseBase58<ripple::AccountID>(std::string(accountId));
|
||||
tx.setAccountID(ripple::sfAccount, account.value());
|
||||
auto amount = ripple::STAmount(fee, false);
|
||||
tx.setFieldAmount(ripple::sfFee, amount);
|
||||
tx.setFieldU32(ripple::sfLastUpdateTime, lastUpdateTime);
|
||||
tx.setFieldU32(ripple::sfOracleDocumentID, docId);
|
||||
tx.setFieldU32(ripple::sfSequence, seq);
|
||||
char const* key = "test";
|
||||
ripple::Slice const slice(key, 4);
|
||||
tx.setFieldVL(ripple::sfSigningPubKey, slice);
|
||||
tx.setFieldArray(ripple::sfPriceDataSeries, priceDataSeries);
|
||||
|
||||
// meta
|
||||
ripple::STObject metaObj(ripple::sfTransactionMetaData);
|
||||
ripple::STArray metaArray{1};
|
||||
|
||||
ripple::STObject node(created ? ripple::sfCreatedNode : ripple::sfModifiedNode);
|
||||
node.setFieldU16(ripple::sfLedgerEntryType, ripple::ltORACLE);
|
||||
node.setFieldH256(ripple::sfLedgerIndex, ripple::uint256{oracleIndex});
|
||||
node.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{previousTxnId});
|
||||
ripple::STObject fields(created ? ripple::sfNewFields : ripple::sfFinalFields);
|
||||
fields.setFieldU32(ripple::sfOracleDocumentID, docId);
|
||||
fields.setFieldU32(ripple::sfLastUpdateTime, lastUpdateTime);
|
||||
fields.setFieldArray(ripple::sfPriceDataSeries, priceDataSeries);
|
||||
node.emplace_back(std::move(fields));
|
||||
|
||||
metaArray.push_back(node);
|
||||
metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray);
|
||||
metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS);
|
||||
metaObj.setFieldU32(ripple::sfTransactionIndex, 0);
|
||||
|
||||
data::TransactionAndMetadata ret;
|
||||
ret.transaction = tx.getSerializer().peekData();
|
||||
ret.metadata = metaObj.getSerializer().peekData();
|
||||
return ret;
|
||||
}
|
||||
|
||||
ripple::STObject
|
||||
CreateAmendmentsObject(std::vector<ripple::uint256> const& enabledAmendments)
|
||||
{
|
||||
@@ -1046,14 +1099,7 @@ CreateOraclePriceData(
|
||||
ripple::STArray
|
||||
CreatePriceDataSeries(std::vector<ripple::STObject> const& series)
|
||||
{
|
||||
auto priceDataSeries = ripple::STArray{series.size()};
|
||||
|
||||
for (auto& data : series) {
|
||||
auto serializer = data.getSerializer();
|
||||
priceDataSeries.add(serializer);
|
||||
}
|
||||
|
||||
return priceDataSeries;
|
||||
return ripple::STArray{series.begin(), series.end()};
|
||||
}
|
||||
|
||||
ripple::STObject
|
||||
|
||||
@@ -407,3 +407,16 @@ CreateOracleObject(
|
||||
ripple::uint256 previousTxId,
|
||||
ripple::STArray priceDataSeries
|
||||
);
|
||||
|
||||
[[nodiscard]] data::TransactionAndMetadata
|
||||
CreateOracleSetTxWithMetadata(
|
||||
std::string_view accountId,
|
||||
uint32_t seq,
|
||||
uint32_t fee,
|
||||
uint32_t docId,
|
||||
std::uint32_t lastUpdateTime,
|
||||
ripple::STArray priceDataSeries,
|
||||
std::string_view oracleIndex,
|
||||
bool created,
|
||||
std::string_view previousTxnId
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user