mirror of
https://github.com/XRPLF/clio.git
synced 2026-04-29 15:37:53 +00:00
274 lines
9.6 KiB
C++
274 lines
9.6 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of clio: https://github.com/XRPLF/clio
|
|
Copyright (c) 2023, 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.
|
|
*/
|
|
//==============================================================================
|
|
|
|
/** @file */
|
|
#pragma once
|
|
|
|
#include "data/Types.hpp"
|
|
#include "rpc/JS.hpp"
|
|
#include "rpc/RPCHelpers.hpp"
|
|
|
|
#include <boost/json/conversion.hpp>
|
|
#include <boost/json/object.hpp>
|
|
#include <boost/json/value.hpp>
|
|
#include <xrpl/beast/utility/Zero.h>
|
|
#include <xrpl/protocol/IOUAmount.h>
|
|
#include <xrpl/protocol/Issue.h>
|
|
#include <xrpl/protocol/LedgerFormats.h>
|
|
#include <xrpl/protocol/LedgerHeader.h>
|
|
#include <xrpl/protocol/SField.h>
|
|
#include <xrpl/protocol/STAmount.h>
|
|
#include <xrpl/protocol/STObject.h>
|
|
#include <xrpl/protocol/STTx.h>
|
|
#include <xrpl/protocol/TxFormats.h>
|
|
#include <xrpl/protocol/XRPAmount.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <iterator>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace rpc {
|
|
|
|
/**
|
|
* @brief Represents an entry in the book_changes' changes array.
|
|
*/
|
|
struct BookChange {
|
|
ripple::STAmount sideAVolume;
|
|
ripple::STAmount sideBVolume;
|
|
ripple::STAmount highRate;
|
|
ripple::STAmount lowRate;
|
|
ripple::STAmount openRate;
|
|
ripple::STAmount closeRate;
|
|
};
|
|
|
|
/**
|
|
* @brief Encapsulates the book_changes computations and transformations.
|
|
*/
|
|
class BookChanges final {
|
|
public:
|
|
BookChanges() = delete; // only accessed via static handle function
|
|
|
|
/**
|
|
* @brief Computes all book_changes for the given transactions.
|
|
*
|
|
* @param transactions The transactions to compute book changes for
|
|
* @return Book changes
|
|
*/
|
|
[[nodiscard]] static std::vector<BookChange>
|
|
compute(std::vector<data::TransactionAndMetadata> const& transactions)
|
|
{
|
|
return HandlerImpl{}(transactions);
|
|
}
|
|
|
|
private:
|
|
class HandlerImpl final {
|
|
std::map<std::string, BookChange> tally_;
|
|
std::optional<uint32_t> offerCancel_;
|
|
|
|
public:
|
|
[[nodiscard]] std::vector<BookChange>
|
|
operator()(std::vector<data::TransactionAndMetadata> const& transactions)
|
|
{
|
|
for (auto const& tx : transactions)
|
|
handleBookChange(tx);
|
|
|
|
// TODO: rewrite this with std::ranges when compilers catch up
|
|
std::vector<BookChange> changes;
|
|
std::transform(
|
|
std::make_move_iterator(std::begin(tally_)),
|
|
std::make_move_iterator(std::end(tally_)),
|
|
std::back_inserter(changes),
|
|
[](auto obj) { return obj.second; }
|
|
);
|
|
return changes;
|
|
}
|
|
|
|
private:
|
|
void
|
|
handleAffectedNode(ripple::STObject const& node)
|
|
{
|
|
auto const& metaType = node.getFName();
|
|
auto const nodeType = node.getFieldU16(ripple::sfLedgerEntryType);
|
|
|
|
// we only care about ripple::ltOFFER objects being modified or
|
|
// deleted
|
|
if (nodeType != ripple::ltOFFER || metaType == ripple::sfCreatedNode)
|
|
return;
|
|
|
|
// if either FF or PF are missing we can't compute
|
|
// but generally these are cancelled rather than crossed
|
|
// so skipping them is consistent
|
|
if (!node.isFieldPresent(ripple::sfFinalFields) || !node.isFieldPresent(ripple::sfPreviousFields))
|
|
return;
|
|
|
|
auto const& finalFields = node.peekAtField(ripple::sfFinalFields).downcast<ripple::STObject>();
|
|
auto const& previousFields = node.peekAtField(ripple::sfPreviousFields).downcast<ripple::STObject>();
|
|
|
|
// defensive case that should never be hit
|
|
if (!finalFields.isFieldPresent(ripple::sfTakerGets) || !finalFields.isFieldPresent(ripple::sfTakerPays) ||
|
|
!previousFields.isFieldPresent(ripple::sfTakerGets) ||
|
|
!previousFields.isFieldPresent(ripple::sfTakerPays))
|
|
return;
|
|
|
|
// filter out any offers deleted by explicit offer cancels
|
|
if (metaType == ripple::sfDeletedNode && offerCancel_ &&
|
|
finalFields.getFieldU32(ripple::sfSequence) == *offerCancel_)
|
|
return;
|
|
|
|
// compute the difference in gets and pays actually
|
|
// affected onto the offer
|
|
auto const deltaGets =
|
|
finalFields.getFieldAmount(ripple::sfTakerGets) - previousFields.getFieldAmount(ripple::sfTakerGets);
|
|
auto const deltaPays =
|
|
finalFields.getFieldAmount(ripple::sfTakerPays) - previousFields.getFieldAmount(ripple::sfTakerPays);
|
|
|
|
transformAndStore(deltaGets, deltaPays);
|
|
}
|
|
|
|
void
|
|
transformAndStore(ripple::STAmount const& deltaGets, ripple::STAmount const& deltaPays)
|
|
{
|
|
auto const g = to_string(deltaGets.issue());
|
|
auto const p = to_string(deltaPays.issue());
|
|
|
|
auto const noswap = [&]() {
|
|
if (isXRP(deltaGets))
|
|
return true;
|
|
return isXRP(deltaPays) ? false : (g < p);
|
|
}();
|
|
|
|
auto first = noswap ? deltaGets : deltaPays;
|
|
auto second = noswap ? deltaPays : deltaGets;
|
|
|
|
// defensively programmed, should (probably) never happen
|
|
if (second == beast::zero)
|
|
return;
|
|
|
|
auto const rate = divide(first, second, ripple::noIssue());
|
|
|
|
if (first < beast::zero)
|
|
first = -first;
|
|
|
|
if (second < beast::zero)
|
|
second = -second;
|
|
|
|
auto const key = noswap ? (g + '|' + p) : (p + '|' + g);
|
|
if (tally_.contains(key)) {
|
|
auto& entry = tally_.at(key);
|
|
|
|
entry.sideAVolume += first;
|
|
entry.sideBVolume += second;
|
|
|
|
if (entry.highRate < rate)
|
|
entry.highRate = rate;
|
|
|
|
if (entry.lowRate > rate)
|
|
entry.lowRate = rate;
|
|
|
|
entry.closeRate = rate;
|
|
} else {
|
|
tally_[key] = {
|
|
.sideAVolume = first,
|
|
.sideBVolume = second,
|
|
.highRate = rate,
|
|
.lowRate = rate,
|
|
.openRate = rate,
|
|
.closeRate = rate,
|
|
};
|
|
}
|
|
}
|
|
|
|
void
|
|
handleBookChange(data::TransactionAndMetadata const& blob)
|
|
{
|
|
auto const [tx, meta] = rpc::deserializeTxPlusMeta(blob);
|
|
if (!tx || !meta || !tx->isFieldPresent(ripple::sfTransactionType))
|
|
return;
|
|
|
|
offerCancel_ = shouldCancelOffer(tx);
|
|
for (auto const& node : meta->getFieldArray(ripple::sfAffectedNodes))
|
|
handleAffectedNode(node);
|
|
}
|
|
|
|
static std::optional<uint32_t>
|
|
shouldCancelOffer(std::shared_ptr<ripple::STTx const> const& tx)
|
|
{
|
|
switch (tx->getFieldU16(ripple::sfTransactionType)) {
|
|
// in future if any other ways emerge to cancel an offer
|
|
// this switch makes them easy to add
|
|
case ripple::ttOFFER_CANCEL:
|
|
case ripple::ttOFFER_CREATE:
|
|
if (tx->isFieldPresent(ripple::sfOfferSequence))
|
|
return tx->getFieldU32(ripple::sfOfferSequence);
|
|
[[fallthrough]];
|
|
default:
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @brief Implementation of value_from for BookChange type.
|
|
*
|
|
* @param [out] jv The JSON value to populate
|
|
* @param change The BookChange to serialize
|
|
*/
|
|
inline void
|
|
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, BookChange const& change)
|
|
{
|
|
auto amountStr = [](ripple::STAmount const& amount) -> std::string {
|
|
return isXRP(amount) ? to_string(amount.xrp()) : to_string(amount.iou());
|
|
};
|
|
|
|
auto currencyStr = [](ripple::STAmount const& amount) -> std::string {
|
|
return isXRP(amount) ? "XRP_drops" : to_string(amount.issue());
|
|
};
|
|
|
|
jv = {
|
|
{JS(currency_a), currencyStr(change.sideAVolume)},
|
|
{JS(currency_b), currencyStr(change.sideBVolume)},
|
|
{JS(volume_a), amountStr(change.sideAVolume)},
|
|
{JS(volume_b), amountStr(change.sideBVolume)},
|
|
{JS(high), to_string(change.highRate.iou())},
|
|
{JS(low), to_string(change.lowRate.iou())},
|
|
{JS(open), to_string(change.openRate.iou())},
|
|
{JS(close), to_string(change.closeRate.iou())},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @brief Computes all book changes for the given ledger header and transactions.
|
|
*
|
|
* @param lgrInfo The ledger header
|
|
* @param transactions The vector of transactions with heir metadata
|
|
* @return The book changes
|
|
*/
|
|
[[nodiscard]] boost::json::object
|
|
computeBookChanges(ripple::LedgerHeader const& lgrInfo, std::vector<data::TransactionAndMetadata> const& transactions);
|
|
|
|
} // namespace rpc
|