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/basics/safe_cast.h>
7#include <xrpl/json/json_value.h>
8#include <xrpl/ledger/ReadView.h>
9#include <xrpl/protocol/ErrorCodes.h>
10#include <xrpl/protocol/jss.h>
11
12#include <boost/bimap.hpp>
13#include <boost/bimap/multiset_of.hpp>
14
15namespace xrpl {
16
17using namespace boost::bimaps;
18// sorted descending by lastUpdateTime, ascending by AssetPrice
19using Prices =
20 bimap<multiset_of<std::uint32_t, std::greater<std::uint32_t>>, multiset_of<STAmount>>;
21
25static void
27 RPC::JsonContext& context,
29 std::function<bool(STObject const&)> 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 == nullptr) || f(*oracle) || isNew)
60 return;
61
62 if (++history > maxHistory)
63 return;
64
65 uint256 const prevTx = chain->getFieldH256(sfPreviousTxnID);
66 std::uint32_t const 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 ? &safe_downcast<STObject const&>(node.peekAtField(sfNewFields))
92 : &safe_downcast<STObject const&>(node.peekAtField(sfFinalFields));
93 break;
94 }
95 }
96}
97
98// Return avg, sd, data set size
100getStats(Prices::right_const_iterator const& begin, Prices::right_const_iterator const& end)
101{
102 STAmount avg{noIssue(), 0, 0};
103 Number sd{0};
104 std::uint16_t const size = std::distance(begin, end);
105 avg = std::accumulate(
106 begin, end, avg, [&](STAmount const& acc, auto const& it) { return acc + it.first; });
107 avg = divide(avg, STAmount{noIssue(), size, 0}, noIssue());
108 if (size > 1)
109 {
110 sd = std::accumulate(begin, end, sd, [&](Number const& acc, auto const& it) {
111 return acc + (it.first - avg) * (it.first - avg);
112 });
113 sd = root2(sd / (size - 1));
114 }
115 return {avg, sd, size};
116};
117
128{
129 Json::Value result;
130 auto const& params(context.params);
131
132 constexpr std::uint16_t maxOracles = 200;
133 if (!params.isMember(jss::oracles))
134 return RPC::missing_field_error(jss::oracles);
135 if (!params[jss::oracles].isArray() || params[jss::oracles].size() == 0 ||
136 params[jss::oracles].size() > maxOracles)
137 {
139 return result;
140 }
141
142 if (!params.isMember(jss::base_asset))
143 return RPC::missing_field_error(jss::base_asset);
144
145 if (!params.isMember(jss::quote_asset))
146 return RPC::missing_field_error(jss::quote_asset);
147
148 // Lambda to validate uint type
149 // support positive int, uint, and a number represented as a string
150 auto validUInt = [](Json::Value const& params, Json::StaticString const& field) {
151 auto const& jv = params[field];
152 std::uint32_t v = 0;
153 return jv.isUInt() || (jv.isInt() && jv.asInt() >= 0) ||
154 (jv.isString() && beast::lexicalCastChecked(v, jv.asString()));
155 };
156
157 // Lambda to get `trim` and `time_threshold` fields. If the field
158 // is not included in the input then a default value is returned.
159 auto getField = [&params, &validUInt](
160 Json::StaticString const& field,
161 unsigned int def = 0) -> std::variant<std::uint32_t, error_code_i> {
162 if (params.isMember(field))
163 {
164 if (!validUInt(params, field))
165 return rpcINVALID_PARAMS;
166 return params[field].asUInt();
167 }
168 return def;
169 };
170
171 // Lambda to get `base_asset` and `quote_asset`. The values have
172 // to conform to the Currency type.
173 auto getCurrency = [&params](SField const& sField, Json::StaticString const& field)
175 try
176 {
177 if (params[field].asString().empty())
178 return rpcINVALID_PARAMS;
179 currencyFromJson(sField, params[field]);
180 return params[field];
181 }
182 catch (...)
183 {
184 return rpcINVALID_PARAMS;
185 }
186 };
187
188 auto const trim = getField(jss::trim);
190 {
192 return result;
193 }
194 if (params.isMember(jss::trim) &&
196 {
198 return result;
199 }
200
201 auto const timeThreshold = getField(jss::time_threshold, 0);
202 if (std::holds_alternative<error_code_i>(timeThreshold))
203 {
204 RPC::inject_error(std::get<error_code_i>(timeThreshold), result);
205 return result;
206 }
207
208 auto const baseAsset = getCurrency(sfBaseAsset, jss::base_asset);
210 {
211 RPC::inject_error(std::get<error_code_i>(baseAsset), result);
212 return result;
213 }
214 auto const quoteAsset = getCurrency(sfQuoteAsset, jss::quote_asset);
216 {
217 RPC::inject_error(std::get<error_code_i>(quoteAsset), result);
218 return result;
219 }
220
221 // Get the ledger
223 result = RPC::lookupLedger(ledger, context);
224 if (!ledger)
225 return result; // LCOV_EXCL_LINE
226
227 // Collect the dataset into bimap keyed by lastUpdateTime and
228 // STAmount (Number is int64 and price is uint64)
229 Prices prices;
230 for (auto const& oracle : params[jss::oracles])
231 {
232 if (!oracle.isMember(jss::oracle_document_id) || !oracle.isMember(jss::account))
233 {
235 return result;
236 }
237 auto const documentID = validUInt(oracle, jss::oracle_document_id)
238 ? std::make_optional(oracle[jss::oracle_document_id].asUInt())
239 : std::nullopt;
240 auto const account = parseBase58<AccountID>(oracle[jss::account].asString());
241 if (!account || account->isZero() || !documentID)
242 {
244 return result;
245 }
246
247 auto const sle = ledger->read(keylet::oracle(*account, *documentID));
248 iteratePriceData(context, sle, [&](STObject const& node) {
249 auto const& series = node.getFieldArray(sfPriceDataSeries);
250 // find the token pair entry with the price
251 if (auto iter = std::find_if(
252 series.begin(),
253 series.end(),
254 [&](STObject const& o) -> bool {
255 return o.getFieldCurrency(sfBaseAsset).getText() ==
256 std::get<Json::Value>(baseAsset) &&
257 o.getFieldCurrency(sfQuoteAsset).getText() ==
258 std::get<Json::Value>(quoteAsset) &&
259 o.isFieldPresent(sfAssetPrice);
260 });
261 iter != series.end())
262 {
263 auto const price = iter->getFieldU64(sfAssetPrice);
264 auto const scale = iter->isFieldPresent(sfScale)
265 ? -static_cast<int>(iter->getFieldU8(sfScale))
266 : 0;
267 prices.insert(
268 Prices::value_type(
269 node.getFieldU32(sfLastUpdateTime), STAmount{noIssue(), price, scale}));
270 return true;
271 }
272 return false;
273 });
274 }
275
276 if (prices.empty())
277 {
279 return result;
280 }
281
282 // erase outdated data
283 // sorted in descending, therefore begin is the latest, end is the oldest
284 auto const latestTime = prices.left.begin()->first;
285 if (auto const threshold = std::get<std::uint32_t>(timeThreshold))
286 {
287 // threshold defines an acceptable range {max,min} of lastUpdateTime as
288 // {latestTime, latestTime - threshold}. Prices with lastUpdateTime
289 // less than (latestTime - threshold) are erased (outdated prices).
290 auto const oldestTime = prices.left.rbegin()->first;
291 auto const upperBound = latestTime > threshold ? (latestTime - threshold) : oldestTime;
292 if (upperBound > oldestTime)
293 prices.left.erase(prices.left.upper_bound(upperBound), prices.left.end());
294
295 // At least one element should remain since upperBound is either
296 // equal to oldestTime or is less than latestTime, in which case
297 // the data is deleted between the oldestTime and upperBound.
298 if (prices.empty())
299 {
300 // LCOV_EXCL_START
302 return result;
303 // LCOV_EXCL_STOP
304 }
305 }
306 result[jss::time] = latestTime;
307
308 // calculate stats
309 auto const [avg, sd, size] = getStats(prices.right.begin(), prices.right.end());
310 result[jss::entire_set][jss::mean] = avg.getText();
311 result[jss::entire_set][jss::size] = size;
312 result[jss::entire_set][jss::standard_deviation] = to_string(sd);
313
314 auto itAdvance = [&](auto it, int distance) {
315 std::advance(it, distance);
316 return it;
317 };
318
319 auto const median = [&prices, &itAdvance, &size_ = size]() {
320 auto const middle = size_ / 2;
321 if ((size_ % 2) == 0)
322 {
323 static STAmount const two{noIssue(), 2, 0};
324 auto it = itAdvance(prices.right.begin(), middle - 1);
325 auto const& a1 = it->first;
326 auto const& a2 = (++it)->first;
327 return divide(a1 + a2, two, noIssue());
328 }
329 return itAdvance(prices.right.begin(), middle)->first;
330 }();
331 result[jss::median] = median.getText();
332
333 if (std::get<std::uint32_t>(trim) != 0)
334 {
335 auto const trimCount = prices.size() * std::get<std::uint32_t>(trim) / 100;
336
337 auto const [avg, sd, size] = getStats(
338 itAdvance(prices.right.begin(), trimCount), itAdvance(prices.right.end(), -trimCount));
339 result[jss::trimmed_set][jss::mean] = avg.getText();
340 result[jss::trimmed_set][jss::size] = size;
341 result[jss::trimmed_set][jss::standard_deviation] = to_string(sd);
342 }
343
344 return result;
345}
346
347} // 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
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)
Number is a floating point type that can represent a wide range of values.
Definition Number.h:207
Identifies fields.
Definition SField.h:126
std::uint32_t getFieldU32(SField const &field) const
Definition STObject.cpp:593
STArray const & getFieldArray(SField const &field) const
Definition STObject.cpp:680
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:456
uint256 getFieldH256(SField const &field) const
Definition STObject.cpp:623
bool isZero() const
Definition base_uint.h:513
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.
Json::Value missing_field_error(std::string const &name)
Definition ErrorCodes.h:231
void inject_error(error_code_i code, Json::Value &json)
Add or update the json update to reflect the error code.
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.
Keylet oracle(AccountID const &account, std::uint32_t const &documentID) noexcept
Definition Indexes.cpp:468
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
STCurrency currencyFromJson(SField const &name, Json::Value const &v)
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:602
static void iteratePriceData(RPC::JsonContext &context, std::shared_ptr< SLE const > const &sle, std::function< bool(STObject const &)> const &f)
Calls callback "f" on the ledger-object sle and up to three previous metadata objects.
std::size_t constexpr maxTrim
The maximum percentage of outliers to trim.
Definition Protocol.h:301
static std::tuple< STAmount, Number, std::uint16_t > getStats(Prices::right_const_iterator const &begin, Prices::right_const_iterator const &end)
bimap< multiset_of< std::uint32_t, std::greater< std::uint32_t > >, multiset_of< STAmount > > Prices
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)
Definition Number.cpp:1030
Issue const & noIssue()
Returns an asset specifier that represents no account and currency.
Definition Issue.h:105
@ rpcOBJECT_NOT_FOUND
Definition ErrorCodes.h:123
@ rpcORACLE_MALFORMED
Definition ErrorCodes.h:129
@ rpcINTERNAL
Definition ErrorCodes.h:110
@ rpcINVALID_PARAMS
Definition ErrorCodes.h:64
LedgerMaster & ledgerMaster
Definition Context.h:24
Json::Value params
Definition Context.h:43