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) || !node.isFieldPresent(ripple::sfPreviousFields))
126 return;
127
128 auto const& finalFields = node.peekAtField(ripple::sfFinalFields).downcast<ripple::STObject>();
129 auto const& previousFields = node.peekAtField(ripple::sfPreviousFields).downcast<ripple::STObject>();
130
131 // defensive case that should never be hit
132 if (!finalFields.isFieldPresent(ripple::sfTakerGets) || !finalFields.isFieldPresent(ripple::sfTakerPays) ||
133 !previousFields.isFieldPresent(ripple::sfTakerGets) ||
134 !previousFields.isFieldPresent(ripple::sfTakerPays))
135 return;
136
137 // filter out any offers deleted by explicit offer cancels
138 if (metaType == ripple::sfDeletedNode && offerCancel_ &&
139 finalFields.getFieldU32(ripple::sfSequence) == *offerCancel_)
140 return;
141
142 // compute the difference in gets and pays actually
143 // affected onto the offer
144 auto const deltaGets =
145 finalFields.getFieldAmount(ripple::sfTakerGets) - previousFields.getFieldAmount(ripple::sfTakerGets);
146 auto const deltaPays =
147 finalFields.getFieldAmount(ripple::sfTakerPays) - previousFields.getFieldAmount(ripple::sfTakerPays);
148
149 transformAndStore(deltaGets, deltaPays, finalFields[~ripple::sfDomainID]);
150 }
151
152 void
153 transformAndStore(
154 ripple::STAmount const& deltaGets,
155 ripple::STAmount const& deltaPays,
156 std::optional<ripple::uint256> const& domain
157 )
158 {
159 auto const g = to_string(deltaGets.issue());
160 auto const p = to_string(deltaPays.issue());
161
162 auto const noswap = [&]() {
163 if (isXRP(deltaGets))
164 return true;
165 return isXRP(deltaPays) ? false : (g < p);
166 }();
167
168 auto first = noswap ? deltaGets : deltaPays;
169 auto second = noswap ? deltaPays : deltaGets;
170
171 // defensively programmed, should (probably) never happen
172 if (second == beast::zero)
173 return;
174
175 auto const rate = divide(first, second, ripple::noIssue());
176
177 if (first < beast::zero)
178 first = -first;
179
180 if (second < beast::zero)
181 second = -second;
182
183 auto const key = noswap ? (g + '|' + p) : (p + '|' + g);
184 if (tally_.contains(key)) {
185 auto& entry = tally_.at(key);
186
187 entry.sideAVolume += first;
188 entry.sideBVolume += second;
189
190 if (entry.highRate < rate)
191 entry.highRate = rate;
192
193 if (entry.lowRate > rate)
194 entry.lowRate = rate;
195
196 entry.closeRate = rate;
197 entry.domain = domain;
198 } else {
199 tally_[key] = {
200 .sideAVolume = first,
201 .sideBVolume = second,
202 .highRate = rate,
203 .lowRate = rate,
204 .openRate = rate,
205 .closeRate = rate,
206 .domain = domain,
207 };
208 }
209 }
210
211 void
212 handleBookChange(data::TransactionAndMetadata const& blob)
213 {
214 auto const [tx, meta] = rpc::deserializeTxPlusMeta(blob);
215 if (!tx || !meta || !tx->isFieldPresent(ripple::sfTransactionType))
216 return;
217
218 offerCancel_ = shouldCancelOffer(tx);
219 for (auto const& node : meta->getFieldArray(ripple::sfAffectedNodes))
220 handleAffectedNode(node);
221 }
222
223 static std::optional<uint32_t>
224 shouldCancelOffer(std::shared_ptr<ripple::STTx const> const& tx)
225 {
226 switch (tx->getFieldU16(ripple::sfTransactionType)) {
227 // in future if any other ways emerge to cancel an offer
228 // this switch makes them easy to add
229 case ripple::ttOFFER_CANCEL:
230 case ripple::ttOFFER_CREATE:
231 if (tx->isFieldPresent(ripple::sfOfferSequence))
232 return tx->getFieldU32(ripple::sfOfferSequence);
233 [[fallthrough]];
234 default:
235 return std::nullopt;
236 }
237 }
238 };
239};
240
247inline void
248tag_invoke(boost::json::value_from_tag, boost::json::value& jv, BookChange const& change)
249{
250 auto amountStr = [](ripple::STAmount const& amount) -> std::string {
251 return isXRP(amount) ? to_string(amount.xrp()) : to_string(amount.iou());
252 };
253
254 auto currencyStr = [](ripple::STAmount const& amount) -> std::string {
255 return isXRP(amount) ? "XRP_drops" : to_string(amount.issue());
256 };
257
258 jv = {
259 {JS(currency_a), currencyStr(change.sideAVolume)},
260 {JS(currency_b), currencyStr(change.sideBVolume)},
261 {JS(volume_a), amountStr(change.sideAVolume)},
262 {JS(volume_b), amountStr(change.sideBVolume)},
263 {JS(high), to_string(change.highRate.iou())},
264 {JS(low), to_string(change.lowRate.iou())},
265 {JS(open), to_string(change.openRate.iou())},
266 {JS(close), to_string(change.closeRate.iou())},
267 };
268
269 if (change.domain.has_value())
270 jv.as_object()[JS(domain)] = ripple::to_string(*change.domain);
271}
272
280[[nodiscard]] boost::json::object
281computeBookChanges(ripple::LedgerHeader const& lgrInfo, std::vector<data::TransactionAndMetadata> const& transactions);
282
283} // namespace rpc
Encapsulates the book_changes computations and transformations.
Definition BookChangesHelper.hpp:71
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:209
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