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