xrpld
Loading...
Searching...
No Matches
GetAggregatePrice.cpp
1#include <xrpld/app/ledger/LedgerMaster.h>
2#include <xrpld/app/main/Application.h>
3#include <xrpld/rpc/Context.h>
4#include <xrpld/rpc/detail/RPCLedgerHelpers.h>
5
6#include <xrpl/basics/Number.h>
7#include <xrpl/basics/base_uint.h>
8#include <xrpl/basics/safe_cast.h>
9#include <xrpl/beast/core/LexicalCast.h>
10#include <xrpl/json/json_value.h>
11#include <xrpl/ledger/ReadView.h>
12#include <xrpl/protocol/AccountID.h>
13#include <xrpl/protocol/ErrorCodes.h>
14#include <xrpl/protocol/Indexes.h>
15#include <xrpl/protocol/Issue.h>
16#include <xrpl/protocol/LedgerFormats.h>
17#include <xrpl/protocol/Protocol.h>
18#include <xrpl/protocol/SField.h>
19#include <xrpl/protocol/STAmount.h>
20#include <xrpl/protocol/STArray.h>
21#include <xrpl/protocol/STCurrency.h>
22#include <xrpl/protocol/STObject.h>
23#include <xrpl/protocol/jss.h>
24
25#include <boost/bimap.hpp>
26#include <boost/bimap/bimap.hpp>
27#include <boost/bimap/multiset_of.hpp>
28
29#include <algorithm>
30#include <cstdint>
31#include <functional>
32#include <iterator>
33#include <memory>
34#include <numeric>
35#include <optional>
36#include <set>
37#include <tuple>
38#include <utility>
39#include <variant>
40
41namespace xrpl {
42
43using namespace boost::bimaps;
44// sorted descending by lastUpdateTime, ascending by AssetPrice
45using Prices = bimap<multiset_of<std::uint32_t, std::greater<>>, multiset_of<STAmount>>;
46
51static void
53 rpc::JsonContext& context,
55 std::function<bool(STObject const&)> const& f)
56{
57 static constexpr std::uint8_t kMaxHistory = 3;
58 bool isNew = false;
59 std::uint8_t history = 0;
60
61 // `oracle` points to an object that has an `sfPriceDataSeries` field.
62 // When this function is called, that is a `PriceOracle` ledger object,
63 // but after one iteration of the loop below, it is an `sfNewFields`
64 // / `sfFinalFields` object in a `CreatedNode` / `ModifiedNode` object in
65 // a transaction's metadata.
66
67 // `chain` points to an object that has `sfPreviousTxnID` and
68 // `sfPreviousTxnLgrSeq` fields. When this function is called,
69 // that is the `PriceOracle` ledger object pointed to by `oracle`,
70 // but after one iteration of the loop below, then it is a `ModifiedNode`
71 // / `CreatedNode` object in a transaction's metadata.
72 STObject const* oracle = sle.get();
73 STObject const* chain = oracle;
74 // Use to test an unlikely scenario when CreatedNode / ModifiedNode
75 // for the Oracle is not found in the inner loop
76 STObject const* prevChain = nullptr;
77
79 while (true)
80 {
81 if (prevChain == chain)
82 return;
83
84 if ((oracle == nullptr) || f(*oracle) || isNew)
85 return;
86
87 if (++history > kMaxHistory)
88 return;
89
90 uint256 const prevTx = chain->getFieldH256(sfPreviousTxnID);
91 std::uint32_t const prevSeq = chain->getFieldU32(sfPreviousTxnLgrSeq);
92
93 auto const ledger = context.ledgerMaster.getLedgerBySeq(prevSeq);
94 if (!ledger)
95 return; // LCOV_EXCL_LINE
96
97 meta = ledger->txRead(prevTx).second;
98 if (!meta)
99 return;
100
101 prevChain = chain;
102 for (STObject const& node : meta->getFieldArray(sfAffectedNodes))
103 {
104 if (node.getFieldU16(sfLedgerEntryType) != ltORACLE)
105 {
106 continue;
107 }
108
109 chain = &node;
110 isNew = node.isFieldPresent(sfNewFields);
111 // if a meta is for the new and this is the first
112 // look-up then it's the meta for the tx that
113 // created the current object; i.e. there is no
114 // historical data
115 if (isNew && history == 1)
116 return;
117
118 oracle = isNew ? &safeDowncast<STObject const&>(node.peekAtField(sfNewFields))
119 : &safeDowncast<STObject const&>(node.peekAtField(sfFinalFields));
120 break;
121 }
122 }
123}
124
125// Return avg, sd, data set size
127getStats(Prices::right_const_iterator const& begin, Prices::right_const_iterator const& end)
128{
129 STAmount avg{noIssue(), 0, 0};
130 Number sd{0};
131 std::uint16_t const size = std::distance(begin, end);
132 avg = std::accumulate(
133 begin, end, avg, [&](STAmount const& acc, auto const& it) { return acc + it.first; });
134 avg = divide(avg, STAmount{noIssue(), size, 0}, noIssue());
135 if (size > 1)
136 {
137 sd = std::accumulate(begin, end, sd, [&](Number const& acc, auto const& it) {
138 return acc + (it.first - avg) * (it.first - avg);
139 });
140 sd = root2(sd / (size - 1));
141 }
142 return {avg, sd, size};
143};
144
155{
156 json::Value result;
157 auto const& params(context.params);
158
159 static constexpr std::uint16_t kMaxOracles = 200;
160 if (!params.isMember(jss::oracles))
161 return rpc::missingFieldError(jss::oracles);
162 if (!params[jss::oracles].isArray() || params[jss::oracles].size() == 0 ||
163 params[jss::oracles].size() > kMaxOracles)
164 {
166 return result;
167 }
168
169 if (!params.isMember(jss::base_asset))
170 return rpc::missingFieldError(jss::base_asset);
171
172 if (!params.isMember(jss::quote_asset))
173 return rpc::missingFieldError(jss::quote_asset);
174
175 // Lambda to validate uint type
176 // support positive int, uint, and a number represented as a string
177 auto validUInt = [](json::Value const& params, json::StaticString const& field) {
178 auto const& jv = params[field];
179 std::uint32_t v = 0;
180 return jv.isUInt() || (jv.isInt() && jv.asInt() >= 0) ||
181 (jv.isString() && beast::lexicalCastChecked(v, jv.asString()));
182 };
183
184 // Lambda to get `trim` and `time_threshold` fields. If the field
185 // is not included in the input then a default value is returned.
186 auto getField = [&params, &validUInt](
187 json::StaticString const& field,
188 unsigned int def = 0) -> std::variant<std::uint32_t, ErrorCodeI> {
189 if (params.isMember(field))
190 {
191 if (!validUInt(params, field))
192 return RpcInvalidParams;
193 return params[field].asUInt();
194 }
195 return def;
196 };
197
198 // Lambda to get `base_asset` and `quote_asset`. The values have
199 // to conform to the Currency type.
200 auto getCurrency = [&params](SField const& sField, json::StaticString const& field)
202 try
203 {
204 if (params[field].asString().empty())
205 return RpcInvalidParams;
206 currencyFromJson(sField, params[field]);
207 return params[field];
208 }
209 catch (...)
210 {
211 return RpcInvalidParams;
212 }
213 };
214
215 auto const trim = getField(jss::trim);
217 {
218 rpc::injectError(std::get<ErrorCodeI>(trim), result);
219 return result;
220 }
221 if (params.isMember(jss::trim) &&
222 (std::get<std::uint32_t>(trim) == 0 || std::get<std::uint32_t>(trim) > kMaxTrim))
223 {
225 return result;
226 }
227
228 auto const timeThreshold = getField(jss::time_threshold, 0);
229 if (std::holds_alternative<ErrorCodeI>(timeThreshold))
230 {
231 rpc::injectError(std::get<ErrorCodeI>(timeThreshold), result);
232 return result;
233 }
234
235 auto const baseAsset = getCurrency(sfBaseAsset, jss::base_asset);
237 {
238 rpc::injectError(std::get<ErrorCodeI>(baseAsset), result);
239 return result;
240 }
241 auto const quoteAsset = getCurrency(sfQuoteAsset, jss::quote_asset);
243 {
244 rpc::injectError(std::get<ErrorCodeI>(quoteAsset), result);
245 return result;
246 }
247
249 result = rpc::lookupLedger(ledger, context);
250 if (!ledger)
251 return result; // LCOV_EXCL_LINE
252
253 // Collect the dataset into bimap keyed by lastUpdateTime and
254 // STAmount (Number is int64 and price is uint64)
255 Prices prices;
256 // Track seen {account, documentID} pairs to skip duplicates
258 for (auto const& oracle : params[jss::oracles])
259 {
260 if (!oracle.isMember(jss::oracle_document_id) || !oracle.isMember(jss::account))
261 {
263 return result;
264 }
265 auto const documentID = validUInt(oracle, jss::oracle_document_id)
266 ? std::make_optional(oracle[jss::oracle_document_id].asUInt())
267 : std::nullopt;
268 auto const account = parseBase58<AccountID>(oracle[jss::account].asString());
269 if (!account || account->isZero() || !documentID)
270 {
272 return result;
273 }
274
275 // Skip duplicate oracle entries
276 if (!seen.emplace(*account, *documentID).second)
277 continue;
278
279 auto const sle = ledger->read(keylet::oracle(*account, *documentID));
280 iteratePriceData(context, sle, [&](STObject const& node) {
281 auto const& series = node.getFieldArray(sfPriceDataSeries);
282 // find the token pair entry with the price
283 if (auto iter = std::ranges::find_if(
284 series,
285 [&](STObject const& o) -> bool {
286 return o.getFieldCurrency(sfBaseAsset).getText() ==
287 std::get<json::Value>(baseAsset) &&
288 o.getFieldCurrency(sfQuoteAsset).getText() ==
289 std::get<json::Value>(quoteAsset) &&
290 o.isFieldPresent(sfAssetPrice);
291 });
292 iter != series.end())
293 {
294 auto const price = iter->getFieldU64(sfAssetPrice);
295 auto const scale = iter->isFieldPresent(sfScale)
296 ? -static_cast<int>(iter->getFieldU8(sfScale))
297 : 0;
298 prices.insert(
299 Prices::value_type(
300 node.getFieldU32(sfLastUpdateTime), STAmount{noIssue(), price, scale}));
301 return true;
302 }
303 return false;
304 });
305 }
306
307 if (prices.empty())
308 {
310 return result;
311 }
312
313 // erase outdated data
314 // sorted in descending, therefore begin is the latest, end is the oldest
315 auto const latestTime = prices.left.begin()->first;
316 if (auto const threshold = std::get<std::uint32_t>(timeThreshold))
317 {
318 // threshold defines an acceptable range {max,min} of lastUpdateTime as
319 // {latestTime, latestTime - threshold}. Prices with lastUpdateTime
320 // less than (latestTime - threshold) are erased (outdated prices).
321 auto const oldestTime = prices.left.rbegin()->first;
322 auto const upperBound = latestTime > threshold ? (latestTime - threshold) : oldestTime;
323 if (upperBound > oldestTime)
324 prices.left.erase(prices.left.upper_bound(upperBound), prices.left.end());
325
326 // At least one element should remain since upperBound is either
327 // equal to oldestTime or is less than latestTime, in which case
328 // the data is deleted between the oldestTime and upperBound.
329 if (prices.empty())
330 {
331 // LCOV_EXCL_START
333 return result;
334 // LCOV_EXCL_STOP
335 }
336 }
337 result[jss::time] = latestTime;
338
339 // calculate stats
340 auto const [avg, sd, size] = getStats(prices.right.begin(), prices.right.end());
341 result[jss::entire_set][jss::mean] = avg.getText();
342 result[jss::entire_set][jss::size] = size;
343 result[jss::entire_set][jss::standard_deviation] = to_string(sd);
344
345 auto itAdvance = [&](auto it, int distance) {
346 std::advance(it, distance);
347 return it;
348 };
349
350 auto const median = [&prices, &itAdvance, &size = size]() {
351 auto const middle = size / 2;
352 if ((size % 2) == 0)
353 {
354 static STAmount const kTwo{noIssue(), 2, 0};
355 auto it = itAdvance(prices.right.begin(), middle - 1);
356 auto const& a1 = it->first;
357 auto const& a2 = (++it)->first;
358 return divide(a1 + a2, kTwo, noIssue());
359 }
360 return itAdvance(prices.right.begin(), middle)->first;
361 }();
362 result[jss::median] = median.getText();
363
364 if (std::get<std::uint32_t>(trim) != 0)
365 {
366 auto const trimCount = prices.size() * std::get<std::uint32_t>(trim) / 100;
367
368 auto const [avg, sd, size] = getStats(
369 itAdvance(prices.right.begin(), trimCount), itAdvance(prices.right.end(), -trimCount));
370 result[jss::trimmed_set][jss::mean] = avg.getText();
371 result[jss::trimmed_set][jss::size] = size;
372 result[jss::trimmed_set][jss::standard_deviation] = to_string(sd);
373 }
374
375 return result;
376}
377
378} // namespace xrpl
T accumulate(T... args)
T advance(T... args)
T begin(T... args)
Lightweight wrapper to tag static string.
Definition json_value.h:48
Represents a JSON value.
Definition json_value.h:117
UInt asUInt() const
bool isMember(char const *key) const
Return true if the object has a member named key.
std::shared_ptr< Ledger const > getLedgerBySeq(std::uint32_t index)
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
Identifies fields.
Definition SField.h:132
std::string getText() const override
std::shared_ptr< STLedgerEntry const > const & const_ref
STCurrency const & getFieldCurrency(SField const &field) const
Definition STObject.cpp:695
std::uint32_t getFieldU32(SField const &field) const
Definition STObject.cpp:601
STArray const & getFieldArray(SField const &field) const
Definition STObject.cpp:688
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:464
uint256 getFieldH256(SField const &field) const
Definition STObject.cpp:631
T distance(T... args)
T emplace(T... args)
T find_if(T... args)
T get(T... args)
T holds_alternative(T... args)
T make_optional(T... args)
constexpr bool lexicalCastChecked(Out &out, In in)
Intelligently convert from one type to another.
Keylet oracle(AccountID const &account, std::uint32_t const documentID) noexcept
Definition Indexes.cpp:531
void injectError(ErrorCodeI code, json::Value &json)
Add or update the json update to reflect the error code.
json::Value missingFieldError(std::string const &name)
Definition ErrorCodes.h:243
Status lookupLedger(std::shared_ptr< ReadView const > &ledger, JsonContext const &context, json::Value &result)
Looks up a ledger from a request and fills a json::Value with ledger data.
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
@ RpcOracleMalformed
Definition ErrorCodes.h:132
@ RpcInternal
Definition ErrorCodes.h:113
@ RpcObjectNotFound
Definition ErrorCodes.h:126
@ RpcInvalidParams
Definition ErrorCodes.h:67
Dest safeDowncast(Src *s) noexcept
Definition safe_cast.h:84
STCurrency currencyFromJson(SField const &name, json::Value const &v)
std::optional< AccountID > parseBase58(std::string const &s)
Parse AccountID from checked, base58 string.
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:794
static void iteratePriceData(rpc::JsonContext &context, SLE::const_ref sle, std::function< bool(STObject const &)> const &f)
Calls callback "f" on the ledger-object sle and up to three previous metadata objects.
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
static std::tuple< STAmount, Number, std::uint16_t > getStats(Prices::right_const_iterator const &begin, Prices::right_const_iterator const &end)
constexpr std::size_t kMaxTrim
The maximum percentage of outliers to trim.
Definition Protocol.h:433
json::Value doGetAggregatePrice(rpc::JsonContext &)
oracles: array of {account, oracle_document_id} base_asset: is the asset to be priced quote_asset: is...
Number root2(Number f)
Issue const & noIssue()
Returns an asset specifier that represents no account and currency.
Definition Issue.h:118
bimap< multiset_of< std::uint32_t, std::greater<> >, multiset_of< STAmount > > Prices
BaseUInt< 256 > uint256
Definition base_uint.h:580
LedgerMaster & ledgerMaster
Definition Context.h:32
json::Value params
Definition Context.h:51