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 std::optional<ripple::uint256> domain;
66};
67
71class BookChanges final {
72public:
73 BookChanges() = delete; // only accessed via static handle function
74
81 [[nodiscard]] static std::vector<BookChange>
82 compute(std::vector<data::TransactionAndMetadata> const& transactions)
83 {
84 return HandlerImpl{}(transactions);
85 }
86
87private:
88 class HandlerImpl final {
89 std::map<std::string, BookChange> tally_;
90 std::optional<uint32_t> offerCancel_;
91
92 public:
93 [[nodiscard]] std::vector<BookChange>
94 operator()(std::vector<data::TransactionAndMetadata> const& transactions)
95 {
96 for (auto const& tx : transactions)
97 handleBookChange(tx);
98
99 // TODO: rewrite this with std::ranges when compilers catch up
100 std::vector<BookChange> changes;
101 std::transform(
102 std::make_move_iterator(std::begin(tally_)),
103 std::make_move_iterator(std::end(tally_)),
104 std::back_inserter(changes),
105 [](auto obj) { return obj.second; }
106 );
107 return changes;
108 }
109
110 private:
111 void
112 handleAffectedNode(ripple::STObject const& node)
113 {
114 auto const& metaType = node.getFName();
115 auto const nodeType = node.getFieldU16(ripple::sfLedgerEntryType);
116
117 // we only care about ripple::ltOFFER objects being modified or
118 // deleted
119 if (nodeType != ripple::ltOFFER || metaType == ripple::sfCreatedNode)
120 return;
121
122 // if either FF or PF are missing we can't compute
123 // but generally these are cancelled rather than crossed
124 // so skipping them is consistent
125 if (!node.isFieldPresent(ripple::sfFinalFields) ||
126 !node.isFieldPresent(ripple::sfPreviousFields))
127 return;
128
129 auto const& finalFields =
130 node.peekAtField(ripple::sfFinalFields).downcast<ripple::STObject>();
131 auto const& previousFields =
132 node.peekAtField(ripple::sfPreviousFields).downcast<ripple::STObject>();
133
134 // defensive case that should never be hit
135 if (!finalFields.isFieldPresent(ripple::sfTakerGets) ||
136 !finalFields.isFieldPresent(ripple::sfTakerPays) ||
137 !previousFields.isFieldPresent(ripple::sfTakerGets) ||
138 !previousFields.isFieldPresent(ripple::sfTakerPays))
139 return;
140
141 // filter out any offers deleted by explicit offer cancels
142 if (metaType == ripple::sfDeletedNode && offerCancel_ &&
143 finalFields.getFieldU32(ripple::sfSequence) == *offerCancel_)
144 return;
145
146 // compute the difference in gets and pays actually
147 // affected onto the offer
148 auto const deltaGets = finalFields.getFieldAmount(ripple::sfTakerGets) -
149 previousFields.getFieldAmount(ripple::sfTakerGets);
150 auto const deltaPays = finalFields.getFieldAmount(ripple::sfTakerPays) -
151 previousFields.getFieldAmount(ripple::sfTakerPays);
152
153 transformAndStore(deltaGets, deltaPays, finalFields[~ripple::sfDomainID]);
154 }
155
156 void
157 transformAndStore(
158 ripple::STAmount const& deltaGets,
159 ripple::STAmount const& deltaPays,
160 std::optional<ripple::uint256> const& domain
161 )
162 {
163 auto const g = to_string(deltaGets.issue());
164 auto const p = to_string(deltaPays.issue());
165
166 auto const noswap = [&]() {
167 if (isXRP(deltaGets))
168 return true;
169 return isXRP(deltaPays) ? false : (g < p);
170 }();
171
172 auto first = noswap ? deltaGets : deltaPays;
173 auto second = noswap ? deltaPays : deltaGets;
174
175 // defensively programmed, should (probably) never happen
176 if (second == beast::zero)
177 return;
178
179 auto const rate = divide(first, second, ripple::noIssue());
180
181 if (first < beast::zero)
182 first = -first;
183
184 if (second < beast::zero)
185 second = -second;
186
187 auto const key = noswap ? (g + '|' + p) : (p + '|' + g);
188 if (tally_.contains(key)) {
189 auto& entry = tally_.at(key);
190
191 entry.sideAVolume += first;
192 entry.sideBVolume += second;
193
194 if (entry.highRate < rate)
195 entry.highRate = rate;
196
197 if (entry.lowRate > rate)
198 entry.lowRate = rate;
199
200 entry.closeRate = rate;
201 entry.domain = domain;
202 } else {
203 tally_[key] = {
204 .sideAVolume = first,
205 .sideBVolume = second,
206 .highRate = rate,
207 .lowRate = rate,
208 .openRate = rate,
209 .closeRate = rate,
210 .domain = domain,
211 };
212 }
213 }
214
215 void
216 handleBookChange(data::TransactionAndMetadata const& blob)
217 {
218 auto const [tx, meta] = rpc::deserializeTxPlusMeta(blob);
219 if (!tx || !meta || !tx->isFieldPresent(ripple::sfTransactionType))
220 return;
221
222 offerCancel_ = shouldCancelOffer(tx);
223 for (auto const& node : meta->getFieldArray(ripple::sfAffectedNodes))
224 handleAffectedNode(node);
225 }
226
227 static std::optional<uint32_t>
228 shouldCancelOffer(std::shared_ptr<ripple::STTx const> const& tx)
229 {
230 switch (tx->getFieldU16(ripple::sfTransactionType)) {
231 // in future if any other ways emerge to cancel an offer
232 // this switch makes them easy to add
233 case ripple::ttOFFER_CANCEL:
234 case ripple::ttOFFER_CREATE:
235 if (tx->isFieldPresent(ripple::sfOfferSequence))
236 return tx->getFieldU32(ripple::sfOfferSequence);
237 [[fallthrough]];
238 default:
239 return std::nullopt;
240 }
241 }
242 };
243};
244
251inline void
252tag_invoke(boost::json::value_from_tag, boost::json::value& jv, BookChange const& change)
253{
254 auto amountStr = [](ripple::STAmount const& amount) -> std::string {
255 return isXRP(amount) ? to_string(amount.xrp()) : to_string(amount.iou());
256 };
257
258 auto currencyStr = [](ripple::STAmount const& amount) -> std::string {
259 return isXRP(amount) ? "XRP_drops" : to_string(amount.issue());
260 };
261
262 jv = {
263 {JS(currency_a), currencyStr(change.sideAVolume)},
264 {JS(currency_b), currencyStr(change.sideBVolume)},
265 {JS(volume_a), amountStr(change.sideAVolume)},
266 {JS(volume_b), amountStr(change.sideBVolume)},
267 {JS(high), to_string(change.highRate.iou())},
268 {JS(low), to_string(change.lowRate.iou())},
269 {JS(open), to_string(change.openRate.iou())},
270 {JS(close), to_string(change.closeRate.iou())},
271 };
272
273 if (change.domain.has_value())
274 jv.as_object()[JS(domain)] = ripple::to_string(*change.domain);
275}
276
284[[nodiscard]] boost::json::object
285computeBookChanges(
286 ripple::LedgerHeader const& lgrInfo,
287 std::vector<data::TransactionAndMetadata> const& transactions
288);
289
290} // namespace rpc
static std::vector< BookChange > compute(std::vector< data::TransactionAndMetadata > const &transactions)
Computes all book_changes for the given transactions.
Definition BookChangesHelper.hpp:82
This namespace contains all the RPC logic and handlers.
Definition AMMHelpers.cpp:37
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:204
void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, BookChange const &change)
Implementation of value_from for BookChange type.
Definition BookChangesHelper.hpp:252
Represents an entry in the book_changes' changes array.
Definition BookChangesHelper.hpp:58