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
698
src/test/app/Oracle_test.cpp
Normal file
698
src/test/app/Oracle_test.cpp
Normal file
@@ -0,0 +1,698 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/protocol/jss.h>
|
||||
#include <test/jtx/Oracle.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
namespace oracle {
|
||||
|
||||
struct Oracle_test : public beast::unit_test::suite
|
||||
{
|
||||
private:
|
||||
// Helper function that returns the owner count of an account root.
|
||||
static std::uint32_t
|
||||
ownerCount(jtx::Env const& env, jtx::Account const& acct)
|
||||
{
|
||||
std::uint32_t ret{0};
|
||||
if (auto const sleAcct = env.le(acct))
|
||||
ret = sleAcct->at(sfOwnerCount);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void
|
||||
testInvalidSet()
|
||||
{
|
||||
testcase("Invalid Set");
|
||||
|
||||
using namespace jtx;
|
||||
Account const owner("owner");
|
||||
|
||||
{
|
||||
// Invalid account
|
||||
Env env(*this);
|
||||
Account const bad("bad");
|
||||
env.memoize(bad);
|
||||
Oracle oracle(
|
||||
env, {.owner = bad, .seq = seq(1), .err = ter(terNO_ACCOUNT)});
|
||||
}
|
||||
|
||||
// Insufficient reserve
|
||||
{
|
||||
Env env(*this);
|
||||
env.fund(env.current()->fees().accountReserve(0), owner);
|
||||
Oracle oracle(
|
||||
env, {.owner = owner, .err = ter(tecINSUFFICIENT_RESERVE)});
|
||||
}
|
||||
// Insufficient reserve if the data series extends to greater than 5
|
||||
{
|
||||
Env env(*this);
|
||||
env.fund(
|
||||
env.current()->fees().accountReserve(1) +
|
||||
env.current()->fees().base * 2,
|
||||
owner);
|
||||
Oracle oracle(env, {.owner = owner});
|
||||
BEAST_EXPECT(oracle.exists());
|
||||
oracle.set(UpdateArg{
|
||||
.series =
|
||||
{
|
||||
{"XRP", "EUR", 740, 1},
|
||||
{"XRP", "GBP", 740, 1},
|
||||
{"XRP", "CNY", 740, 1},
|
||||
{"XRP", "CAD", 740, 1},
|
||||
{"XRP", "AUD", 740, 1},
|
||||
},
|
||||
.err = ter(tecINSUFFICIENT_RESERVE)});
|
||||
}
|
||||
|
||||
{
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000), owner);
|
||||
Oracle oracle(env, {.owner = owner}, false);
|
||||
|
||||
// Invalid flag
|
||||
oracle.set(
|
||||
CreateArg{.flags = tfSellNFToken, .err = ter(temINVALID_FLAG)});
|
||||
|
||||
// Duplicate token pair
|
||||
oracle.set(CreateArg{
|
||||
.series = {{"XRP", "USD", 740, 1}, {"XRP", "USD", 750, 1}},
|
||||
.err = ter(temMALFORMED)});
|
||||
|
||||
// Price is not included
|
||||
oracle.set(CreateArg{
|
||||
.series =
|
||||
{{"XRP", "USD", 740, 1}, {"XRP", "EUR", std::nullopt, 1}},
|
||||
.err = ter(temMALFORMED)});
|
||||
|
||||
// Token pair is in update and delete
|
||||
oracle.set(CreateArg{
|
||||
.series =
|
||||
{{"XRP", "USD", 740, 1}, {"XRP", "USD", std::nullopt, 1}},
|
||||
.err = ter(temMALFORMED)});
|
||||
// Token pair is in add and delete
|
||||
oracle.set(CreateArg{
|
||||
.series =
|
||||
{{"XRP", "EUR", 740, 1}, {"XRP", "EUR", std::nullopt, 1}},
|
||||
.err = ter(temMALFORMED)});
|
||||
|
||||
// Array of token pair is 0 or exceeds 10
|
||||
oracle.set(CreateArg{
|
||||
.series =
|
||||
{{"XRP", "US1", 740, 1},
|
||||
{"XRP", "US2", 750, 1},
|
||||
{"XRP", "US3", 740, 1},
|
||||
{"XRP", "US4", 750, 1},
|
||||
{"XRP", "US5", 740, 1},
|
||||
{"XRP", "US6", 750, 1},
|
||||
{"XRP", "US7", 740, 1},
|
||||
{"XRP", "US8", 750, 1},
|
||||
{"XRP", "US9", 740, 1},
|
||||
{"XRP", "U10", 750, 1},
|
||||
{"XRP", "U11", 740, 1}},
|
||||
.err = ter(temARRAY_TOO_LARGE)});
|
||||
oracle.set(CreateArg{.series = {}, .err = ter(temARRAY_EMPTY)});
|
||||
}
|
||||
|
||||
// Array of token pair exceeds 10 after update
|
||||
{
|
||||
Env env{*this};
|
||||
env.fund(XRP(1'000), owner);
|
||||
|
||||
Oracle oracle(
|
||||
env,
|
||||
CreateArg{
|
||||
.owner = owner, .series = {{{"XRP", "USD", 740, 1}}}});
|
||||
oracle.set(UpdateArg{
|
||||
.series =
|
||||
{
|
||||
{"XRP", "US1", 740, 1},
|
||||
{"XRP", "US2", 750, 1},
|
||||
{"XRP", "US3", 740, 1},
|
||||
{"XRP", "US4", 750, 1},
|
||||
{"XRP", "US5", 740, 1},
|
||||
{"XRP", "US6", 750, 1},
|
||||
{"XRP", "US7", 740, 1},
|
||||
{"XRP", "US8", 750, 1},
|
||||
{"XRP", "US9", 740, 1},
|
||||
{"XRP", "U10", 750, 1},
|
||||
},
|
||||
.err = ter(tecARRAY_TOO_LARGE)});
|
||||
}
|
||||
|
||||
{
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000), owner);
|
||||
Oracle oracle(env, {.owner = owner}, false);
|
||||
|
||||
// Symbol class or provider not included on create
|
||||
oracle.set(CreateArg{
|
||||
.assetClass = std::nullopt,
|
||||
.provider = "provider",
|
||||
.err = ter(temMALFORMED)});
|
||||
oracle.set(CreateArg{
|
||||
.assetClass = "currency",
|
||||
.provider = std::nullopt,
|
||||
.uri = "URI",
|
||||
.err = ter(temMALFORMED)});
|
||||
|
||||
// Symbol class or provider are included on update
|
||||
// and don't match the current values
|
||||
oracle.set(CreateArg{});
|
||||
BEAST_EXPECT(oracle.exists());
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"XRP", "USD", 740, 1}},
|
||||
.provider = "provider1",
|
||||
.err = ter(temMALFORMED)});
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"XRP", "USD", 740, 1}},
|
||||
.assetClass = "currency1",
|
||||
.err = ter(temMALFORMED)});
|
||||
}
|
||||
|
||||
{
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000), owner);
|
||||
Oracle oracle(env, {.owner = owner}, false);
|
||||
|
||||
// Fields too long
|
||||
// Symbol class
|
||||
std::string assetClass(17, '0');
|
||||
oracle.set(
|
||||
CreateArg{.assetClass = assetClass, .err = ter(temMALFORMED)});
|
||||
// provider
|
||||
std::string const large(257, '0');
|
||||
oracle.set(CreateArg{.provider = large, .err = ter(temMALFORMED)});
|
||||
// URI
|
||||
oracle.set(CreateArg{.uri = large, .err = ter(temMALFORMED)});
|
||||
}
|
||||
|
||||
{
|
||||
// Different owner creates a new object and fails because
|
||||
// of missing fields currency/provider
|
||||
Env env(*this);
|
||||
Account const some("some");
|
||||
env.fund(XRP(1'000), owner);
|
||||
env.fund(XRP(1'000), some);
|
||||
Oracle oracle(env, {.owner = owner});
|
||||
BEAST_EXPECT(oracle.exists());
|
||||
oracle.set(UpdateArg{
|
||||
.owner = some,
|
||||
.series = {{"XRP", "USD", 740, 1}},
|
||||
.err = ter(temMALFORMED)});
|
||||
}
|
||||
|
||||
{
|
||||
// Invalid update time
|
||||
using namespace std::chrono;
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000), owner);
|
||||
Oracle oracle(env, {.owner = owner});
|
||||
BEAST_EXPECT(oracle.exists());
|
||||
env.close(seconds(400));
|
||||
// Less than the last close time - 300s
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"XRP", "USD", 740, 1}},
|
||||
.lastUpdateTime = testStartTime.count() + 400 - 301,
|
||||
.err = ter(tecINVALID_UPDATE_TIME)});
|
||||
// Greater than last close time + 300s
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"XRP", "USD", 740, 1}},
|
||||
.lastUpdateTime = testStartTime.count() + 400 + 301,
|
||||
.err = ter(tecINVALID_UPDATE_TIME)});
|
||||
oracle.set(UpdateArg{.series = {{"XRP", "USD", 740, 1}}});
|
||||
BEAST_EXPECT(
|
||||
oracle.expectLastUpdateTime(testStartTime.count() + 450));
|
||||
// Less than the previous lastUpdateTime
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"XRP", "USD", 740, 1}},
|
||||
.lastUpdateTime = testStartTime.count() + 449,
|
||||
.err = ter(tecINVALID_UPDATE_TIME)});
|
||||
}
|
||||
|
||||
{
|
||||
// delete token pair that doesn't exist
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000), owner);
|
||||
Oracle oracle(env, {.owner = owner});
|
||||
BEAST_EXPECT(oracle.exists());
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"XRP", "EUR", std::nullopt, std::nullopt}},
|
||||
.err = ter(tecTOKEN_PAIR_NOT_FOUND)});
|
||||
// delete all token pairs
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"XRP", "USD", std::nullopt, std::nullopt}},
|
||||
.err = ter(tecARRAY_EMPTY)});
|
||||
}
|
||||
|
||||
{
|
||||
// same BaseAsset and QuoteAsset
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000), owner);
|
||||
Oracle oracle(
|
||||
env,
|
||||
{.owner = owner,
|
||||
.series = {{"USD", "USD", 740, 1}},
|
||||
.err = ter(temMALFORMED)});
|
||||
}
|
||||
|
||||
{
|
||||
// Scale is greater than maxPriceScale
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000), owner);
|
||||
Oracle oracle(
|
||||
env,
|
||||
{.owner = owner,
|
||||
.series = {{"USD", "BTC", 740, maxPriceScale + 1}},
|
||||
.err = ter(temMALFORMED)});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCreate()
|
||||
{
|
||||
testcase("Create");
|
||||
using namespace jtx;
|
||||
Account const owner("owner");
|
||||
|
||||
auto test = [&](Env& env, DataSeries const& series, std::uint16_t adj) {
|
||||
env.fund(XRP(1'000), owner);
|
||||
auto const count = ownerCount(env, owner);
|
||||
Oracle oracle(env, {.owner = owner, .series = series});
|
||||
BEAST_EXPECT(oracle.exists());
|
||||
BEAST_EXPECT(ownerCount(env, owner) == (count + adj));
|
||||
BEAST_EXPECT(oracle.expectLastUpdateTime(946694810));
|
||||
};
|
||||
|
||||
{
|
||||
// owner count is adjusted by 1
|
||||
Env env(*this);
|
||||
test(env, {{"XRP", "USD", 740, 1}}, 1);
|
||||
}
|
||||
|
||||
{
|
||||
// owner count is adjusted by 2
|
||||
Env env(*this);
|
||||
test(
|
||||
env,
|
||||
{{"XRP", "USD", 740, 1},
|
||||
{"BTC", "USD", 740, 1},
|
||||
{"ETH", "USD", 740, 1},
|
||||
{"CAN", "USD", 740, 1},
|
||||
{"YAN", "USD", 740, 1},
|
||||
{"GBP", "USD", 740, 1}},
|
||||
2);
|
||||
}
|
||||
|
||||
{
|
||||
// Different owner creates a new object
|
||||
Env env(*this);
|
||||
Account const some("some");
|
||||
env.fund(XRP(1'000), owner);
|
||||
env.fund(XRP(1'000), some);
|
||||
Oracle oracle(env, {.owner = owner});
|
||||
BEAST_EXPECT(oracle.exists());
|
||||
oracle.set(CreateArg{
|
||||
.owner = some, .series = {{"912810RR9", "USD", 740, 1}}});
|
||||
BEAST_EXPECT(Oracle::exists(env, some, oracle.documentID()));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testInvalidDelete()
|
||||
{
|
||||
testcase("Invalid Delete");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this);
|
||||
Account const owner("owner");
|
||||
env.fund(XRP(1'000), owner);
|
||||
Oracle oracle(env, {.owner = owner});
|
||||
BEAST_EXPECT(oracle.exists());
|
||||
|
||||
{
|
||||
// Invalid account
|
||||
Account const bad("bad");
|
||||
env.memoize(bad);
|
||||
oracle.remove(
|
||||
{.owner = bad, .seq = seq(1), .err = ter(terNO_ACCOUNT)});
|
||||
}
|
||||
|
||||
// Invalid Sequence
|
||||
oracle.remove({.documentID = 2, .err = ter(tecNO_ENTRY)});
|
||||
|
||||
// Invalid owner
|
||||
Account const invalid("invalid");
|
||||
env.fund(XRP(1'000), invalid);
|
||||
oracle.remove({.owner = invalid, .err = ter(tecNO_ENTRY)});
|
||||
}
|
||||
|
||||
void
|
||||
testDelete()
|
||||
{
|
||||
testcase("Delete");
|
||||
using namespace jtx;
|
||||
Account const owner("owner");
|
||||
|
||||
auto test = [&](Env& env, DataSeries const& series, std::uint16_t adj) {
|
||||
env.fund(XRP(1'000), owner);
|
||||
Oracle oracle(env, {.owner = owner, .series = series});
|
||||
auto const count = ownerCount(env, owner);
|
||||
BEAST_EXPECT(oracle.exists());
|
||||
oracle.remove({});
|
||||
BEAST_EXPECT(!oracle.exists());
|
||||
BEAST_EXPECT(ownerCount(env, owner) == (count - adj));
|
||||
};
|
||||
|
||||
{
|
||||
// owner count is adjusted by 1
|
||||
Env env(*this);
|
||||
test(env, {{"XRP", "USD", 740, 1}}, 1);
|
||||
}
|
||||
|
||||
{
|
||||
// owner count is adjusted by 2
|
||||
Env env(*this);
|
||||
test(
|
||||
env,
|
||||
{
|
||||
{"XRP", "USD", 740, 1},
|
||||
{"BTC", "USD", 740, 1},
|
||||
{"ETH", "USD", 740, 1},
|
||||
{"CAN", "USD", 740, 1},
|
||||
{"YAN", "USD", 740, 1},
|
||||
{"GBP", "USD", 740, 1},
|
||||
},
|
||||
2);
|
||||
}
|
||||
|
||||
{
|
||||
// deleting the account deletes the oracles
|
||||
Env env(*this);
|
||||
auto const alice = Account("alice");
|
||||
auto const acctDelFee{drops(env.current()->fees().increment)};
|
||||
env.fund(XRP(1'000), owner);
|
||||
env.fund(XRP(1'000), alice);
|
||||
Oracle oracle(
|
||||
env, {.owner = owner, .series = {{"XRP", "USD", 740, 1}}});
|
||||
Oracle oracle1(
|
||||
env,
|
||||
{.owner = owner,
|
||||
.documentID = 2,
|
||||
.series = {{"XRP", "EUR", 740, 1}}});
|
||||
BEAST_EXPECT(ownerCount(env, owner) == 2);
|
||||
BEAST_EXPECT(oracle.exists());
|
||||
BEAST_EXPECT(oracle1.exists());
|
||||
auto const index = env.closed()->seq();
|
||||
auto const hash = env.closed()->info().hash;
|
||||
for (int i = 0; i < 256; ++i)
|
||||
env.close();
|
||||
env(acctdelete(owner, alice), fee(acctDelFee));
|
||||
env.close();
|
||||
BEAST_EXPECT(!oracle.exists());
|
||||
BEAST_EXPECT(!oracle1.exists());
|
||||
|
||||
// can still get the oracles via the ledger index or hash
|
||||
auto verifyLedgerData = [&](auto const& field, auto const& value) {
|
||||
Json::Value jvParams;
|
||||
jvParams[field] = value;
|
||||
jvParams[jss::binary] = false;
|
||||
jvParams[jss::type] = jss::oracle;
|
||||
Json::Value jrr = env.rpc(
|
||||
"json",
|
||||
"ledger_data",
|
||||
boost::lexical_cast<std::string>(jvParams));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::state].size() == 2);
|
||||
};
|
||||
verifyLedgerData(jss::ledger_index, index);
|
||||
verifyLedgerData(jss::ledger_hash, to_string(hash));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testUpdate()
|
||||
{
|
||||
testcase("Update");
|
||||
using namespace jtx;
|
||||
Account const owner("owner");
|
||||
|
||||
{
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000), owner);
|
||||
auto count = ownerCount(env, owner);
|
||||
Oracle oracle(env, {.owner = owner});
|
||||
BEAST_EXPECT(oracle.exists());
|
||||
|
||||
// update existing pair
|
||||
oracle.set(UpdateArg{.series = {{"XRP", "USD", 740, 2}}});
|
||||
BEAST_EXPECT(oracle.expectPrice({{"XRP", "USD", 740, 2}}));
|
||||
// owner count is increased by 1 since the oracle object is added
|
||||
// with one token pair
|
||||
count += 1;
|
||||
BEAST_EXPECT(ownerCount(env, owner) == count);
|
||||
|
||||
// add new pairs, not-included pair is reset
|
||||
oracle.set(UpdateArg{.series = {{"XRP", "EUR", 700, 2}}});
|
||||
BEAST_EXPECT(oracle.expectPrice(
|
||||
{{"XRP", "USD", 0, 0}, {"XRP", "EUR", 700, 2}}));
|
||||
// owner count is not changed since the number of pairs is 2
|
||||
BEAST_EXPECT(ownerCount(env, owner) == count);
|
||||
|
||||
// update both pairs
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"XRP", "USD", 741, 2}, {"XRP", "EUR", 710, 2}}});
|
||||
BEAST_EXPECT(oracle.expectPrice(
|
||||
{{"XRP", "USD", 741, 2}, {"XRP", "EUR", 710, 2}}));
|
||||
// owner count is not changed since the number of pairs is 2
|
||||
BEAST_EXPECT(ownerCount(env, owner) == count);
|
||||
|
||||
// owner count is increased by 1 since the number of pairs is 6
|
||||
oracle.set(UpdateArg{
|
||||
.series = {
|
||||
{"BTC", "USD", 741, 2},
|
||||
{"ETH", "EUR", 710, 2},
|
||||
{"YAN", "EUR", 710, 2},
|
||||
{"CAN", "EUR", 710, 2},
|
||||
}});
|
||||
count += 1;
|
||||
BEAST_EXPECT(ownerCount(env, owner) == count);
|
||||
|
||||
// update two pairs and delete four
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"BTC", "USD", std::nullopt, std::nullopt}}});
|
||||
oracle.set(UpdateArg{
|
||||
.series = {
|
||||
{"XRP", "USD", 742, 2},
|
||||
{"XRP", "EUR", 711, 2},
|
||||
{"ETH", "EUR", std::nullopt, std::nullopt},
|
||||
{"YAN", "EUR", std::nullopt, std::nullopt},
|
||||
{"CAN", "EUR", std::nullopt, std::nullopt}}});
|
||||
BEAST_EXPECT(oracle.expectPrice(
|
||||
{{"XRP", "USD", 742, 2}, {"XRP", "EUR", 711, 2}}));
|
||||
// owner count is decreased by 1 since the number of pairs is 2
|
||||
count -= 1;
|
||||
BEAST_EXPECT(ownerCount(env, owner) == count);
|
||||
}
|
||||
|
||||
// Min reserve to create and update
|
||||
{
|
||||
Env env(*this);
|
||||
env.fund(
|
||||
env.current()->fees().accountReserve(1) +
|
||||
env.current()->fees().base * 2,
|
||||
owner);
|
||||
Oracle oracle(env, {.owner = owner});
|
||||
oracle.set(UpdateArg{.series = {{"XRP", "USD", 742, 2}}});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMultisig(FeatureBitset features)
|
||||
{
|
||||
testcase("Multisig");
|
||||
using namespace jtx;
|
||||
Oracle::setFee(100'000);
|
||||
|
||||
Env env(*this, features);
|
||||
Account const alice{"alice", KeyType::secp256k1};
|
||||
Account const bogie{"bogie", KeyType::secp256k1};
|
||||
Account const ed{"ed", KeyType::secp256k1};
|
||||
Account const becky{"becky", KeyType::ed25519};
|
||||
Account const zelda{"zelda", KeyType::secp256k1};
|
||||
Account const bob{"bob", KeyType::secp256k1};
|
||||
env.fund(XRP(10'000), alice, becky, zelda, ed, bob);
|
||||
|
||||
// alice uses a regular key with the master disabled.
|
||||
Account const alie{"alie", KeyType::secp256k1};
|
||||
env(regkey(alice, alie));
|
||||
env(fset(alice, asfDisableMaster), sig(alice));
|
||||
|
||||
// Attach signers to alice.
|
||||
env(signers(alice, 2, {{becky, 1}, {bogie, 1}, {ed, 2}}), sig(alie));
|
||||
env.close();
|
||||
// if multiSignReserve disabled then its 2 + 1 per signer
|
||||
int const signerListOwners{features[featureMultiSignReserve] ? 1 : 5};
|
||||
env.require(owners(alice, signerListOwners));
|
||||
|
||||
// Create
|
||||
// Force close (true) and time advancement because the close time
|
||||
// is no longer 0.
|
||||
Oracle oracle(env, CreateArg{.owner = alice, .close = true}, false);
|
||||
oracle.set(CreateArg{.msig = msig(becky), .err = ter(tefBAD_QUORUM)});
|
||||
oracle.set(
|
||||
CreateArg{.msig = msig(zelda), .err = ter(tefBAD_SIGNATURE)});
|
||||
oracle.set(CreateArg{.msig = msig(becky, bogie)});
|
||||
BEAST_EXPECT(oracle.exists());
|
||||
|
||||
// Update
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"XRP", "USD", 740, 1}},
|
||||
.msig = msig(becky),
|
||||
.err = ter(tefBAD_QUORUM)});
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"XRP", "USD", 740, 1}},
|
||||
.msig = msig(zelda),
|
||||
.err = ter(tefBAD_SIGNATURE)});
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"XRP", "USD", 741, 1}}, .msig = msig(becky, bogie)});
|
||||
BEAST_EXPECT(oracle.expectPrice({{"XRP", "USD", 741, 1}}));
|
||||
// remove the signer list
|
||||
env(signers(alice, jtx::none), sig(alie));
|
||||
env.close();
|
||||
env.require(owners(alice, 1));
|
||||
// create new signer list
|
||||
env(signers(alice, 2, {{zelda, 1}, {bob, 1}, {ed, 2}}), sig(alie));
|
||||
env.close();
|
||||
// old list fails
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"XRP", "USD", 740, 1}},
|
||||
.msig = msig(becky, bogie),
|
||||
.err = ter(tefBAD_SIGNATURE)});
|
||||
// updated list succeeds
|
||||
oracle.set(UpdateArg{
|
||||
.series = {{"XRP", "USD", 7412, 2}}, .msig = msig(zelda, bob)});
|
||||
BEAST_EXPECT(oracle.expectPrice({{"XRP", "USD", 7412, 2}}));
|
||||
oracle.set(
|
||||
UpdateArg{.series = {{"XRP", "USD", 74245, 3}}, .msig = msig(ed)});
|
||||
BEAST_EXPECT(oracle.expectPrice({{"XRP", "USD", 74245, 3}}));
|
||||
|
||||
// Remove
|
||||
oracle.remove({.msig = msig(bob), .err = ter(tefBAD_QUORUM)});
|
||||
oracle.remove({.msig = msig(becky), .err = ter(tefBAD_SIGNATURE)});
|
||||
oracle.remove({.msig = msig(ed)});
|
||||
BEAST_EXPECT(!oracle.exists());
|
||||
}
|
||||
|
||||
void
|
||||
testAmendment()
|
||||
{
|
||||
testcase("Amendment");
|
||||
using namespace jtx;
|
||||
|
||||
auto const features = supported_amendments() - featurePriceOracle;
|
||||
Account const owner("owner");
|
||||
Env env(*this, features);
|
||||
|
||||
env.fund(XRP(1'000), owner);
|
||||
{
|
||||
Oracle oracle(env, {.owner = owner, .err = ter(temDISABLED)});
|
||||
}
|
||||
|
||||
{
|
||||
Oracle oracle(env, {.owner = owner}, false);
|
||||
oracle.remove({.err = ter(temDISABLED)});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLedgerEntry()
|
||||
{
|
||||
testcase("Ledger Entry");
|
||||
using namespace jtx;
|
||||
|
||||
Env env(*this);
|
||||
std::vector<AccountID> accounts;
|
||||
std::vector<std::uint32_t> oracles;
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
Account const owner(std::string("owner") + std::to_string(i));
|
||||
env.fund(XRP(1'000), owner);
|
||||
// different accounts can have the same asset pair
|
||||
Oracle oracle(env, {.owner = owner, .documentID = i});
|
||||
accounts.push_back(owner.id());
|
||||
oracles.push_back(oracle.documentID());
|
||||
// same account can have different asset pair
|
||||
Oracle oracle1(env, {.owner = owner, .documentID = i + 10});
|
||||
accounts.push_back(owner.id());
|
||||
oracles.push_back(oracle1.documentID());
|
||||
}
|
||||
for (int i = 0; i < accounts.size(); ++i)
|
||||
{
|
||||
auto const jv = [&]() {
|
||||
// document id is uint32
|
||||
if (i % 2)
|
||||
return Oracle::ledgerEntry(env, accounts[i], oracles[i]);
|
||||
// document id is string
|
||||
return Oracle::ledgerEntry(
|
||||
env, accounts[i], std::to_string(oracles[i]));
|
||||
}();
|
||||
try
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
jv[jss::node][jss::Owner] == to_string(accounts[i]));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
fail();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace jtx;
|
||||
auto const all = supported_amendments();
|
||||
testInvalidSet();
|
||||
testInvalidDelete();
|
||||
testCreate();
|
||||
testDelete();
|
||||
testUpdate();
|
||||
testAmendment();
|
||||
for (auto const& features :
|
||||
{all,
|
||||
all - featureMultiSignReserve - featureExpandedSignerList,
|
||||
all - featureExpandedSignerList})
|
||||
testMultisig(features);
|
||||
testLedgerEntry();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Oracle, app, ripple);
|
||||
|
||||
} // namespace oracle
|
||||
|
||||
} // namespace jtx
|
||||
|
||||
} // namespace test
|
||||
|
||||
} // namespace ripple
|
||||
186
src/test/jtx/Oracle.h
Normal file
186
src/test/jtx/Oracle.h
Normal file
@@ -0,0 +1,186 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012, 2013 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TEST_JTX_ORACLE_H_INCLUDED
|
||||
#define RIPPLE_TEST_JTX_ORACLE_H_INCLUDED
|
||||
|
||||
#include <date/date.h>
|
||||
#include <test/jtx.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
namespace oracle {
|
||||
|
||||
// base asset, quote asset, price, scale
|
||||
using DataSeries = std::vector<std::tuple<
|
||||
std::string,
|
||||
std::string,
|
||||
std::optional<std::uint32_t>,
|
||||
std::optional<std::uint8_t>>>;
|
||||
|
||||
// Typical defaults for Create
|
||||
struct CreateArg
|
||||
{
|
||||
std::optional<AccountID> owner = std::nullopt;
|
||||
std::optional<std::uint32_t> documentID = 1;
|
||||
DataSeries series = {{"XRP", "USD", 740, 1}};
|
||||
std::optional<std::string> assetClass = "currency";
|
||||
std::optional<std::string> provider = "provider";
|
||||
std::optional<std::string> uri = "URI";
|
||||
std::optional<std::uint32_t> lastUpdateTime = std::nullopt;
|
||||
std::uint32_t flags = 0;
|
||||
std::optional<jtx::msig> msig = std::nullopt;
|
||||
std::optional<jtx::seq> seq = std::nullopt;
|
||||
std::uint32_t fee = 10;
|
||||
std::optional<ter> err = std::nullopt;
|
||||
bool close = false;
|
||||
};
|
||||
|
||||
// Typical defaults for Update
|
||||
struct UpdateArg
|
||||
{
|
||||
std::optional<AccountID> owner = std::nullopt;
|
||||
std::optional<std::uint32_t> documentID = std::nullopt;
|
||||
DataSeries series = {};
|
||||
std::optional<std::string> assetClass = std::nullopt;
|
||||
std::optional<std::string> provider = std::nullopt;
|
||||
std::optional<std::string> uri = "URI";
|
||||
std::optional<std::uint32_t> lastUpdateTime = std::nullopt;
|
||||
std::uint32_t flags = 0;
|
||||
std::optional<jtx::msig> msig = std::nullopt;
|
||||
std::optional<jtx::seq> seq = std::nullopt;
|
||||
std::uint32_t fee = 10;
|
||||
std::optional<ter> err = std::nullopt;
|
||||
};
|
||||
|
||||
struct RemoveArg
|
||||
{
|
||||
std::optional<AccountID> const& owner = std::nullopt;
|
||||
std::optional<std::uint32_t> const& documentID = std::nullopt;
|
||||
std::optional<jtx::msig> const& msig = std::nullopt;
|
||||
std::optional<jtx::seq> seq = std::nullopt;
|
||||
std::uint32_t fee = 10;
|
||||
std::optional<ter> const& err = std::nullopt;
|
||||
};
|
||||
|
||||
// Simulate testStartTime as 10'000s from Ripple epoch time to make
|
||||
// LastUpdateTime validation to work and to make unit-test consistent.
|
||||
// The value doesn't matter much, it has to be greater
|
||||
// than maxLastUpdateTimeDelta in order to pass LastUpdateTime
|
||||
// validation {close-maxLastUpdateTimeDelta,close+maxLastUpdateTimeDelta}.
|
||||
constexpr static std::chrono::seconds testStartTime =
|
||||
epoch_offset + std::chrono::seconds(10'000);
|
||||
|
||||
/** Oracle class facilitates unit-testing of the Price Oracle feature.
|
||||
* It defines functions to create, update, and delete the Oracle object,
|
||||
* to query for various states, and to call APIs.
|
||||
*/
|
||||
class Oracle
|
||||
{
|
||||
private:
|
||||
// Global fee if not 0
|
||||
static inline std::uint32_t fee = 0;
|
||||
Env& env_;
|
||||
AccountID owner_;
|
||||
std::uint32_t documentID_;
|
||||
|
||||
private:
|
||||
void
|
||||
submit(
|
||||
Json::Value const& jv,
|
||||
std::optional<jtx::msig> const& msig,
|
||||
std::optional<jtx::seq> const& seq,
|
||||
std::optional<ter> const& err);
|
||||
|
||||
public:
|
||||
Oracle(Env& env, CreateArg const& arg, bool submit = true);
|
||||
|
||||
void
|
||||
remove(RemoveArg const& arg);
|
||||
|
||||
void
|
||||
set(CreateArg const& arg);
|
||||
void
|
||||
set(UpdateArg const& arg);
|
||||
|
||||
static Json::Value
|
||||
aggregatePrice(
|
||||
Env& env,
|
||||
std::optional<std::string> const& baseAsset,
|
||||
std::optional<std::string> const& quoteAsset,
|
||||
std::optional<std::vector<std::pair<Account, std::uint32_t>>> const&
|
||||
oracles = std::nullopt,
|
||||
std::optional<std::uint8_t> const& trim = std::nullopt,
|
||||
std::optional<std::uint8_t> const& timeTreshold = std::nullopt);
|
||||
|
||||
std::uint32_t
|
||||
documentID() const
|
||||
{
|
||||
return documentID_;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
exists() const
|
||||
{
|
||||
return exists(env_, owner_, documentID_);
|
||||
}
|
||||
|
||||
[[nodiscard]] static bool
|
||||
exists(Env& env, AccountID const& account, std::uint32_t documentID);
|
||||
|
||||
[[nodiscard]] bool
|
||||
expectPrice(DataSeries const& pricess) const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
expectLastUpdateTime(std::uint32_t lastUpdateTime) const;
|
||||
|
||||
static Json::Value
|
||||
ledgerEntry(
|
||||
Env& env,
|
||||
AccountID const& account,
|
||||
std::variant<std::uint32_t, std::string> const& documentID,
|
||||
std::optional<std::string> const& index = std::nullopt);
|
||||
|
||||
Json::Value
|
||||
ledgerEntry(std::optional<std::string> const& index = std::nullopt) const
|
||||
{
|
||||
return Oracle::ledgerEntry(env_, owner_, documentID_, index);
|
||||
}
|
||||
|
||||
static void
|
||||
setFee(std::uint32_t f)
|
||||
{
|
||||
fee = f;
|
||||
}
|
||||
|
||||
friend std::ostream&
|
||||
operator<<(std::ostream& strm, Oracle const& oracle)
|
||||
{
|
||||
strm << oracle.ledgerEntry().toStyledString();
|
||||
return strm;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace oracle
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
#endif // RIPPLE_TEST_JTX_ORACLE_H_INCLUDED
|
||||
292
src/test/jtx/impl/Oracle.cpp
Normal file
292
src/test/jtx/impl/Oracle.cpp
Normal file
@@ -0,0 +1,292 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012, 2013 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/protocol/digest.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx/Oracle.h>
|
||||
|
||||
#include <boost/lexical_cast/try_lexical_convert.hpp>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
namespace oracle {
|
||||
|
||||
Oracle::Oracle(Env& env, CreateArg const& arg, bool submit)
|
||||
: env_(env), owner_{}, documentID_{}
|
||||
{
|
||||
// LastUpdateTime is checked to be in range
|
||||
// {close-maxLastUpdateTimeDelta, close+maxLastUpdateTimeDelta}.
|
||||
// To make the validation work and to make the clock consistent
|
||||
// for tests running at different time, simulate Unix time starting
|
||||
// on testStartTime since Ripple epoch.
|
||||
auto const now = env_.timeKeeper().now();
|
||||
if (now.time_since_epoch().count() == 0 || arg.close)
|
||||
env_.close(now + testStartTime - epoch_offset);
|
||||
if (arg.owner)
|
||||
owner_ = *arg.owner;
|
||||
if (arg.documentID)
|
||||
documentID_ = *arg.documentID;
|
||||
if (submit)
|
||||
set(arg);
|
||||
}
|
||||
|
||||
void
|
||||
Oracle::remove(RemoveArg const& arg)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::OracleDelete;
|
||||
jv[jss::Account] = to_string(arg.owner.value_or(owner_));
|
||||
jv[jss::OracleDocumentID] = arg.documentID.value_or(documentID_);
|
||||
if (Oracle::fee != 0)
|
||||
jv[jss::Fee] = std::to_string(Oracle::fee);
|
||||
else if (arg.fee != 0)
|
||||
jv[jss::Fee] = std::to_string(arg.fee);
|
||||
else
|
||||
jv[jss::Fee] = std::to_string(env_.current()->fees().increment.drops());
|
||||
submit(jv, arg.msig, arg.seq, arg.err);
|
||||
}
|
||||
|
||||
void
|
||||
Oracle::submit(
|
||||
Json::Value const& jv,
|
||||
std::optional<jtx::msig> const& msig,
|
||||
std::optional<jtx::seq> const& seq,
|
||||
std::optional<ter> const& err)
|
||||
{
|
||||
if (msig)
|
||||
{
|
||||
if (seq && err)
|
||||
env_(jv, *msig, *seq, *err);
|
||||
else if (seq)
|
||||
env_(jv, *msig, *seq);
|
||||
else if (err)
|
||||
env_(jv, *msig, *err);
|
||||
else
|
||||
env_(jv, *msig);
|
||||
}
|
||||
else if (seq && err)
|
||||
env_(jv, *seq, *err);
|
||||
else if (seq)
|
||||
env_(jv, *seq);
|
||||
else if (err)
|
||||
env_(jv, *err);
|
||||
else
|
||||
env_(jv);
|
||||
env_.close();
|
||||
}
|
||||
|
||||
bool
|
||||
Oracle::exists(Env& env, AccountID const& account, std::uint32_t documentID)
|
||||
{
|
||||
assert(account.isNonZero());
|
||||
return env.le(keylet::oracle(account, documentID)) != nullptr;
|
||||
}
|
||||
|
||||
bool
|
||||
Oracle::expectPrice(DataSeries const& series) const
|
||||
{
|
||||
if (auto const sle = env_.le(keylet::oracle(owner_, documentID_)))
|
||||
{
|
||||
auto const& leSeries = sle->getFieldArray(sfPriceDataSeries);
|
||||
if (leSeries.size() == 0 || leSeries.size() != series.size())
|
||||
return false;
|
||||
for (auto const& data : series)
|
||||
{
|
||||
if (std::find_if(
|
||||
leSeries.begin(),
|
||||
leSeries.end(),
|
||||
[&](STObject const& o) -> bool {
|
||||
auto const& baseAsset = o.getFieldCurrency(sfBaseAsset);
|
||||
auto const& quoteAsset =
|
||||
o.getFieldCurrency(sfQuoteAsset);
|
||||
auto const& price = o.getFieldU64(sfAssetPrice);
|
||||
auto const& scale = o.getFieldU8(sfScale);
|
||||
return baseAsset.getText() == std::get<0>(data) &&
|
||||
quoteAsset.getText() == std::get<1>(data) &&
|
||||
price == std::get<2>(data) &&
|
||||
scale == std::get<3>(data);
|
||||
}) == leSeries.end())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
Oracle::expectLastUpdateTime(std::uint32_t lastUpdateTime) const
|
||||
{
|
||||
auto const sle = env_.le(keylet::oracle(owner_, documentID_));
|
||||
return sle && (*sle)[sfLastUpdateTime] == lastUpdateTime;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
Oracle::aggregatePrice(
|
||||
Env& env,
|
||||
std::optional<std::string> const& baseAsset,
|
||||
std::optional<std::string> const& quoteAsset,
|
||||
std::optional<std::vector<std::pair<Account, std::uint32_t>>> const&
|
||||
oracles,
|
||||
std::optional<std::uint8_t> const& trim,
|
||||
std::optional<std::uint8_t> const& timeThreshold)
|
||||
{
|
||||
Json::Value jv;
|
||||
Json::Value jvOracles(Json::arrayValue);
|
||||
if (oracles)
|
||||
{
|
||||
for (auto const& id : *oracles)
|
||||
{
|
||||
Json::Value oracle;
|
||||
oracle[jss::account] = to_string(id.first.id());
|
||||
oracle[jss::oracle_document_id] = id.second;
|
||||
jvOracles.append(oracle);
|
||||
}
|
||||
jv[jss::oracles] = jvOracles;
|
||||
}
|
||||
if (trim)
|
||||
jv[jss::trim] = *trim;
|
||||
if (baseAsset)
|
||||
jv[jss::base_asset] = *baseAsset;
|
||||
if (quoteAsset)
|
||||
jv[jss::quote_asset] = *quoteAsset;
|
||||
if (timeThreshold)
|
||||
jv[jss::time_threshold] = *timeThreshold;
|
||||
|
||||
auto jr = env.rpc("json", "get_aggregate_price", to_string(jv));
|
||||
|
||||
if (jr.isObject() && jr.isMember(jss::result) &&
|
||||
jr[jss::result].isMember(jss::status))
|
||||
return jr[jss::result];
|
||||
return Json::nullValue;
|
||||
}
|
||||
|
||||
void
|
||||
Oracle::set(UpdateArg const& arg)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
Json::Value jv;
|
||||
if (arg.owner)
|
||||
owner_ = *arg.owner;
|
||||
if (arg.documentID)
|
||||
documentID_ = *arg.documentID;
|
||||
jv[jss::TransactionType] = jss::OracleSet;
|
||||
jv[jss::Account] = to_string(owner_);
|
||||
jv[jss::OracleDocumentID] = documentID_;
|
||||
if (arg.assetClass)
|
||||
jv[jss::AssetClass] = strHex(*arg.assetClass);
|
||||
if (arg.provider)
|
||||
jv[jss::Provider] = strHex(*arg.provider);
|
||||
if (arg.uri)
|
||||
jv[jss::URI] = strHex(*arg.uri);
|
||||
if (arg.flags != 0)
|
||||
jv[jss::Flags] = arg.flags;
|
||||
if (Oracle::fee != 0)
|
||||
jv[jss::Fee] = std::to_string(Oracle::fee);
|
||||
else if (arg.fee != 0)
|
||||
jv[jss::Fee] = std::to_string(arg.fee);
|
||||
else
|
||||
jv[jss::Fee] = std::to_string(env_.current()->fees().increment.drops());
|
||||
// lastUpdateTime if provided is offset from testStartTime
|
||||
if (arg.lastUpdateTime)
|
||||
jv[jss::LastUpdateTime] =
|
||||
to_string(testStartTime.count() + *arg.lastUpdateTime);
|
||||
else
|
||||
jv[jss::LastUpdateTime] = to_string(
|
||||
duration_cast<seconds>(
|
||||
env_.current()->info().closeTime.time_since_epoch())
|
||||
.count() +
|
||||
epoch_offset.count());
|
||||
Json::Value dataSeries(Json::arrayValue);
|
||||
auto assetToStr = [](std::string const& s) {
|
||||
// assume standard currency
|
||||
if (s.size() == 3)
|
||||
return s;
|
||||
assert(s.size() <= 20);
|
||||
// anything else must be 160-bit hex string
|
||||
std::string h = strHex(s);
|
||||
return strHex(s).append(40 - s.size() * 2, '0');
|
||||
};
|
||||
for (auto const& data : arg.series)
|
||||
{
|
||||
Json::Value priceData;
|
||||
Json::Value price;
|
||||
price[jss::BaseAsset] = assetToStr(std::get<0>(data));
|
||||
price[jss::QuoteAsset] = assetToStr(std::get<1>(data));
|
||||
if (std::get<2>(data))
|
||||
price[jss::AssetPrice] = *std::get<2>(data);
|
||||
if (std::get<3>(data))
|
||||
price[jss::Scale] = *std::get<3>(data);
|
||||
priceData[jss::PriceData] = price;
|
||||
dataSeries.append(priceData);
|
||||
}
|
||||
jv[jss::PriceDataSeries] = dataSeries;
|
||||
submit(jv, arg.msig, arg.seq, arg.err);
|
||||
}
|
||||
|
||||
void
|
||||
Oracle::set(CreateArg const& arg)
|
||||
{
|
||||
set(UpdateArg{
|
||||
.owner = arg.owner,
|
||||
.documentID = arg.documentID,
|
||||
.series = arg.series,
|
||||
.assetClass = arg.assetClass,
|
||||
.provider = arg.provider,
|
||||
.uri = arg.uri,
|
||||
.lastUpdateTime = arg.lastUpdateTime,
|
||||
.flags = arg.flags,
|
||||
.msig = arg.msig,
|
||||
.seq = arg.seq,
|
||||
.fee = arg.fee,
|
||||
.err = arg.err});
|
||||
}
|
||||
|
||||
Json::Value
|
||||
Oracle::ledgerEntry(
|
||||
Env& env,
|
||||
AccountID const& account,
|
||||
std::variant<std::uint32_t, std::string> const& documentID,
|
||||
std::optional<std::string> const& index)
|
||||
{
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::oracle][jss::account] = to_string(account);
|
||||
if (std::holds_alternative<std::uint32_t>(documentID))
|
||||
jvParams[jss::oracle][jss::oracle_document_id] =
|
||||
std::get<std::uint32_t>(documentID);
|
||||
else
|
||||
jvParams[jss::oracle][jss::oracle_document_id] =
|
||||
std::get<std::string>(documentID);
|
||||
if (index)
|
||||
{
|
||||
std::uint32_t i;
|
||||
if (boost::conversion::try_lexical_convert(*index, i))
|
||||
jvParams[jss::oracle][jss::ledger_index] = i;
|
||||
else
|
||||
jvParams[jss::oracle][jss::ledger_index] = *index;
|
||||
}
|
||||
return env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
}
|
||||
|
||||
} // namespace oracle
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
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