mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Implement native support for Price Oracles. A Price Oracle is used to bring real-world data, such as market prices, onto the blockchain, enabling dApps to access and utilize information that resides outside the blockchain. Add Price Oracle functionality: - OracleSet: create or update the Oracle object - OracleDelete: delete the Oracle object To support this functionality add: - New RPC method, `get_aggregate_price`, to calculate aggregate price for a token pair of the specified oracles - `ltOracle` object The `ltOracle` object maintains: - Oracle Owner's account - Oracle's metadata - Up to ten token pairs with the scaled price - The last update time the token pairs were updated Add Oracle unit-tests
This commit is contained in:
committed by
GitHub
parent
d7d15a922a
commit
e718378bdb
260
src/test/rpc/GetAggregatePrice_test.cpp
Normal file
260
src/test/rpc/GetAggregatePrice_test.cpp
Normal file
@@ -0,0 +1,260 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/app/ledger/LedgerMaster.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/Oracle.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
namespace oracle {
|
||||
|
||||
class GetAggregatePrice_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
void
|
||||
testErrors()
|
||||
{
|
||||
testcase("Errors");
|
||||
using namespace jtx;
|
||||
Account const owner{"owner"};
|
||||
Account const some{"some"};
|
||||
static std::vector<std::pair<Account, std::uint32_t>> oracles = {
|
||||
{owner, 1}};
|
||||
|
||||
{
|
||||
Env env(*this);
|
||||
// missing base_asset
|
||||
auto ret =
|
||||
Oracle::aggregatePrice(env, std::nullopt, "USD", oracles);
|
||||
BEAST_EXPECT(
|
||||
ret[jss::error_message].asString() ==
|
||||
"Missing field 'base_asset'.");
|
||||
|
||||
// missing quote_asset
|
||||
ret = Oracle::aggregatePrice(env, "XRP", std::nullopt, oracles);
|
||||
BEAST_EXPECT(
|
||||
ret[jss::error_message].asString() ==
|
||||
"Missing field 'quote_asset'.");
|
||||
|
||||
// missing oracles array
|
||||
ret = Oracle::aggregatePrice(env, "XRP", "USD");
|
||||
BEAST_EXPECT(
|
||||
ret[jss::error_message].asString() ==
|
||||
"Missing field 'oracles'.");
|
||||
|
||||
// empty oracles array
|
||||
ret = Oracle::aggregatePrice(env, "XRP", "USD", {{}});
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed");
|
||||
|
||||
// invalid oracle document id
|
||||
ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{owner, 2}}});
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
|
||||
|
||||
// invalid owner
|
||||
ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{some, 1}}});
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
|
||||
|
||||
// oracles have wrong asset pair
|
||||
env.fund(XRP(1'000), owner);
|
||||
Oracle oracle(
|
||||
env, {.owner = owner, .series = {{"XRP", "EUR", 740, 1}}});
|
||||
ret = Oracle::aggregatePrice(
|
||||
env, "XRP", "USD", {{{owner, oracle.documentID()}}});
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
|
||||
|
||||
// invalid trim value
|
||||
ret = Oracle::aggregatePrice(
|
||||
env, "XRP", "USD", {{{owner, oracle.documentID()}}}, 0);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
|
||||
ret = Oracle::aggregatePrice(
|
||||
env, "XRP", "USD", {{{owner, oracle.documentID()}}}, 26);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
|
||||
}
|
||||
|
||||
// too many oracles
|
||||
{
|
||||
Env env(*this);
|
||||
std::vector<std::pair<Account, std::uint32_t>> oracles;
|
||||
for (int i = 0; i < 201; ++i)
|
||||
{
|
||||
Account const owner(std::to_string(i));
|
||||
env.fund(XRP(1'000), owner);
|
||||
Oracle oracle(env, {.owner = owner, .documentID = i});
|
||||
oracles.emplace_back(owner, oracle.documentID());
|
||||
}
|
||||
auto const ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testRpc()
|
||||
{
|
||||
testcase("RPC");
|
||||
using namespace jtx;
|
||||
|
||||
auto prep = [&](Env& env, auto& oracles) {
|
||||
oracles.reserve(10);
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
Account const owner{std::to_string(i)};
|
||||
env.fund(XRP(1'000), owner);
|
||||
Oracle oracle(
|
||||
env,
|
||||
{.owner = owner,
|
||||
.documentID = rand(),
|
||||
.series = {
|
||||
{"XRP", "USD", 740 + i, 1}, {"XRP", "EUR", 740, 1}}});
|
||||
oracles.emplace_back(owner, oracle.documentID());
|
||||
}
|
||||
};
|
||||
|
||||
// Aggregate data set includes all price oracle instances, no trimming
|
||||
// or time threshold
|
||||
{
|
||||
Env env(*this);
|
||||
std::vector<std::pair<Account, std::uint32_t>> oracles;
|
||||
prep(env, oracles);
|
||||
// entire and trimmed stats
|
||||
auto ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles);
|
||||
BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.45");
|
||||
BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 10);
|
||||
BEAST_EXPECT(
|
||||
ret[jss::entire_set][jss::standard_deviation] ==
|
||||
"0.3027650354097492");
|
||||
BEAST_EXPECT(ret[jss::median] == "74.45");
|
||||
BEAST_EXPECT(ret[jss::time] == 946694900);
|
||||
}
|
||||
|
||||
// Aggregate data set includes all price oracle instances
|
||||
{
|
||||
Env env(*this);
|
||||
std::vector<std::pair<Account, std::uint32_t>> oracles;
|
||||
prep(env, oracles);
|
||||
// entire and trimmed stats
|
||||
auto ret =
|
||||
Oracle::aggregatePrice(env, "XRP", "USD", oracles, 20, 100);
|
||||
BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.45");
|
||||
BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 10);
|
||||
BEAST_EXPECT(
|
||||
ret[jss::entire_set][jss::standard_deviation] ==
|
||||
"0.3027650354097492");
|
||||
BEAST_EXPECT(ret[jss::median] == "74.45");
|
||||
BEAST_EXPECT(ret[jss::trimmed_set][jss::mean] == "74.45");
|
||||
BEAST_EXPECT(ret[jss::trimmed_set][jss::size].asUInt() == 6);
|
||||
BEAST_EXPECT(
|
||||
ret[jss::trimmed_set][jss::standard_deviation] ==
|
||||
"0.187082869338697");
|
||||
BEAST_EXPECT(ret[jss::time] == 946694900);
|
||||
}
|
||||
|
||||
// A reduced dataset, as some price oracles have data beyond three
|
||||
// updated ledgers
|
||||
{
|
||||
Env env(*this);
|
||||
std::vector<std::pair<Account, std::uint32_t>> oracles;
|
||||
prep(env, oracles);
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
Oracle oracle(
|
||||
env,
|
||||
{.owner = oracles[i].first,
|
||||
.documentID = oracles[i].second},
|
||||
false);
|
||||
// push XRP/USD by more than three ledgers, so this price
|
||||
// oracle is not included in the dataset
|
||||
oracle.set(UpdateArg{.series = {{"XRP", "EUR", 740, 1}}});
|
||||
oracle.set(UpdateArg{.series = {{"XRP", "EUR", 740, 1}}});
|
||||
oracle.set(UpdateArg{.series = {{"XRP", "EUR", 740, 1}}});
|
||||
}
|
||||
for (int i = 3; i < 6; ++i)
|
||||
{
|
||||
Oracle oracle(
|
||||
env,
|
||||
{.owner = oracles[i].first,
|
||||
.documentID = oracles[i].second},
|
||||
false);
|
||||
// push XRP/USD by two ledgers, so this price
|
||||
// is included in the dataset
|
||||
oracle.set(UpdateArg{.series = {{"XRP", "EUR", 740, 1}}});
|
||||
oracle.set(UpdateArg{.series = {{"XRP", "EUR", 740, 1}}});
|
||||
}
|
||||
|
||||
// entire and trimmed stats
|
||||
auto ret =
|
||||
Oracle::aggregatePrice(env, "XRP", "USD", oracles, 20, 200);
|
||||
BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.6");
|
||||
BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 7);
|
||||
BEAST_EXPECT(
|
||||
ret[jss::entire_set][jss::standard_deviation] ==
|
||||
"0.2160246899469287");
|
||||
BEAST_EXPECT(ret[jss::median] == "74.6");
|
||||
BEAST_EXPECT(ret[jss::trimmed_set][jss::mean] == "74.6");
|
||||
BEAST_EXPECT(ret[jss::trimmed_set][jss::size].asUInt() == 5);
|
||||
BEAST_EXPECT(
|
||||
ret[jss::trimmed_set][jss::standard_deviation] ==
|
||||
"0.158113883008419");
|
||||
BEAST_EXPECT(ret[jss::time] == 946694900);
|
||||
}
|
||||
|
||||
// Reduced data set because of the time threshold
|
||||
{
|
||||
Env env(*this);
|
||||
std::vector<std::pair<Account, std::uint32_t>> oracles;
|
||||
prep(env, oracles);
|
||||
for (int i = 0; i < oracles.size(); ++i)
|
||||
{
|
||||
Oracle oracle(
|
||||
env,
|
||||
{.owner = oracles[i].first,
|
||||
.documentID = oracles[i].second},
|
||||
false);
|
||||
// push XRP/USD by two ledgers, so this price
|
||||
// is included in the dataset
|
||||
oracle.set(UpdateArg{.series = {{"XRP", "USD", 740, 1}}});
|
||||
}
|
||||
|
||||
// entire stats only, limit lastUpdateTime to {200, 125}
|
||||
auto ret = Oracle::aggregatePrice(
|
||||
env, "XRP", "USD", oracles, std::nullopt, 75);
|
||||
BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74");
|
||||
BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 8);
|
||||
BEAST_EXPECT(ret[jss::entire_set][jss::standard_deviation] == "0");
|
||||
BEAST_EXPECT(ret[jss::median] == "74");
|
||||
BEAST_EXPECT(ret[jss::time] == 946695000);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testErrors();
|
||||
testRpc();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(GetAggregatePrice, app, ripple);
|
||||
|
||||
} // namespace oracle
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
Reference in New Issue
Block a user