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