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