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/ledger/ReadView.h>
23#include <xrpld/rpc/Context.h>
24#include <xrpld/rpc/detail/RPCHelpers.h>
25#include <xrpl/json/json_value.h>
26#include <xrpl/protocol/ErrorCodes.h>
27#include <xrpl/protocol/jss.h>
28
29#include <boost/bimap.hpp>
30#include <boost/bimap/multiset_of.hpp>
31
32namespace ripple {
33
34using namespace boost::bimaps;
35// sorted descending by lastUpdateTime, ascending by AssetPrice
36using Prices = bimap<
37 multiset_of<std::uint32_t, std::greater<std::uint32_t>>,
38 multiset_of<STAmount>>;
39
43static void
45 RPC::JsonContext& context,
47 std::function<bool(STObject const&)>&& f)
48{
50 constexpr std::uint8_t maxHistory = 3;
51 bool isNew = false;
52 std::uint8_t history = 0;
53
54 // `oracle` points to an object that has an `sfPriceDataSeries` field.
55 // When this function is called, that is a `PriceOracle` ledger object,
56 // but after one iteration of the loop below, it is an `sfNewFields`
57 // / `sfFinalFields` object in a `CreatedNode` / `ModifiedNode` object in
58 // a transaction's metadata.
59
60 // `chain` points to an object that has `sfPreviousTxnID` and
61 // `sfPreviousTxnLgrSeq` fields. When this function is called,
62 // that is the `PriceOracle` ledger object pointed to by `oracle`,
63 // but after one iteration of the loop below, then it is a `ModifiedNode`
64 // / `CreatedNode` object in a transaction's metadata.
65 STObject const* oracle = sle.get();
66 STObject const* chain = oracle;
67 // Use to test an unlikely scenario when CreatedNode / ModifiedNode
68 // for the Oracle is not found in the inner loop
69 STObject const* prevChain = nullptr;
70
71 Meta meta = nullptr;
72 while (true)
73 {
74 if (prevChain == chain)
75 return;
76
77 if (!oracle || f(*oracle) || isNew)
78 return;
79
80 if (++history > maxHistory)
81 return;
82
83 uint256 prevTx = chain->getFieldH256(sfPreviousTxnID);
84 std::uint32_t prevSeq = chain->getFieldU32(sfPreviousTxnLgrSeq);
85
86 auto const ledger = context.ledgerMaster.getLedgerBySeq(prevSeq);
87 if (!ledger)
88 return; // LCOV_EXCL_LINE
89
90 meta = ledger->txRead(prevTx).second;
91
92 prevChain = chain;
93 for (STObject const& node : meta->getFieldArray(sfAffectedNodes))
94 {
95 if (node.getFieldU16(sfLedgerEntryType) != ltORACLE)
96 {
97 continue;
98 }
99
100 chain = &node;
101 isNew = node.isFieldPresent(sfNewFields);
102 // if a meta is for the new and this is the first
103 // look-up then it's the meta for the tx that
104 // created the current object; i.e. there is no
105 // historical data
106 if (isNew && history == 1)
107 return;
108
109 oracle = isNew
110 ? &static_cast<const STObject&>(node.peekAtField(sfNewFields))
111 : &static_cast<const STObject&>(
112 node.peekAtField(sfFinalFields));
113 break;
114 }
115 }
116}
117
118// Return avg, sd, data set size
121 Prices::right_const_iterator const& begin,
122 Prices::right_const_iterator const& end)
123{
124 STAmount avg{noIssue(), 0, 0};
125 Number sd{0};
126 std::uint16_t const size = std::distance(begin, end);
127 avg = std::accumulate(
128 begin, end, avg, [&](STAmount const& acc, auto const& it) {
129 return acc + it.first;
130 });
131 avg = divide(avg, STAmount{noIssue(), size, 0}, noIssue());
132 if (size > 1)
133 {
134 sd = std::accumulate(
135 begin, end, sd, [&](Number const& acc, auto const& it) {
136 return acc + (it.first - avg) * (it.first - avg);
137 });
138 sd = root2(sd / (size - 1));
139 }
140 return {avg, sd, size};
141};
142
153{
154 Json::Value result;
155 auto const& params(context.params);
156
157 constexpr std::uint16_t maxOracles = 200;
158 if (!params.isMember(jss::oracles))
159 return RPC::missing_field_error(jss::oracles);
160 if (!params[jss::oracles].isArray() || params[jss::oracles].size() == 0 ||
161 params[jss::oracles].size() > maxOracles)
162 {
164 return result;
165 }
166
167 if (!params.isMember(jss::base_asset))
168 return RPC::missing_field_error(jss::base_asset);
169
170 if (!params.isMember(jss::quote_asset))
171 return RPC::missing_field_error(jss::quote_asset);
172
173 // Lambda to validate uint type
174 // support positive int, uint, and a number represented as a string
175 auto validUInt = [](Json::Value const& params,
176 Json::StaticString const& field) {
177 auto const& jv = params[field];
179 return jv.isUInt() || (jv.isInt() && jv.asInt() >= 0) ||
180 (jv.isString() && beast::lexicalCastChecked(v, jv.asString()));
181 };
182
183 // Lambda to get `trim` and `time_threshold` fields. If the field
184 // is not included in the input then a default value is returned.
185 auto getField = [&params, &validUInt](
186 Json::StaticString const& field,
187 unsigned int def =
189 if (params.isMember(field))
190 {
191 if (!validUInt(params, field))
192 return rpcINVALID_PARAMS;
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 =
201 [&params](SField const& sField, Json::StaticString const& field)
203 try
204 {
205 if (params[field].asString().empty())
206 return rpcINVALID_PARAMS;
207 currencyFromJson(sField, params[field]);
208 return params[field];
209 }
210 catch (...)
211 {
212 return rpcINVALID_PARAMS;
213 }
214 };
215
216 auto const trim = getField(jss::trim);
217 if (std::holds_alternative<error_code_i>(trim))
218 {
219 RPC::inject_error(std::get<error_code_i>(trim), result);
220 return result;
221 }
222 if (params.isMember(jss::trim) &&
223 (std::get<std::uint32_t>(trim) == 0 ||
224 std::get<std::uint32_t>(trim) > maxTrim))
225 {
227 return result;
228 }
229
230 auto const timeThreshold = getField(jss::time_threshold, 0);
231 if (std::holds_alternative<error_code_i>(timeThreshold))
232 {
233 RPC::inject_error(std::get<error_code_i>(timeThreshold), result);
234 return result;
235 }
236
237 auto const baseAsset = getCurrency(sfBaseAsset, jss::base_asset);
238 if (std::holds_alternative<error_code_i>(baseAsset))
239 {
240 RPC::inject_error(std::get<error_code_i>(baseAsset), result);
241 return result;
242 }
243 auto const quoteAsset = getCurrency(sfQuoteAsset, jss::quote_asset);
244 if (std::holds_alternative<error_code_i>(quoteAsset))
245 {
246 RPC::inject_error(std::get<error_code_i>(quoteAsset), result);
247 return result;
248 }
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) ||
256 !oracle.isMember(jss::account))
257 {
259 return result;
260 }
261 auto const documentID = validUInt(oracle, jss::oracle_document_id)
262 ? std::make_optional(oracle[jss::oracle_document_id].asUInt())
263 : std::nullopt;
264 auto const account =
265 parseBase58<AccountID>(oracle[jss::account].asString());
266 if (!account || account->isZero() || !documentID)
267 {
269 return result;
270 }
271
273 result = RPC::lookupLedger(ledger, context);
274 if (!ledger)
275 return result; // LCOV_EXCL_LINE
276
277 auto const sle = ledger->read(keylet::oracle(*account, *documentID));
278 iteratePriceData(context, sle, [&](STObject const& node) {
279 auto const& series = node.getFieldArray(sfPriceDataSeries);
280 // find the token pair entry with the price
281 if (auto iter = std::find_if(
282 series.begin(),
283 series.end(),
284 [&](STObject const& o) -> bool {
285 return o.getFieldCurrency(sfBaseAsset).getText() ==
286 std::get<Json::Value>(baseAsset) &&
287 o.getFieldCurrency(sfQuoteAsset).getText() ==
288 std::get<Json::Value>(quoteAsset) &&
289 o.isFieldPresent(sfAssetPrice);
290 });
291 iter != series.end())
292 {
293 auto const price = iter->getFieldU64(sfAssetPrice);
294 auto const scale = iter->isFieldPresent(sfScale)
295 ? -static_cast<int>(iter->getFieldU8(sfScale))
296 : 0;
297 prices.insert(Prices::value_type(
298 node.getFieldU32(sfLastUpdateTime),
299 STAmount{noIssue(), price, scale}));
300 return true;
301 }
302 return false;
303 });
304 }
305
306 if (prices.empty())
307 {
309 return result;
310 }
311
312 // erase outdated data
313 // sorted in descending, therefore begin is the latest, end is the oldest
314 auto const latestTime = prices.left.begin()->first;
315 if (auto const threshold = std::get<std::uint32_t>(timeThreshold))
316 {
317 // threshold defines an acceptable range {max,min} of lastUpdateTime as
318 // {latestTime, latestTime - threshold}, the prices with lastUpdateTime
319 // greater than (latestTime - threshold) are erased.
320 auto const oldestTime = prices.left.rbegin()->first;
321 auto const upperBound =
322 latestTime > threshold ? (latestTime - threshold) : oldestTime;
323 if (upperBound > oldestTime)
324 prices.left.erase(
325 prices.left.upper_bound(upperBound), prices.left.end());
326
327 // At least one element should remain since upperBound is either
328 // equal to oldestTime or is less than latestTime, in which case
329 // the data is deleted between the oldestTime and upperBound.
330 if (prices.empty())
331 {
332 // LCOV_EXCL_START
334 return result;
335 // LCOV_EXCL_STOP
336 }
337 }
338 result[jss::time] = latestTime;
339
340 // calculate stats
341 auto const [avg, sd, size] =
342 getStats(prices.right.begin(), prices.right.end());
343 result[jss::entire_set][jss::mean] = avg.getText();
344 result[jss::entire_set][jss::size] = size;
345 result[jss::entire_set][jss::standard_deviation] = to_string(sd);
346
347 auto itAdvance = [&](auto it, int distance) {
348 std::advance(it, distance);
349 return it;
350 };
351
352 auto const median = [&prices, &itAdvance, &size_ = size]() {
353 auto const middle = size_ / 2;
354 if ((size_ % 2) == 0)
355 {
356 static STAmount two{noIssue(), 2, 0};
357 auto it = itAdvance(prices.right.begin(), middle - 1);
358 auto const& a1 = it->first;
359 auto const& a2 = (++it)->first;
360 return divide(a1 + a2, two, noIssue());
361 }
362 return itAdvance(prices.right.begin(), middle)->first;
363 }();
364 result[jss::median] = median.getText();
365
366 if (std::get<std::uint32_t>(trim) != 0)
367 {
368 auto const trimCount =
369 prices.size() * std::get<std::uint32_t>(trim) / 100;
370
371 auto const [avg, sd, size] = getStats(
372 itAdvance(prices.right.begin(), trimCount),
373 itAdvance(prices.right.end(), -trimCount));
374 result[jss::trimmed_set][jss::mean] = avg.getText();
375 result[jss::trimmed_set][jss::size] = size;
376 result[jss::trimmed_set][jss::standard_deviation] = to_string(sd);
377 }
378
379 return result;
380}
381
382} // namespace ripple
T accumulate(T... args)
T advance(T... args)
Lightweight wrapper to tag static string.
Definition: json_value.h:61
Represents a JSON value.
Definition: json_value.h:147
UInt asUInt() const
Definition: json_value.cpp:545
bool isMember(const char *key) const
Return true if the object has a member named key.
Definition: json_value.cpp:943
std::shared_ptr< Ledger const > getLedgerBySeq(std::uint32_t index)
Identifies fields.
Definition: SField.h:144
const STArray & getFieldArray(SField const &field) const
Definition: STObject.cpp:656
std::uint32_t getFieldU32(SField const &field) const
Definition: STObject.cpp:585
bool isFieldPresent(SField const &field) const
Definition: STObject.cpp:454
uint256 getFieldH256(SField const &field) const
Definition: STObject.cpp:615
bool isZero() const
Definition: base_uint.h:539
T distance(T... args)
T find_if(T... args)
T get(T... args)
T make_optional(T... args)
bool lexicalCastChecked(Out &out, In in)
Intelligently convert from one type to another.
Definition: LexicalCast.h:201
void inject_error(error_code_i code, JsonValue &json)
Add or update the json update to reflect the error code.
Definition: ErrorCodes.h:223
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.
Definition: RPCHelpers.cpp:623
Json::Value missing_field_error(std::string const &name)
Definition: ErrorCodes.h:273
Keylet oracle(AccountID const &account, std::uint32_t const &documentID) noexcept
Definition: Indexes.cpp:488
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: algorithm.h:26
STAmount divide(STAmount const &amount, Rate const &rate)
Definition: Rate2.cpp:87
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)
Definition: STCurrency.cpp:97
@ 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:155
Issue const & noIssue()
Returns an asset specifier that represents no account and currency.
Definition: Issue.h:126
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:629
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:695
T size(T... args)
LedgerMaster & ledgerMaster
Definition: Context.h:45
Json::Value params
Definition: Context.h:64