mirror of
				https://github.com/XRPLF/clio.git
				synced 2025-11-04 11:55:51 +00:00 
			
		
		
		
	@@ -1026,13 +1026,13 @@ public:
 | 
			
		||||
    isTooBusy() const override;
 | 
			
		||||
 | 
			
		||||
    inline void
 | 
			
		||||
    incremementOutstandingRequestCount() const
 | 
			
		||||
    incrementOutstandingRequestCount() const
 | 
			
		||||
    {
 | 
			
		||||
        {
 | 
			
		||||
            std::unique_lock<std::mutex> lck(throttleMutex_);
 | 
			
		||||
            if (!canAddRequest())
 | 
			
		||||
            {
 | 
			
		||||
                BOOST_LOG_TRIVIAL(info)
 | 
			
		||||
                BOOST_LOG_TRIVIAL(debug)
 | 
			
		||||
                    << __func__ << " : "
 | 
			
		||||
                    << "Max outstanding requests reached. "
 | 
			
		||||
                    << "Waiting for other requests to finish";
 | 
			
		||||
@@ -1109,7 +1109,7 @@ public:
 | 
			
		||||
        bool isRetry) const
 | 
			
		||||
    {
 | 
			
		||||
        if (!isRetry)
 | 
			
		||||
            incremementOutstandingRequestCount();
 | 
			
		||||
            incrementOutstandingRequestCount();
 | 
			
		||||
        executeAsyncHelper(statement, callback, callbackData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -202,6 +202,9 @@ ReportingETL::publishLedger(ripple::LedgerInfo const& lgrInfo)
 | 
			
		||||
 | 
			
		||||
        for (auto& txAndMeta : transactions)
 | 
			
		||||
            subscriptions_->pubTransaction(txAndMeta, lgrInfo);
 | 
			
		||||
 | 
			
		||||
        subscriptions_->pubBookChanges(lgrInfo, transactions);
 | 
			
		||||
 | 
			
		||||
        BOOST_LOG_TRIVIAL(info) << __func__ << " - Published ledger "
 | 
			
		||||
                                << std::to_string(lgrInfo.seq);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ Result
 | 
			
		||||
doChannelVerify(Context const& context);
 | 
			
		||||
 | 
			
		||||
// book methods
 | 
			
		||||
Result
 | 
			
		||||
[[nodiscard]] Result
 | 
			
		||||
doBookChanges(Context const& context);
 | 
			
		||||
 | 
			
		||||
Result
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@
 | 
			
		||||
#include <rpc/Counters.h>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <variant>
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * This file contains various classes necessary for executing RPC handlers.
 | 
			
		||||
 * Context gives the handlers access to various other parts of the application
 | 
			
		||||
 
 | 
			
		||||
@@ -270,5 +270,10 @@ traverseTransactions(
 | 
			
		||||
        std::optional<Backend::TransactionsCursor> const&,
 | 
			
		||||
        boost::asio::yield_context& yield)> transactionFetcher);
 | 
			
		||||
 | 
			
		||||
[[nodiscard]] boost::json::object const
 | 
			
		||||
computeBookChanges(
 | 
			
		||||
    ripple::LedgerInfo const& lgrInfo,
 | 
			
		||||
    std::vector<Backend::TransactionAndMetadata> const& transactions);
 | 
			
		||||
 | 
			
		||||
}  // namespace RPC
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,9 @@ using namespace ripple;
 | 
			
		||||
 | 
			
		||||
namespace RPC {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Represents an entry in the book_changes' changes array.
 | 
			
		||||
 */
 | 
			
		||||
struct BookChange
 | 
			
		||||
{
 | 
			
		||||
    STAmount sideAVolume;
 | 
			
		||||
@@ -22,183 +25,180 @@ struct BookChange
 | 
			
		||||
    STAmount closeRate;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class BookChangesHandler
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Encapsulates the book_changes computations and transformations.
 | 
			
		||||
 */
 | 
			
		||||
class BookChanges final
 | 
			
		||||
{
 | 
			
		||||
    std::reference_wrapper<Context const> context_;
 | 
			
		||||
    std::map<std::string, BookChange> tally_ = {};
 | 
			
		||||
    std::optional<uint32_t> offerCancel_ = {};
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    ~BookChangesHandler() = default;
 | 
			
		||||
    explicit BookChangesHandler(Context const& context)
 | 
			
		||||
        : context_{std::cref(context)}
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    BookChangesHandler(BookChangesHandler const&) = delete;
 | 
			
		||||
    BookChangesHandler(BookChangesHandler&&) = delete;
 | 
			
		||||
    BookChangesHandler&
 | 
			
		||||
    operator=(BookChangesHandler const&) = delete;
 | 
			
		||||
    BookChangesHandler&
 | 
			
		||||
    operator=(BookChangesHandler&&) = delete;
 | 
			
		||||
    BookChanges() = delete;  // only accessed via static handle function
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Handles the `book_change` request for given transactions
 | 
			
		||||
     * @brief Computes all book_changes for the given transactions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param transactions The transactions to compute changes for
 | 
			
		||||
     * @return std::vector<BookChange> The changes
 | 
			
		||||
     * @param transactions The transactions to compute book changes for
 | 
			
		||||
     * @return std::vector<BookChange> Book changes
 | 
			
		||||
     */
 | 
			
		||||
    std::vector<BookChange>
 | 
			
		||||
    handle(LedgerInfo const& ledger)
 | 
			
		||||
    [[nodiscard]] static std::vector<BookChange>
 | 
			
		||||
    compute(std::vector<Backend::TransactionAndMetadata> const& transactions)
 | 
			
		||||
    {
 | 
			
		||||
        reset();
 | 
			
		||||
 | 
			
		||||
        for (auto const transactions =
 | 
			
		||||
                 context_.get().backend->fetchAllTransactionsInLedger(
 | 
			
		||||
                     ledger.seq, context_.get().yield);
 | 
			
		||||
             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;
 | 
			
		||||
        return HandlerImpl{}(transactions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    inline void
 | 
			
		||||
    reset() noexcept
 | 
			
		||||
    class HandlerImpl final
 | 
			
		||||
    {
 | 
			
		||||
        tally_.clear();
 | 
			
		||||
        offerCancel_ = std::nullopt;
 | 
			
		||||
    }
 | 
			
		||||
        std::map<std::string, BookChange> tally_ = {};
 | 
			
		||||
        std::optional<uint32_t> offerCancel_ = {};
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    handleAffectedNode(STObject const& node)
 | 
			
		||||
    {
 | 
			
		||||
        auto const& metaType = node.getFName();
 | 
			
		||||
        auto const nodeType = node.getFieldU16(sfLedgerEntryType);
 | 
			
		||||
 | 
			
		||||
        // we only care about ltOFFER objects being modified or
 | 
			
		||||
        // deleted
 | 
			
		||||
        if (nodeType != ltOFFER || metaType == 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(sfFinalFields) ||
 | 
			
		||||
            !node.isFieldPresent(sfPreviousFields))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        auto const& finalFields =
 | 
			
		||||
            node.peekAtField(sfFinalFields).downcast<STObject>();
 | 
			
		||||
        auto const& previousFields =
 | 
			
		||||
            node.peekAtField(sfPreviousFields).downcast<STObject>();
 | 
			
		||||
 | 
			
		||||
        // defensive case that should never be hit
 | 
			
		||||
        if (!finalFields.isFieldPresent(sfTakerGets) ||
 | 
			
		||||
            !finalFields.isFieldPresent(sfTakerPays) ||
 | 
			
		||||
            !previousFields.isFieldPresent(sfTakerGets) ||
 | 
			
		||||
            !previousFields.isFieldPresent(sfTakerPays))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        // filter out any offers deleted by explicit offer cancels
 | 
			
		||||
        if (metaType == sfDeletedNode && offerCancel_ &&
 | 
			
		||||
            finalFields.getFieldU32(sfSequence) == *offerCancel_)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        // compute the difference in gets and pays actually
 | 
			
		||||
        // affected onto the offer
 | 
			
		||||
        auto const deltaGets = finalFields.getFieldAmount(sfTakerGets) -
 | 
			
		||||
            previousFields.getFieldAmount(sfTakerGets);
 | 
			
		||||
        auto const deltaPays = finalFields.getFieldAmount(sfTakerPays) -
 | 
			
		||||
            previousFields.getFieldAmount(sfTakerPays);
 | 
			
		||||
 | 
			
		||||
        auto const g = to_string(deltaGets.issue());
 | 
			
		||||
        auto const p = to_string(deltaPays.issue());
 | 
			
		||||
 | 
			
		||||
        auto const noswap =
 | 
			
		||||
            isXRP(deltaGets) ? true : (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, 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))
 | 
			
		||||
    public:
 | 
			
		||||
        [[nodiscard]] std::vector<BookChange>
 | 
			
		||||
        operator()(
 | 
			
		||||
            std::vector<Backend::TransactionAndMetadata> const& transactions)
 | 
			
		||||
        {
 | 
			
		||||
            auto& entry = tally_.at(key);
 | 
			
		||||
            for (auto const& tx : transactions)
 | 
			
		||||
                handleBookChange(tx);
 | 
			
		||||
 | 
			
		||||
            entry.sideAVolume += first;
 | 
			
		||||
            entry.sideBVolume += second;
 | 
			
		||||
 | 
			
		||||
            if (entry.highRate < rate)
 | 
			
		||||
                entry.highRate = rate;
 | 
			
		||||
 | 
			
		||||
            if (entry.lowRate > rate)
 | 
			
		||||
                entry.lowRate = rate;
 | 
			
		||||
 | 
			
		||||
            entry.closeRate = rate;
 | 
			
		||||
            // 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;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
 | 
			
		||||
    private:
 | 
			
		||||
        void
 | 
			
		||||
        handleAffectedNode(STObject const& node)
 | 
			
		||||
        {
 | 
			
		||||
            // TODO: use paranthesized initialization when clang catches up
 | 
			
		||||
            tally_[key] = {
 | 
			
		||||
                first,   // sideAVolume
 | 
			
		||||
                second,  // sideBVolume
 | 
			
		||||
                rate,    // highRate
 | 
			
		||||
                rate,    // lowRate
 | 
			
		||||
                rate,    // openRate
 | 
			
		||||
                rate,    // closeRate
 | 
			
		||||
            };
 | 
			
		||||
            auto const& metaType = node.getFName();
 | 
			
		||||
            auto const nodeType = node.getFieldU16(sfLedgerEntryType);
 | 
			
		||||
 | 
			
		||||
            // we only care about ltOFFER objects being modified or
 | 
			
		||||
            // deleted
 | 
			
		||||
            if (nodeType != ltOFFER || metaType == 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(sfFinalFields) ||
 | 
			
		||||
                !node.isFieldPresent(sfPreviousFields))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            auto const& finalFields =
 | 
			
		||||
                node.peekAtField(sfFinalFields).downcast<STObject>();
 | 
			
		||||
            auto const& previousFields =
 | 
			
		||||
                node.peekAtField(sfPreviousFields).downcast<STObject>();
 | 
			
		||||
 | 
			
		||||
            // defensive case that should never be hit
 | 
			
		||||
            if (!finalFields.isFieldPresent(sfTakerGets) ||
 | 
			
		||||
                !finalFields.isFieldPresent(sfTakerPays) ||
 | 
			
		||||
                !previousFields.isFieldPresent(sfTakerGets) ||
 | 
			
		||||
                !previousFields.isFieldPresent(sfTakerPays))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            // filter out any offers deleted by explicit offer cancels
 | 
			
		||||
            if (metaType == sfDeletedNode && offerCancel_ &&
 | 
			
		||||
                finalFields.getFieldU32(sfSequence) == *offerCancel_)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            // compute the difference in gets and pays actually
 | 
			
		||||
            // affected onto the offer
 | 
			
		||||
            auto const deltaGets = finalFields.getFieldAmount(sfTakerGets) -
 | 
			
		||||
                previousFields.getFieldAmount(sfTakerGets);
 | 
			
		||||
            auto const deltaPays = finalFields.getFieldAmount(sfTakerPays) -
 | 
			
		||||
                previousFields.getFieldAmount(sfTakerPays);
 | 
			
		||||
 | 
			
		||||
            transformAndStore(deltaGets, deltaPays);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    handleBookChange(Backend::TransactionAndMetadata const& blob)
 | 
			
		||||
    {
 | 
			
		||||
        auto const [tx, meta] = deserializeTxPlusMeta(blob);
 | 
			
		||||
        if (!tx || !meta || !tx->isFieldPresent(sfTransactionType))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        offerCancel_ = shouldCancelOffer(tx);
 | 
			
		||||
        for (auto const& node : meta->getFieldArray(sfAffectedNodes))
 | 
			
		||||
            handleAffectedNode(node);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::optional<uint32_t>
 | 
			
		||||
    shouldCancelOffer(std::shared_ptr<ripple::STTx const> const& tx) const
 | 
			
		||||
    {
 | 
			
		||||
        switch (tx->getFieldU16(sfTransactionType))
 | 
			
		||||
        void
 | 
			
		||||
        transformAndStore(
 | 
			
		||||
            ripple::STAmount const& deltaGets,
 | 
			
		||||
            ripple::STAmount const& deltaPays)
 | 
			
		||||
        {
 | 
			
		||||
            // in future if any other ways emerge to cancel an offer
 | 
			
		||||
            // this switch makes them easy to add
 | 
			
		||||
            case ttOFFER_CANCEL:
 | 
			
		||||
            case ttOFFER_CREATE:
 | 
			
		||||
                if (tx->isFieldPresent(sfOfferSequence))
 | 
			
		||||
                    return tx->getFieldU32(sfOfferSequence);
 | 
			
		||||
            default:
 | 
			
		||||
                return std::nullopt;
 | 
			
		||||
            auto const g = to_string(deltaGets.issue());
 | 
			
		||||
            auto const p = to_string(deltaPays.issue());
 | 
			
		||||
 | 
			
		||||
            auto const noswap =
 | 
			
		||||
                isXRP(deltaGets) ? true : (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, 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
 | 
			
		||||
            {
 | 
			
		||||
                // TODO: use paranthesized initialization when clang catches up
 | 
			
		||||
                tally_[key] = {
 | 
			
		||||
                    first,   // sideAVolume
 | 
			
		||||
                    second,  // sideBVolume
 | 
			
		||||
                    rate,    // highRate
 | 
			
		||||
                    rate,    // lowRate
 | 
			
		||||
                    rate,    // openRate
 | 
			
		||||
                    rate,    // closeRate
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        void
 | 
			
		||||
        handleBookChange(Backend::TransactionAndMetadata const& blob)
 | 
			
		||||
        {
 | 
			
		||||
            auto const [tx, meta] = deserializeTxPlusMeta(blob);
 | 
			
		||||
            if (!tx || !meta || !tx->isFieldPresent(sfTransactionType))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            offerCancel_ = shouldCancelOffer(tx);
 | 
			
		||||
            for (auto const& node : meta->getFieldArray(sfAffectedNodes))
 | 
			
		||||
                handleAffectedNode(node);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        std::optional<uint32_t>
 | 
			
		||||
        shouldCancelOffer(std::shared_ptr<ripple::STTx const> const& tx) const
 | 
			
		||||
        {
 | 
			
		||||
            switch (tx->getFieldU16(sfTransactionType))
 | 
			
		||||
            {
 | 
			
		||||
                // in future if any other ways emerge to cancel an offer
 | 
			
		||||
                // this switch makes them easy to add
 | 
			
		||||
                case ttOFFER_CANCEL:
 | 
			
		||||
                case ttOFFER_CREATE:
 | 
			
		||||
                    if (tx->isFieldPresent(sfOfferSequence))
 | 
			
		||||
                        return tx->getFieldU32(sfOfferSequence);
 | 
			
		||||
                default:
 | 
			
		||||
                    return std::nullopt;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
@@ -228,6 +228,20 @@ tag_invoke(
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
json::object const
 | 
			
		||||
computeBookChanges(
 | 
			
		||||
    ripple::LedgerInfo const& lgrInfo,
 | 
			
		||||
    std::vector<Backend::TransactionAndMetadata> const& transactions)
 | 
			
		||||
{
 | 
			
		||||
    return {
 | 
			
		||||
        {JS(type), "bookChanges"},
 | 
			
		||||
        {JS(ledger_index), lgrInfo.seq},
 | 
			
		||||
        {JS(ledger_hash), to_string(lgrInfo.hash)},
 | 
			
		||||
        {JS(ledger_time), lgrInfo.closeTime.time_since_epoch().count()},
 | 
			
		||||
        {JS(changes), json::value_from(BookChanges::compute(transactions))},
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Result
 | 
			
		||||
doBookChanges(Context const& context)
 | 
			
		||||
{
 | 
			
		||||
@@ -237,14 +251,9 @@ doBookChanges(Context const& context)
 | 
			
		||||
        return *status;
 | 
			
		||||
 | 
			
		||||
    auto const lgrInfo = std::get<ripple::LedgerInfo>(info);
 | 
			
		||||
    auto const changes = BookChangesHandler{context}.handle(lgrInfo);
 | 
			
		||||
    return json::object{
 | 
			
		||||
        {JS(type), "bookChanges"},
 | 
			
		||||
        {JS(ledger_index), lgrInfo.seq},
 | 
			
		||||
        {JS(ledger_hash), to_string(lgrInfo.hash)},
 | 
			
		||||
        {JS(ledger_time), lgrInfo.closeTime.time_since_epoch().count()},
 | 
			
		||||
        {JS(changes), json::value_from(changes)},
 | 
			
		||||
    };
 | 
			
		||||
    auto const transactions = context.backend->fetchAllTransactionsInLedger(
 | 
			
		||||
        lgrInfo.seq, context.yield);
 | 
			
		||||
    return computeBookChanges(lgrInfo, transactions);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace RPC
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,8 @@ static std::unordered_set<std::string> validCommonStreams{
 | 
			
		||||
    "transactions",
 | 
			
		||||
    "transactions_proposed",
 | 
			
		||||
    "validations",
 | 
			
		||||
    "manifests"};
 | 
			
		||||
    "manifests",
 | 
			
		||||
    "book_changes"};
 | 
			
		||||
 | 
			
		||||
Status
 | 
			
		||||
validateStreams(boost::json::object const& request)
 | 
			
		||||
@@ -57,6 +58,8 @@ subscribeToStreams(
 | 
			
		||||
            manager.subValidation(session);
 | 
			
		||||
        else if (s == "manifests")
 | 
			
		||||
            manager.subManifest(session);
 | 
			
		||||
        else if (s == "book_changes")
 | 
			
		||||
            manager.subBookChanges(session);
 | 
			
		||||
        else
 | 
			
		||||
            assert(false);
 | 
			
		||||
    }
 | 
			
		||||
@@ -85,6 +88,8 @@ unsubscribeToStreams(
 | 
			
		||||
            manager.unsubValidation(session);
 | 
			
		||||
        else if (s == "manifests")
 | 
			
		||||
            manager.unsubManifest(session);
 | 
			
		||||
        else if (s == "book_changes")
 | 
			
		||||
            manager.unsubBookChanges(session);
 | 
			
		||||
        else
 | 
			
		||||
            assert(false);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,7 @@ Subscription::unsubscribe(std::shared_ptr<WsBase> const& session)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
Subscription::publish(std::shared_ptr<Message>& message)
 | 
			
		||||
Subscription::publish(std::shared_ptr<Message> const& message)
 | 
			
		||||
{
 | 
			
		||||
    boost::asio::post(strand_, [this, message]() {
 | 
			
		||||
        sendToSubscribers(message, subscribers_, subCount_);
 | 
			
		||||
@@ -235,6 +235,22 @@ SubscriptionManager::unsubBook(
 | 
			
		||||
    bookSubscribers_.unsubscribe(session, book);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscriptionManager::subBookChanges(std::shared_ptr<WsBase> session)
 | 
			
		||||
{
 | 
			
		||||
    bookChangesSubscribers_.subscribe(session);
 | 
			
		||||
 | 
			
		||||
    std::unique_lock lk(cleanupMtx_);
 | 
			
		||||
    cleanupFuncs_[session].emplace_back(
 | 
			
		||||
        [this](session_ptr session) { unsubBookChanges(session); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscriptionManager::unsubBookChanges(std::shared_ptr<WsBase> session)
 | 
			
		||||
{
 | 
			
		||||
    bookChangesSubscribers_.unsubscribe(session);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscriptionManager::pubLedger(
 | 
			
		||||
    ripple::LedgerInfo const& lgrInfo,
 | 
			
		||||
@@ -345,6 +361,20 @@ SubscriptionManager::pubTransaction(
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscriptionManager::pubBookChanges(
 | 
			
		||||
    ripple::LedgerInfo const& lgrInfo,
 | 
			
		||||
    std::vector<Backend::TransactionAndMetadata> const& transactions)
 | 
			
		||||
{
 | 
			
		||||
    if (bookChangesSubscribers_.empty())
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    auto const json = RPC::computeBookChanges(lgrInfo, transactions);
 | 
			
		||||
    auto const bookChangesMsg =
 | 
			
		||||
        std::make_shared<Message>(boost::json::serialize(json));
 | 
			
		||||
    bookChangesSubscribers_.publish(bookChangesMsg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscriptionManager::forwardProposedTransaction(
 | 
			
		||||
    boost::json::object const& response)
 | 
			
		||||
 
 | 
			
		||||
@@ -31,13 +31,19 @@ public:
 | 
			
		||||
    unsubscribe(std::shared_ptr<WsBase> const& session);
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    publish(std::shared_ptr<Message>& message);
 | 
			
		||||
    publish(std::shared_ptr<Message> const& message);
 | 
			
		||||
 | 
			
		||||
    std::uint64_t
 | 
			
		||||
    count()
 | 
			
		||||
    count() const
 | 
			
		||||
    {
 | 
			
		||||
        return subCount_.load();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    bool
 | 
			
		||||
    empty() const
 | 
			
		||||
    {
 | 
			
		||||
        return count() == 0;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <class Key>
 | 
			
		||||
@@ -90,6 +96,7 @@ class SubscriptionManager
 | 
			
		||||
    Subscription txProposedSubscribers_;
 | 
			
		||||
    Subscription manifestSubscribers_;
 | 
			
		||||
    Subscription validationsSubscribers_;
 | 
			
		||||
    Subscription bookChangesSubscribers_;
 | 
			
		||||
 | 
			
		||||
    SubscriptionMap<ripple::AccountID> accountSubscribers_;
 | 
			
		||||
    SubscriptionMap<ripple::AccountID> accountProposedSubscribers_;
 | 
			
		||||
@@ -122,6 +129,7 @@ public:
 | 
			
		||||
        , txProposedSubscribers_(ioc_)
 | 
			
		||||
        , manifestSubscribers_(ioc_)
 | 
			
		||||
        , validationsSubscribers_(ioc_)
 | 
			
		||||
        , bookChangesSubscribers_(ioc_)
 | 
			
		||||
        , accountSubscribers_(ioc_)
 | 
			
		||||
        , accountProposedSubscribers_(ioc_)
 | 
			
		||||
        , bookSubscribers_(ioc_)
 | 
			
		||||
@@ -159,6 +167,11 @@ public:
 | 
			
		||||
        std::string const& ledgerRange,
 | 
			
		||||
        std::uint32_t txnCount);
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    pubBookChanges(
 | 
			
		||||
        ripple::LedgerInfo const& lgrInfo,
 | 
			
		||||
        std::vector<Backend::TransactionAndMetadata> const& transactions);
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    unsubLedger(session_ptr session);
 | 
			
		||||
 | 
			
		||||
@@ -185,6 +198,12 @@ public:
 | 
			
		||||
    void
 | 
			
		||||
    unsubBook(ripple::Book const& book, session_ptr session);
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    subBookChanges(std::shared_ptr<WsBase> session);
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    unsubBookChanges(std::shared_ptr<WsBase> session);
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    subManifest(session_ptr session);
 | 
			
		||||
 | 
			
		||||
@@ -234,6 +253,7 @@ public:
 | 
			
		||||
        counts["account"] = accountSubscribers_.count();
 | 
			
		||||
        counts["accounts_proposed"] = accountProposedSubscribers_.count();
 | 
			
		||||
        counts["books"] = bookSubscribers_.count();
 | 
			
		||||
        counts["book_changes"] = bookChangesSubscribers_.count();
 | 
			
		||||
 | 
			
		||||
        return counts;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								test.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								test.py
									
									
									
									
									
								
							@@ -836,6 +836,7 @@ async def subscribe(ip, port):
 | 
			
		||||
    try:
 | 
			
		||||
        async with websockets.connect(address) as ws:
 | 
			
		||||
            await ws.send(json.dumps({"command":"subscribe","streams":["ledger"]}))
 | 
			
		||||
            #await ws.send(json.dumps({"command":"subscribe","streams":["book_changes"]}))
 | 
			
		||||
            #await ws.send(json.dumps({"command":"subscribe","streams":["manifests"]}))
 | 
			
		||||
            while True:
 | 
			
		||||
                res = json.loads(await ws.recv())
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user