xrpld
Loading...
Searching...
No Matches
BookChanges.h
1#pragma once
2
3#include <xrpl/basics/base_uint.h>
4#include <xrpl/beast/utility/Zero.h>
5#include <xrpl/json/json_value.h>
6#include <xrpl/protocol/Asset.h>
7#include <xrpl/protocol/Issue.h>
8#include <xrpl/protocol/LedgerFormats.h>
9#include <xrpl/protocol/MPTIssue.h>
10#include <xrpl/protocol/Protocol.h>
11#include <xrpl/protocol/SField.h>
12#include <xrpl/protocol/STAmount.h>
13#include <xrpl/protocol/STObject.h>
14#include <xrpl/protocol/TxFormats.h>
15#include <xrpl/protocol/jss.h>
16
17#include <cstdint>
18#include <map>
19#include <memory>
20#include <optional>
21#include <sstream>
22#include <stdexcept>
23#include <string>
24#include <tuple>
25
26namespace json {
27class Value;
28} // namespace json
29
30namespace xrpl {
31
32class ReadView;
33class Transaction;
34class TxMeta;
35class STTx;
36
37namespace rpc {
38
39template <class L>
40json::Value
42{
46 STAmount, // side A volume
47 STAmount, // side B volume
48 STAmount, // high rate
49 STAmount, // low rate
50 STAmount, // open rate
51 STAmount, // close rate
52 std::optional<UInt256>>> // optional: domain id
53 tally;
54
55 // Accumulating volume can exceed what the asset can represent, and the two
56 // types fail differently: STAmount's IOU addition throws, while its MPT
57 // addition is a raw int64 add that wraps past kMaxMpTokenAmount to a
58 // negative amount. Reject both so that one extreme crossing cannot poison
59 // this ledger's report, which is otherwise permanent -- the ledger is
60 // immutable and the computation deterministic.
61 auto const checkedAdd = [](STAmount& acc, STAmount const& delta) {
62 return acc.asset().visit(
63 [&](Issue const&) {
64 try
65 {
66 acc += delta;
67 }
68 catch (std::overflow_error const&)
69 {
70 return false;
71 }
72 return true;
73 },
74 [&](MPTIssue const&) {
75 // Both volumes are non-negative by the time they reach the
76 // tally, so this cannot underflow.
77 auto const room = static_cast<std::int64_t>(kMaxMpTokenAmount) - acc.mpt().value();
78 if (delta.mpt().value() > room)
79 return false;
80 acc += delta;
81 return true;
82 });
83 };
84
85 for (auto& tx : lpAccepted->txs)
86 {
87 if (!tx.first || !tx.second || !tx.first->isFieldPresent(sfTransactionType))
88 continue;
89
90 std::optional<uint32_t> offerCancel;
91 uint16_t const tt = tx.first->getFieldU16(sfTransactionType);
92 switch (tt)
93 {
94 case ttOFFER_CANCEL:
95 case ttOFFER_CREATE: {
96 if (tx.first->isFieldPresent(sfOfferSequence))
97 offerCancel = tx.first->getFieldU32(sfOfferSequence);
98 break;
99 }
100 // in future if any other ways emerge to cancel an offer
101 // this switch makes them easy to add
102 default:
103 break;
104 }
105
106 for (auto const& node : tx.second->getFieldArray(sfAffectedNodes))
107 {
108 SField const& metaType = node.getFName();
109 uint16_t const nodeType = node.getFieldU16(sfLedgerEntryType);
110
111 // we only care about ltOFFER objects being modified or
112 // deleted
113 if (nodeType != ltOFFER || metaType == sfCreatedNode)
114 continue;
115
116 // if either FF or PF are missing we can't compute
117 // but generally these are cancelled rather than crossed
118 // so skipping them is consistent
119 if (!node.isFieldPresent(sfFinalFields) || !node.isFieldPresent(sfPreviousFields))
120 continue;
121
122 auto const& ffBase = node.peekAtField(sfFinalFields);
123 auto const& finalFields = ffBase.template downcast<STObject>();
124 auto const& pfBase = node.peekAtField(sfPreviousFields);
125 auto const& previousFields = pfBase.template downcast<STObject>();
126
127 // defensive case that should never be hit
128 if (!finalFields.isFieldPresent(sfTakerGets) ||
129 !finalFields.isFieldPresent(sfTakerPays) ||
130 !previousFields.isFieldPresent(sfTakerGets) ||
131 !previousFields.isFieldPresent(sfTakerPays))
132 continue;
133
134 // filter out any offers deleted by explicit offer cancels
135 if (metaType == sfDeletedNode && offerCancel &&
136 finalFields.getFieldU32(sfSequence) == *offerCancel)
137 continue;
138
139 // compute the difference in gets and pays actually
140 // affected onto the offer
141 STAmount const deltaGets = finalFields.getFieldAmount(sfTakerGets) -
142 previousFields.getFieldAmount(sfTakerGets);
143 STAmount const deltaPays = finalFields.getFieldAmount(sfTakerPays) -
144 previousFields.getFieldAmount(sfTakerPays);
145
146 std::string const g{to_string(deltaGets.asset())};
147 std::string const p{to_string(deltaPays.asset())};
148
149 bool const noswap = isXRP(deltaGets) || (!isXRP(deltaPays) && (g < p));
150
151 STAmount first = noswap ? deltaGets : deltaPays;
152 STAmount second = noswap ? deltaPays : deltaGets;
153
154 // defensively programmed, should (probably) never happen
155 if (second == beast::kZero)
156 continue;
157
158 std::optional<STAmount> maybeRate;
159 try
160 {
161 maybeRate = divide(first, second, noIssue());
162 }
163 catch (std::overflow_error const&)
164 {
165 continue;
166 }
167 STAmount const rate = *maybeRate;
168
169 if (first < beast::kZero)
170 first = -first;
171
172 if (second < beast::kZero)
173 second = -second;
174
176 if (noswap)
177 {
178 ss << g << "|" << p;
179 }
180 else
181 {
182 ss << p << "|" << g;
183 }
184
185 std::optional<UInt256> const domain = finalFields[~sfDomainID];
186
187 std::string const key{ss.str()};
188
189 if (!tally.contains(key))
190 {
191 tally[key] = {
192 first, // side A vol
193 second, // side B vol
194 rate, // high
195 rate, // low
196 rate, // open
197 rate, // close
198 domain};
199 }
200 else
201 {
202 // increment volume
203 auto& entry = tally[key];
204
205 // Commit both sides or neither, so an overflow on the second
206 // cannot leave the entry half-updated. Skipping the crossing
207 // matches how an unrepresentable rate is handled above.
208 STAmount volA = std::get<0>(entry);
209 STAmount volB = std::get<1>(entry);
210 if (!checkedAdd(volA, first) || !checkedAdd(volB, second))
211 continue;
212 std::get<0>(entry) = volA; // side A vol
213 std::get<1>(entry) = volB; // side B vol
214
215 if (std::get<2>(entry) < rate) // high
216 std::get<2>(entry) = rate;
217
218 if (std::get<3>(entry) > rate) // low
219 std::get<3>(entry) = rate;
220
221 std::get<5>(entry) = rate; // close
222 std::get<6>(entry) = domain; // domain
223 }
224 }
225 }
226
228 jvObj[jss::type] = "bookChanges";
229
230 // retrieve validated information from LedgerHeader class
231 jvObj[jss::validated] = lpAccepted->header().validated;
232 jvObj[jss::ledger_index] = lpAccepted->header().seq;
233 jvObj[jss::ledger_hash] = to_string(lpAccepted->header().hash);
234 jvObj[jss::ledger_time] =
235 json::Value::UInt(lpAccepted->header().closeTime.time_since_epoch().count());
236
237 jvObj[jss::changes] = json::ValueType::Array;
238
239 auto volToStr = [](STAmount const& vol) {
240 return vol.asset().visit(
241 [&](Issue const& issue) {
242 if (isXRP(issue))
243 return to_string(vol.xrp());
244 return to_string(vol.iou());
245 },
246 [&](MPTIssue const&) { return to_string(vol.mpt()); });
247 };
248
249 for (auto const& entry : tally)
250 {
251 json::Value& inner = jvObj[jss::changes].append(json::ValueType::Object);
252
253 STAmount const volA = std::get<0>(entry.second);
254 STAmount const volB = std::get<1>(entry.second);
255
256 volA.asset().visit(
257 [&](Issue const&) {
258 inner[jss::currency_a] = (isXRP(volA) ? "XRP_drops" : to_string(volA.asset()));
259 },
260 [&](MPTIssue const&) { inner[jss::mpt_issuance_id_a] = to_string(volA.asset()); });
261
262 volB.asset().visit(
263 [&](Issue const&) {
264 inner[jss::currency_b] = (isXRP(volB) ? "XRP_drops" : to_string(volB.asset()));
265 },
266 [&](MPTIssue const&) { inner[jss::mpt_issuance_id_b] = to_string(volB.asset()); });
267
268 inner[jss::volume_a] = volToStr(volA);
269 inner[jss::volume_b] = volToStr(volB);
270
271 inner[jss::high] = to_string(std::get<2>(entry.second).iou());
272 inner[jss::low] = to_string(std::get<3>(entry.second).iou());
273 inner[jss::open] = to_string(std::get<4>(entry.second).iou());
274 inner[jss::close] = to_string(std::get<5>(entry.second).iou());
275
276 std::optional<UInt256> const domain = std::get<6>(entry.second);
277 if (domain)
278 inner[jss::domain] = to_string(*domain);
279 }
280
281 return jvObj;
282}
283
284} // namespace rpc
285} // namespace xrpl
Represents a JSON value.
Definition json_value.h:117
json::UInt UInt
Definition json_value.h:124
Value & append(Value const &value)
Append value to array at the end.
constexpr auto visit(Visitors &&... visitors) const -> decltype(auto)
Definition Asset.h:117
A currency issued by an account.
Definition Issue.h:18
constexpr value_type value() const
Returns the underlying value.
Definition MPTAmount.h:135
A view into a ledger.
Definition ReadView.h:41
Identifies fields.
Definition SField.h:132
Asset const & asset() const
Definition STAmount.h:496
MPTAmount mpt() const
Definition STAmount.cpp:302
constexpr Zero kZero
Definition Zero.h:30
JSON (JavaScript Object Notation).
Definition json_errors.h:5
@ Array
array value (ordered list)
Definition json_value.h:28
@ Object
object value (collection of name/value pairs).
Definition json_value.h:29
API version numbers used in later API versions.
Definition ApiVersion.h:36
json::Value computeBookChanges(std::shared_ptr< L const > const &lpAccepted)
Definition BookChanges.h:41
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
STAmount divide(STAmount const &amount, Rate const &rate)
Definition Rate2.cpp:69
bool isXRP(AccountID const &c)
Definition AccountID.h:84
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
Issue const & noIssue()
Returns an asset specifier that represents no account and currency.
Definition Issue.h:118
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:297
T str(T... args)