rippled
Loading...
Searching...
No Matches
GetAggregatePrice.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2023 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or 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
20#include <xrpld/app/ledger/LedgerMaster.h>
21#include <xrpld/app/main/Application.h>
22#include <xrpld/rpc/Context.h>
23#include <xrpld/rpc/detail/RPCHelpers.h>
24
25#include <xrpl/json/json_value.h>
26#include <xrpl/ledger/ReadView.h>
27#include <xrpl/protocol/ErrorCodes.h>
28#include <xrpl/protocol/jss.h>
29
30#include <boost/bimap.hpp>
31#include <boost/bimap/multiset_of.hpp>
32
33namespace ripple {
34
35using namespace boost::bimaps;
36// sorted descending by lastUpdateTime, ascending by AssetPrice
37using Prices = bimap<
38 multiset_of<std::uint32_t, std::greater<std::uint32_t>>,
39 multiset_of<STAmount>>;
40
44static void
46 RPC::JsonContext& context,
48 std::function<bool(STObject const&)>&& f)
49{
51 constexpr std::uint8_t maxHistory = 3;
52 bool isNew = false;
53 std::uint8_t history = 0;
54
55 // `oracle` points to an object that has an `sfPriceDataSeries` field.
56 // When this function is called, that is a `PriceOracle` ledger object,
57 // but after one iteration of the loop below, it is an `sfNewFields`
58 // / `sfFinalFields` object in a `CreatedNode` / `ModifiedNode` object in
59 // a transaction's metadata.
60
61 // `chain` points to an object that has `sfPreviousTxnID` and
62 // `sfPreviousTxnLgrSeq` fields. When this function is called,
63 // that is the `PriceOracle` ledger object pointed to by `oracle`,
64 // but after one iteration of the loop below, then it is a `ModifiedNode`
65 // / `CreatedNode` object in a transaction's metadata.
66 STObject const* oracle = sle.get();
67 STObject const* chain = oracle;
68 // Use to test an unlikely scenario when CreatedNode / ModifiedNode
69 // for the Oracle is not found in the inner loop
70 STObject const* prevChain = nullptr;
71
72 Meta meta = nullptr;
73 while (true)
74 {
75 if (prevChain == chain)
76 return;
77
78 if (!oracle || f(*oracle) || isNew)
79 return;
80
81 if (++history > maxHistory)
82 return;
83
84 uint256 prevTx = chain->getFieldH256(sfPreviousTxnID);
85 std::uint32_t prevSeq = chain->getFieldU32(sfPreviousTxnLgrSeq);
86
87 auto const ledger = context.ledgerMaster.getLedgerBySeq(prevSeq);
88 if (!ledger)
89 return; // LCOV_EXCL_LINE
90
91 meta = ledger->txRead(prevTx).second;
92
93 prevChain = chain;
94 for (STObject const& node : meta->getFieldArray(sfAffectedNodes))
95 {
96 if (node.getFieldU16(sfLedgerEntryType) != ltORACLE)
97 {
98 continue;
99 }
100
101 chain = &node;
102 isNew = node.isFieldPresent(sfNewFields);
103 // if a meta is for the new and this is the first
104 // look-up then it's the meta for the tx that
105 // created the current object; i.e. there is no
106 // historical data
107 if (isNew && history == 1)
108 return;
109
110 oracle = isNew
111 ? &static_cast<STObject const&>(node.peekAtField(sfNewFields))
112 : &static_cast<STObject const&>(
113 node.peekAtField(sfFinalFields));
114 break;
115 }
116 }
117}
118
119// Return avg, sd, data set size
122 Prices::right_const_iterator const& begin,
123 Prices::right_const_iterator const& end)
124{
125 STAmount avg{noIssue(), 0, 0};
126 Number sd{0};
127 std::uint16_t const size = std::distance(begin, end);
128 avg = std::accumulate(
129 begin, end, avg, [&](STAmount const& acc, auto const& it) {
130 return acc + it.first;
131 });
132 avg = divide(avg, STAmount{noIssue(), size, 0}, noIssue());
133 if (size > 1)
134 {
135 sd = std::accumulate(
136 begin, end, sd, [&](Number const& acc, auto const& it) {
137 return acc + (it.first - avg) * (it.first - avg);
138 });
139 sd = root2(sd / (size - 1));
140 }
141 return {avg, sd, size};
142};
143
154{
155 Json::Value result;
156 auto const& params(context.params);
157
158 constexpr std::uint16_t maxOracles = 200;
159 if (!params.isMember(jss::oracles))
160 return RPC::missing_field_error(jss::oracles);
161 if (!params[jss::oracles].isArray() || params[jss::oracles].size() == 0 ||
162 params[jss::oracles].size() > maxOracles)
163 {
165 return result;
166 }
167
168 if (!params.isMember(jss::base_asset))
169 return RPC::missing_field_error(jss::base_asset);
170
171 if (!params.isMember(jss::quote_asset))
172 return RPC::missing_field_error(jss::quote_asset);
173
174 // Lambda to validate uint type
175 // support positive int, uint, and a number represented as a string
176 auto validUInt = [](Json::Value const& params,
177 Json::StaticString const& field) {
178 auto const& jv = params[field];
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 =
190 if (params.isMember(field))
191 {
192 if (!validUInt(params, field))
193 return rpcINVALID_PARAMS;
194 return params[field].asUInt();
195 }
196 return def;
197 };
198
199 // Lambda to get `base_asset` and `quote_asset`. The values have
200 // to conform to the Currency type.
201 auto getCurrency =
202 [&params](SField const& sField, Json::StaticString const& field)
204 try
205 {
206 if (params[field].asString().empty())
207 return rpcINVALID_PARAMS;
208 currencyFromJson(sField, params[field]);
209 return params[field];
210 }
211 catch (...)
212 {
213 return rpcINVALID_PARAMS;
214 }
215 };
216
217 auto const trim = getField(jss::trim);
219 {
221 return result;
222 }
223 if (params.isMember(jss::trim) &&
224 (std::get<std::uint32_t>(trim) == 0 ||
226 {
228 return result;
229 }
230
231 auto const timeThreshold = getField(jss::time_threshold, 0);
232 if (std::holds_alternative<error_code_i>(timeThreshold))
233 {
234 RPC::inject_error(std::get<error_code_i>(timeThreshold), result);
235 return result;
236 }
237
238 auto const baseAsset = getCurrency(sfBaseAsset, jss::base_asset);
240 {
241 RPC::inject_error(std::get<error_code_i>(baseAsset), result);
242 return result;
243 }
244 auto const quoteAsset = getCurrency(sfQuoteAsset, jss::quote_asset);
246 {
247 RPC::inject_error(std::get<error_code_i>(quoteAsset), result);
248 return result;
249 }
250
251 // Collect the dataset into bimap keyed by lastUpdateTime and
252 // STAmount (Number is int64 and price is uint64)
253 Prices prices;
254 for (auto const& oracle : params[jss::oracles])
255 {
256 if (!oracle.isMember(jss::oracle_document_id) ||
257 !oracle.isMember(jss::account))
258 {
260 return result;
261 }
262 auto const documentID = validUInt(oracle, jss::oracle_document_id)
263 ? std::make_optional(oracle[jss::oracle_document_id].asUInt())
264 : std::nullopt;
265 auto const account =
266 parseBase58<AccountID>(oracle[jss::account].asString());
267 if (!account || account->isZero() || !documentID)
268 {
270 return result;
271 }
272
274 result = RPC::lookupLedger(ledger, context);
275 if (!ledger)
276 return result; // LCOV_EXCL_LINE
277
278 auto const sle = ledger->read(keylet::oracle(*account, *documentID));
279 iteratePriceData(context, sle, [&](STObject const& node) {
280 auto const& series = node.getFieldArray(sfPriceDataSeries);
281 // find the token pair entry with the price
282 if (auto iter = std::find_if(
283 series.begin(),
284 series.end(),
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(Prices::value_type(
299 node.getFieldU32(sfLastUpdateTime),
300 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}, the prices with lastUpdateTime
320 // greater than (latestTime - threshold) are erased.
321 auto const oldestTime = prices.left.rbegin()->first;
322 auto const upperBound =
323 latestTime > threshold ? (latestTime - threshold) : oldestTime;
324 if (upperBound > oldestTime)
325 prices.left.erase(
326 prices.left.upper_bound(upperBound), prices.left.end());
327
328 // At least one element should remain since upperBound is either
329 // equal to oldestTime or is less than latestTime, in which case
330 // the data is deleted between the oldestTime and upperBound.
331 if (prices.empty())
332 {
333 // LCOV_EXCL_START
335 return result;
336 // LCOV_EXCL_STOP
337 }
338 }
339 result[jss::time] = latestTime;
340
341 // calculate stats
342 auto const [avg, sd, size] =
343 getStats(prices.right.begin(), prices.right.end());
344 result[jss::entire_set][jss::mean] = avg.getText();
345 result[jss::entire_set][jss::size] = size;
346 result[jss::entire_set][jss::standard_deviation] = to_string(sd);
347
348 auto itAdvance = [&](auto it, int distance) {
349 std::advance(it, distance);
350 return it;
351 };
352
353 auto const median = [&prices, &itAdvance, &size_ = size]() {
354 auto const middle = size_ / 2;
355 if ((size_ % 2) == 0)
356 {
357 static STAmount two{noIssue(), 2, 0};
358 auto it = itAdvance(prices.right.begin(), middle - 1);
359 auto const& a1 = it->first;
360 auto const& a2 = (++it)->first;
361 return divide(a1 + a2, two, noIssue());
362 }
363 return itAdvance(prices.right.begin(), middle)->first;
364 }();
365 result[jss::median] = median.getText();
366
367 if (std::get<std::uint32_t>(trim) != 0)
368 {
369 auto const trimCount =
370 prices.size() * std::get<std::uint32_t>(trim) / 100;
371
372 auto const [avg, sd, size] = getStats(
373 itAdvance(prices.right.begin(), trimCount),
374 itAdvance(prices.right.end(), -trimCount));
375 result[jss::trimmed_set][jss::mean] = avg.getText();
376 result[jss::trimmed_set][jss::size] = size;
377 result[jss::trimmed_set][jss::standard_deviation] = to_string(sd);
378 }
379
380 return result;
381}
382
383} // namespace ripple
T accumulate(T... args)
T advance(T... args)
Lightweight wrapper to tag static string.
Definition json_value.h:63
Represents a JSON value.
Definition json_value.h:149
const_iterator begin() const
UInt size() const
Number of values in array or object.
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)
Identifies fields.
Definition SField.h:146
STArray const & getFieldArray(SField const &field) const
Definition STObject.cpp:702
std::uint32_t getFieldU32(SField const &field) const
Definition STObject.cpp:615
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:484
uint256 getFieldH256(SField const &field) const
Definition STObject.cpp:645
bool isZero() const
Definition base_uint.h:540
T distance(T... args)
T find_if(T... args)
T get(T... args)
T is_same_v
T make_optional(T... args)
bool lexicalCastChecked(Out &out, In in)
Intelligently convert from one type to another.
void inject_error(error_code_i code, JsonValue &json)
Add or update the json update to reflect the error code.
Definition ErrorCodes.h:233
Status lookupLedger(std::shared_ptr< ReadView const > &ledger, JsonContext &context, Json::Value &result)
Look up a ledger from a request and fill a Json::Result with the data representing a ledger.
Json::Value missing_field_error(std::string const &name)
Definition ErrorCodes.h:283
Keylet oracle(AccountID const &account, std::uint32_t const &documentID) noexcept
Definition Indexes.cpp:520
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:25
STAmount divide(STAmount const &amount, Rate const &rate)
Definition Rate2.cpp:93
Json::Value doGetAggregatePrice(RPC::JsonContext &context)
oracles: array of {account, oracle_document_id} base_asset: is the asset to be priced quote_asset: is...
STCurrency currencyFromJson(SField const &name, Json::Value const &v)
@ rpcORACLE_MALFORMED
Definition ErrorCodes.h:149
@ rpcOBJECT_NOT_FOUND
Definition ErrorCodes.h:143
@ rpcINVALID_PARAMS
Definition ErrorCodes.h:84
@ rpcINTERNAL
Definition ErrorCodes.h:130
std::size_t constexpr maxTrim
The maximum percentage of outliers to trim.
Definition Protocol.h:175
Issue const & noIssue()
Returns an asset specifier that represents no account and currency.
Definition Issue.h:123
bimap< multiset_of< std::uint32_t, std::greater< std::uint32_t > >, multiset_of< STAmount > > Prices
static void iteratePriceData(RPC::JsonContext &context, std::shared_ptr< SLE const > const &sle, std::function< bool(STObject const &)> &&f)
Calls callback "f" on the ledger-object sle and up to three previous metadata objects.
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:630
static std::tuple< STAmount, Number, std::uint16_t > getStats(Prices::right_const_iterator const &begin, Prices::right_const_iterator const &end)
Number root2(Number f)
Definition Number.cpp:701
LedgerMaster & ledgerMaster
Definition Context.h:44