Clio develop
The XRP Ledger API server.
Loading...
Searching...
No Matches
BookChangesHelper.hpp
Go to the documentation of this file.
1//------------------------------------------------------------------------------
2/*
3 This file is part of clio: https://github.com/XRPLF/clio
4 Copyright (c) 2023, the clio developers.
5
6 Permission to use, copy, modify, and distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
21#pragma once
22
23#include "data/Types.hpp"
24#include "rpc/JS.hpp"
25#include "rpc/RPCHelpers.hpp"
26
27#include <boost/json/conversion.hpp>
28#include <boost/json/object.hpp>
29#include <boost/json/value.hpp>
30#include <xrpl/beast/utility/Zero.h>
31#include <xrpl/protocol/IOUAmount.h>
32#include <xrpl/protocol/Issue.h>
33#include <xrpl/protocol/LedgerFormats.h>
34#include <xrpl/protocol/LedgerHeader.h>
35#include <xrpl/protocol/SField.h>
36#include <xrpl/protocol/STAmount.h>
37#include <xrpl/protocol/STObject.h>
38#include <xrpl/protocol/STTx.h>
39#include <xrpl/protocol/TxFormats.h>
40#include <xrpl/protocol/XRPAmount.h>
41#include <xrpl/protocol/jss.h>
42
43#include <algorithm>
44#include <cstdint>
45#include <iterator>
46#include <map>
47#include <memory>
48#include <optional>
49#include <set>
50#include <string>
51#include <vector>
52
53namespace rpc {
54
58struct BookChange {
59 ripple::STAmount sideAVolume;
60 ripple::STAmount sideBVolume;
61 ripple::STAmount highRate;
62 ripple::STAmount lowRate;
63 ripple::STAmount openRate;
64 ripple::STAmount closeRate;
65};
66
70class BookChanges final {
71public:
72 BookChanges() = delete; // only accessed via static handle function
73
80 [[nodiscard]] static std::vector<BookChange>
81 compute(std::vector<data::TransactionAndMetadata> const& transactions)
82 {
83 return HandlerImpl{}(transactions);
84 }
85
86private:
87 class HandlerImpl final {
88 std::map<std::string, BookChange> tally_;
89 std::optional<uint32_t> offerCancel_;
90
91 public:
92 [[nodiscard]] std::vector<BookChange>
93 operator()(std::vector<data::TransactionAndMetadata> const& transactions)
94 {
95 for (auto const& tx : transactions)
96 handleBookChange(tx);
97
98 // TODO: rewrite this with std::ranges when compilers catch up
99 std::vector<BookChange> changes;
100 std::transform(
101 std::make_move_iterator(std::begin(tally_)),
102 std::make_move_iterator(std::end(tally_)),
103 std::back_inserter(changes),
104 [](auto obj) { return obj.second; }
105 );
106 return changes;
107 }
108
109 private:
110 void
111 handleAffectedNode(ripple::STObject const& node)
112 {
113 auto const& metaType = node.getFName();
114 auto const nodeType = node.getFieldU16(ripple::sfLedgerEntryType);
115
116 // we only care about ripple::ltOFFER objects being modified or
117 // deleted
118 if (nodeType != ripple::ltOFFER || metaType == ripple::sfCreatedNode)
119 return;
120
121 // if either FF or PF are missing we can't compute
122 // but generally these are cancelled rather than crossed
123 // so skipping them is consistent
124 if (!node.isFieldPresent(ripple::sfFinalFields) || !node.isFieldPresent(ripple::sfPreviousFields))
125 return;
126
127 auto const& finalFields = node.peekAtField(ripple::sfFinalFields).downcast<ripple::STObject>();
128 auto const& previousFields = node.peekAtField(ripple::sfPreviousFields).downcast<ripple::STObject>();
129
130 // defensive case that should never be hit
131 if (!finalFields.isFieldPresent(ripple::sfTakerGets) || !finalFields.isFieldPresent(ripple::sfTakerPays) ||
132 !previousFields.isFieldPresent(ripple::sfTakerGets) ||
133 !previousFields.isFieldPresent(ripple::sfTakerPays))
134 return;
135
136 // filter out any offers deleted by explicit offer cancels
137 if (metaType == ripple::sfDeletedNode && offerCancel_ &&
138 finalFields.getFieldU32(ripple::sfSequence) == *offerCancel_)
139 return;
140
141 // compute the difference in gets and pays actually
142 // affected onto the offer
143 auto const deltaGets =
144 finalFields.getFieldAmount(ripple::sfTakerGets) - previousFields.getFieldAmount(ripple::sfTakerGets);
145 auto const deltaPays =
146 finalFields.getFieldAmount(ripple::sfTakerPays) - previousFields.getFieldAmount(ripple::sfTakerPays);
147
148 transformAndStore(deltaGets, deltaPays);
149 }
150
151 void
152 transformAndStore(ripple::STAmount const& deltaGets, ripple::STAmount const& deltaPays)
153 {
154 auto const g = to_string(deltaGets.issue());
155 auto const p = to_string(deltaPays.issue());
156
157 auto const noswap = [&]() {
158 if (isXRP(deltaGets))
159 return true;
160 return isXRP(deltaPays) ? false : (g < p);
161 }();
162
163 auto first = noswap ? deltaGets : deltaPays;
164 auto second = noswap ? deltaPays : deltaGets;
165
166 // defensively programmed, should (probably) never happen
167 if (second == beast::zero)
168 return;
169
170 auto const rate = divide(first, second, ripple::noIssue());
171
172 if (first < beast::zero)
173 first = -first;
174
175 if (second < beast::zero)
176 second = -second;
177
178 auto const key = noswap ? (g + '|' + p) : (p + '|' + g);
179 if (tally_.contains(key)) {
180 auto& entry = tally_.at(key);
181
182 entry.sideAVolume += first;
183 entry.sideBVolume += second;
184
185 if (entry.highRate < rate)
186 entry.highRate = rate;
187
188 if (entry.lowRate > rate)
189 entry.lowRate = rate;
190
191 entry.closeRate = rate;
192 } else {
193 tally_[key] = {
194 .sideAVolume = first,
195 .sideBVolume = second,
196 .highRate = rate,
197 .lowRate = rate,
198 .openRate = rate,
199 .closeRate = rate,
200 };
201 }
202 }
203
204 void
205 handleBookChange(data::TransactionAndMetadata const& blob)
206 {
207 auto const [tx, meta] = rpc::deserializeTxPlusMeta(blob);
208 if (!tx || !meta || !tx->isFieldPresent(ripple::sfTransactionType))
209 return;
210
211 offerCancel_ = shouldCancelOffer(tx);
212 for (auto const& node : meta->getFieldArray(ripple::sfAffectedNodes))
213 handleAffectedNode(node);
214 }
215
216 static std::optional<uint32_t>
217 shouldCancelOffer(std::shared_ptr<ripple::STTx const> const& tx)
218 {
219 switch (tx->getFieldU16(ripple::sfTransactionType)) {
220 // in future if any other ways emerge to cancel an offer
221 // this switch makes them easy to add
222 case ripple::ttOFFER_CANCEL:
223 case ripple::ttOFFER_CREATE:
224 if (tx->isFieldPresent(ripple::sfOfferSequence))
225 return tx->getFieldU32(ripple::sfOfferSequence);
226 [[fallthrough]];
227 default:
228 return std::nullopt;
229 }
230 }
231 };
232};
233
240inline void
241tag_invoke(boost::json::value_from_tag, boost::json::value& jv, BookChange const& change)
242{
243 auto amountStr = [](ripple::STAmount const& amount) -> std::string {
244 return isXRP(amount) ? to_string(amount.xrp()) : to_string(amount.iou());
245 };
246
247 auto currencyStr = [](ripple::STAmount const& amount) -> std::string {
248 return isXRP(amount) ? "XRP_drops" : to_string(amount.issue());
249 };
250
251 jv = {
252 {JS(currency_a), currencyStr(change.sideAVolume)},
253 {JS(currency_b), currencyStr(change.sideBVolume)},
254 {JS(volume_a), amountStr(change.sideAVolume)},
255 {JS(volume_b), amountStr(change.sideBVolume)},
256 {JS(high), to_string(change.highRate.iou())},
257 {JS(low), to_string(change.lowRate.iou())},
258 {JS(open), to_string(change.openRate.iou())},
259 {JS(close), to_string(change.closeRate.iou())},
260 };
261}
262
270[[nodiscard]] boost::json::object
271computeBookChanges(ripple::LedgerHeader const& lgrInfo, std::vector<data::TransactionAndMetadata> const& transactions);
272
273} // namespace rpc
Encapsulates the book_changes computations and transformations.
Definition BookChangesHelper.hpp:70
static std::vector< BookChange > compute(std::vector< data::TransactionAndMetadata > const &transactions)
Computes all book_changes for the given transactions.
Definition BookChangesHelper.hpp:81
This namespace contains all the RPC logic and handlers.
Definition AMMHelpers.cpp:36
std::pair< std::shared_ptr< ripple::STTx const >, std::shared_ptr< ripple::STObject const > > deserializeTxPlusMeta(data::TransactionAndMetadata const &blobs)
Deserialize a TransactionAndMetadata into a pair of STTx and STObject.
Definition RPCHelpers.cpp:207
Represents a transaction and its metadata bundled together.
Definition Types.hpp:68
Represents an entry in the book_changes' changes array.
Definition BookChangesHelper.hpp:58