Price Oracle: validate input parameters and extend test coverage: (#5013)

* Price Oracle: validate input parameters and extend test coverage:

Validate trim, time_threshold, document_id are valid
Int, UInt, or string convertible to UInt. Validate base_asset
and quote_asset are valid currency. Update error codes.
Extend Oracle and GetAggregatePrice unit-tests.
Denote unreachable coverage code.

* Set one-line LCOV_EXCL_LINE

* Move ledger_entry tests to LedgerRPC_test.cpp

* Add constants for "None"

* Fix LedgerRPC test

---------

Co-authored-by: Scott Determan <scott.determan@yahoo.com>
This commit is contained in:
Gregory Tsipenyuk
2024-05-09 15:17:16 -04:00
committed by GitHub
parent f650949573
commit f4da2e31d9
9 changed files with 472 additions and 157 deletions

View File

@@ -48,7 +48,7 @@ TER
DeleteOracle::preclaim(PreclaimContext const& ctx)
{
if (!ctx.view.exists(keylet::account(ctx.tx.getAccountID(sfAccount))))
return terNO_ACCOUNT;
return terNO_ACCOUNT; // LCOV_EXCL_LINE
if (auto const sle = ctx.view.read(keylet::oracle(
ctx.tx.getAccountID(sfAccount), ctx.tx[sfOracleDocumentID]));
@@ -60,8 +60,10 @@ DeleteOracle::preclaim(PreclaimContext const& ctx)
else if (ctx.tx.getAccountID(sfAccount) != sle->getAccountID(sfOwner))
{
// this can't happen because of the above check
// LCOV_EXCL_START
JLOG(ctx.j.debug()) << "Oracle Delete: invalid account.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
return tesSUCCESS;
}
@@ -74,18 +76,20 @@ DeleteOracle::deleteOracle(
beast::Journal j)
{
if (!sle)
return tesSUCCESS;
return tecINTERNAL; // LCOV_EXCL_LINE
if (!view.dirRemove(
keylet::ownerDir(account), (*sle)[sfOwnerNode], sle->key(), true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete Oracle from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const sleOwner = view.peek(keylet::account(account));
if (!sleOwner)
return tecINTERNAL;
return tecINTERNAL; // LCOV_EXCL_LINE
auto const count =
sle->getFieldArray(sfPriceDataSeries).size() > 5 ? -2 : -1;
@@ -104,7 +108,7 @@ DeleteOracle::doApply()
keylet::oracle(account_, ctx_.tx[sfOracleDocumentID])))
return deleteOracle(ctx_.view(), sle, account_, j_);
return tecINTERNAL;
return tecINTERNAL; // LCOV_EXCL_LINE
}
} // namespace ripple

View File

@@ -74,7 +74,7 @@ SetOracle::preclaim(PreclaimContext const& ctx)
auto const sleSetter =
ctx.view.read(keylet::account(ctx.tx.getAccountID(sfAccount)));
if (!sleSetter)
return terNO_ACCOUNT;
return terNO_ACCOUNT; // LCOV_EXCL_LINE
// lastUpdateTime must be within maxLastUpdateTimeDelta seconds
// of the last closed ledger
@@ -88,8 +88,7 @@ SetOracle::preclaim(PreclaimContext const& ctx)
std::size_t const lastUpdateTimeEpoch =
lastUpdateTime - epoch_offset.count();
if (closeTime < maxLastUpdateTimeDelta)
Throw<std::runtime_error>(
"Oracle: close time is less than maxLastUpdateTimeDelta");
return tecINTERNAL; // LCOV_EXCL_LINE
if (lastUpdateTimeEpoch < (closeTime - maxLastUpdateTimeDelta) ||
lastUpdateTimeEpoch > (closeTime + maxLastUpdateTimeDelta))
return tecINVALID_UPDATE_TIME;
@@ -194,7 +193,7 @@ adjustOwnerCount(ApplyContext& ctx, int count)
return true;
}
return false;
return false; // LCOV_EXCL_LINE
}
static void
@@ -274,7 +273,7 @@ SetOracle::doApply()
auto const newCount = pairs.size() > 5 ? 2 : 1;
auto const adjust = newCount - oldCount;
if (adjust != 0 && !adjustOwnerCount(ctx_, adjust))
return tefINTERNAL;
return tefINTERNAL; // LCOV_EXCL_LINE
ctx_.view().update(sle);
}
@@ -295,13 +294,13 @@ SetOracle::doApply()
auto page = ctx_.view().dirInsert(
keylet::ownerDir(account_), sle->key(), describeOwnerDir(account_));
if (!page)
return tecDIR_FULL;
return tecDIR_FULL; // LCOV_EXCL_LINE
(*sle)[sfOwnerNode] = *page;
auto const count = series.size() > 5 ? 2 : 1;
if (!adjustOwnerCount(ctx_, count))
return tefINTERNAL;
return tefINTERNAL; // LCOV_EXCL_LINE
ctx_.view().insert(sle);
}

View File

@@ -85,10 +85,11 @@ iteratePriceData(
auto const ledger = context.ledgerMaster.getLedgerBySeq(prevSeq);
if (!ledger)
return;
return; // LCOV_EXCL_LINE
meta = ledger->txRead(prevTx).second;
prevChain = chain;
for (STObject const& node : meta->getFieldArray(sfAffectedNodes))
{
if (node.getFieldU16(sfLedgerEntryType) != ltORACLE)
@@ -96,7 +97,6 @@ iteratePriceData(
continue;
}
prevChain = chain;
chain = &node;
isNew = node.isFieldPresent(sfNewFields);
// if a meta is for the new and this is the first
@@ -170,21 +170,49 @@ doGetAggregatePrice(RPC::JsonContext& context)
if (!params.isMember(jss::quote_asset))
return RPC::missing_field_error(jss::quote_asset);
// Lambda to validate uint type
// support positive int, uint, and a number represented as a string
auto validUInt = [](Json::Value const& params,
Json::StaticString const& field) {
auto const& jv = params[field];
std::uint32_t v;
return jv.isUInt() || (jv.isInt() && jv.asInt() >= 0) ||
(jv.isString() && beast::lexicalCastChecked(v, jv.asString()));
};
// Lambda to get `trim` and `time_threshold` fields. If the field
// is not included in the input then a default value is returned.
auto getField = [&params](
auto getField = [&params, &validUInt](
Json::StaticString const& field,
unsigned int def =
0) -> std::variant<std::uint32_t, error_code_i> {
if (params.isMember(field))
{
if (!params[field].isConvertibleTo(Json::ValueType::uintValue))
return rpcORACLE_MALFORMED;
if (!validUInt(params, field))
return rpcINVALID_PARAMS;
return params[field].asUInt();
}
return def;
};
// Lambda to get `base_asset` and `quote_asset`. The values have
// to conform to the Currency type.
auto getCurrency =
[&params](SField const& sField, Json::StaticString const& field)
-> std::variant<Json::Value, error_code_i> {
try
{
if (params[field].asString().empty())
return rpcINVALID_PARAMS;
currencyFromJson(sField, params[field]);
return params[field];
}
catch (...)
{
return rpcINVALID_PARAMS;
}
};
auto const trim = getField(jss::trim);
if (std::holds_alternative<error_code_i>(trim))
{
@@ -206,8 +234,18 @@ doGetAggregatePrice(RPC::JsonContext& context)
return result;
}
auto const& baseAsset = params[jss::base_asset];
auto const& quoteAsset = params[jss::quote_asset];
auto const baseAsset = getCurrency(sfBaseAsset, jss::base_asset);
if (std::holds_alternative<error_code_i>(baseAsset))
{
RPC::inject_error(std::get<error_code_i>(baseAsset), result);
return result;
}
auto const quoteAsset = getCurrency(sfQuoteAsset, jss::quote_asset);
if (std::holds_alternative<error_code_i>(quoteAsset))
{
RPC::inject_error(std::get<error_code_i>(quoteAsset), result);
return result;
}
// Collect the dataset into bimap keyed by lastUpdateTime and
// STAmount (Number is int64 and price is uint64)
@@ -220,8 +258,7 @@ doGetAggregatePrice(RPC::JsonContext& context)
RPC::inject_error(rpcORACLE_MALFORMED, result);
return result;
}
auto const documentID = oracle[jss::oracle_document_id].isConvertibleTo(
Json::ValueType::uintValue)
auto const documentID = validUInt(oracle, jss::oracle_document_id)
? std::make_optional(oracle[jss::oracle_document_id].asUInt())
: std::nullopt;
auto const account =
@@ -235,7 +272,7 @@ doGetAggregatePrice(RPC::JsonContext& context)
std::shared_ptr<ReadView const> ledger;
result = RPC::lookupLedger(ledger, context);
if (!ledger)
return result;
return result; // LCOV_EXCL_LINE
auto const sle = ledger->read(keylet::oracle(*account, *documentID));
iteratePriceData(context, sle, [&](STObject const& node) {
@@ -246,9 +283,9 @@ doGetAggregatePrice(RPC::JsonContext& context)
series.end(),
[&](STObject const& o) -> bool {
return o.getFieldCurrency(sfBaseAsset).getText() ==
baseAsset &&
std::get<Json::Value>(baseAsset) &&
o.getFieldCurrency(sfQuoteAsset).getText() ==
quoteAsset &&
std::get<Json::Value>(quoteAsset) &&
o.isFieldPresent(sfAssetPrice);
});
iter != series.end())
@@ -287,10 +324,15 @@ doGetAggregatePrice(RPC::JsonContext& context)
prices.left.erase(
prices.left.upper_bound(upperBound), prices.left.end());
// At least one element should remain since upperBound is either
// equal to oldestTime or is less than latestTime, in which case
// the data is deleted between the oldestTime and upperBound.
if (prices.empty())
{
RPC::inject_error(rpcOBJECT_NOT_FOUND, result);
// LCOV_EXCL_START
RPC::inject_error(rpcINTERNAL, result);
return result;
// LCOV_EXCL_STOP
}
}
result[jss::time] = latestTime;

View File

@@ -624,7 +624,7 @@ doLedgerEntry(RPC::JsonContext& context)
auto const& oracle = context.params[jss::oracle];
auto const documentID = [&]() -> std::optional<std::uint32_t> {
auto const& id = oracle[jss::oracle_document_id];
if (id.isConvertibleTo(Json::ValueType::uintValue))
if (id.isUInt() || (id.isInt() && id.asInt() >= 0))
return std::make_optional(id.asUInt());
else if (id.isString())
{

View File

@@ -163,7 +163,7 @@ private:
env.fund(XRP(1'000), owner);
Oracle oracle(env, {.owner = owner}, false);
// Symbol class or provider not included on create
// Asset class or provider not included on create
oracle.set(CreateArg{
.assetClass = std::nullopt,
.provider = "provider",
@@ -174,7 +174,7 @@ private:
.uri = "URI",
.err = ter(temMALFORMED)});
// Symbol class or provider are included on update
// Asset class or provider are included on update
// and don't match the current values
oracle.set(CreateArg{});
BEAST_EXPECT(oracle.exists());
@@ -194,7 +194,7 @@ private:
Oracle oracle(env, {.owner = owner}, false);
// Fields too long
// Symbol class
// Asset class
std::string assetClass(17, '0');
oracle.set(
CreateArg{.assetClass = assetClass, .err = ter(temMALFORMED)});
@@ -203,6 +203,13 @@ private:
oracle.set(CreateArg{.provider = large, .err = ter(temMALFORMED)});
// URI
oracle.set(CreateArg{.uri = large, .err = ter(temMALFORMED)});
// Empty field
// Asset class
oracle.set(CreateArg{.assetClass = "", .err = ter(temMALFORMED)});
// provider
oracle.set(CreateArg{.provider = "", .err = ter(temMALFORMED)});
// URI
oracle.set(CreateArg{.uri = "", .err = ter(temMALFORMED)});
}
{
@@ -224,6 +231,12 @@ private:
// Invalid update time
using namespace std::chrono;
Env env(*this);
auto closeTime = [&]() {
return duration_cast<seconds>(
env.current()->info().closeTime.time_since_epoch() -
10'000s)
.count();
};
env.fund(XRP(1'000), owner);
Oracle oracle(env, {.owner = owner});
BEAST_EXPECT(oracle.exists());
@@ -231,20 +244,25 @@ private:
// Less than the last close time - 300s
oracle.set(UpdateArg{
.series = {{"XRP", "USD", 740, 1}},
.lastUpdateTime = testStartTime.count() + 400 - 301,
.lastUpdateTime = static_cast<std::uint32_t>(closeTime() - 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,
.lastUpdateTime = static_cast<std::uint32_t>(closeTime() + 311),
.err = ter(tecINVALID_UPDATE_TIME)});
oracle.set(UpdateArg{.series = {{"XRP", "USD", 740, 1}}});
BEAST_EXPECT(
oracle.expectLastUpdateTime(testStartTime.count() + 450));
BEAST_EXPECT(oracle.expectLastUpdateTime(
static_cast<std::uint32_t>(testStartTime.count() + 450)));
// Less than the previous lastUpdateTime
oracle.set(UpdateArg{
.series = {{"XRP", "USD", 740, 1}},
.lastUpdateTime = testStartTime.count() + 449,
.lastUpdateTime = static_cast<std::uint32_t>(449),
.err = ter(tecINVALID_UPDATE_TIME)});
// Less than the epoch time
oracle.set(UpdateArg{
.series = {{"XRP", "USD", 740, 1}},
.lastUpdateTime = static_cast<int>(epoch_offset.count() - 1),
.err = ter(tecINVALID_UPDATE_TIME)});
}
@@ -284,6 +302,38 @@ private:
.series = {{"USD", "BTC", 740, maxPriceScale + 1}},
.err = ter(temMALFORMED)});
}
{
// Updating token pair to add and delete
Env env(*this);
env.fund(XRP(1'000), owner);
Oracle oracle(env, {.owner = owner});
oracle.set(UpdateArg{
.series =
{{"XRP", "EUR", std::nullopt, std::nullopt},
{"XRP", "EUR", 740, 1}},
.err = ter(temMALFORMED)});
// Delete token pair that doesn't exist in this oracle
oracle.set(UpdateArg{
.series = {{"XRP", "EUR", std::nullopt, std::nullopt}},
.err = ter(tecTOKEN_PAIR_NOT_FOUND)});
// Delete token pair in oracle, which is not in the ledger
oracle.set(UpdateArg{
.documentID = 10,
.series = {{"XRP", "EUR", std::nullopt, std::nullopt}},
.err = ter(temMALFORMED)});
}
{
// Bad fee
Env env(*this);
env.fund(XRP(1'000), owner);
Oracle oracle(
env, {.owner = owner, .fee = -1, .err = ter(temBAD_FEE)});
Oracle oracle1(env, {.owner = owner});
oracle.set(
UpdateArg{.owner = owner, .fee = -1, .err = ter(temBAD_FEE)});
}
}
void
@@ -356,13 +406,19 @@ private:
{.owner = bad, .seq = seq(1), .err = ter(terNO_ACCOUNT)});
}
// Invalid Sequence
// Invalid DocumentID
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)});
// Invalid flags
oracle.remove({.flags = tfSellNFToken, .err = ter(temINVALID_FLAG)});
// Bad fee
oracle.remove({.fee = -1, .err = ter(temBAD_FEE)});
}
void
@@ -622,50 +678,6 @@ private:
}
}
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
@@ -683,7 +695,6 @@ public:
all - featureMultiSignReserve - featureExpandedSignerList,
all - featureExpandedSignerList})
testMultisig(features);
testLedgerEntry();
}
};

View File

@@ -28,6 +28,28 @@ namespace test {
namespace jtx {
namespace oracle {
using AnyValue = std::variant<std::string, double, Json::Int, Json::UInt>;
using OraclesData =
std::vector<std::pair<std::optional<Account>, std::optional<AnyValue>>>;
// Special string value, which is converted to unquoted string in the string
// passed to rpc.
constexpr char const* NoneTag = "%None%";
constexpr char const* UnquotedNone = "None";
constexpr char const* NonePattern = "\"%None%\"";
std::uint32_t
asUInt(AnyValue const& v);
bool
validDocumentID(AnyValue const& v);
void
toJson(Json::Value& jv, AnyValue const& v);
void
toJsonHex(Json::Value& jv, AnyValue const& v);
// base asset, quote asset, price, scale
using DataSeries = std::vector<std::tuple<
std::string,
@@ -39,16 +61,16 @@ using DataSeries = std::vector<std::tuple<
struct CreateArg
{
std::optional<AccountID> owner = std::nullopt;
std::optional<std::uint32_t> documentID = 1;
std::optional<AnyValue> 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::optional<AnyValue> assetClass = "currency";
std::optional<AnyValue> provider = "provider";
std::optional<AnyValue> uri = "URI";
std::optional<AnyValue> 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;
int fee = 10;
std::optional<ter> err = std::nullopt;
bool close = false;
};
@@ -57,26 +79,27 @@ struct CreateArg
struct UpdateArg
{
std::optional<AccountID> owner = std::nullopt;
std::optional<std::uint32_t> documentID = std::nullopt;
std::optional<AnyValue> 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::optional<AnyValue> assetClass = std::nullopt;
std::optional<AnyValue> provider = std::nullopt;
std::optional<AnyValue> uri = "URI";
std::optional<AnyValue> 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;
int 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<AnyValue> const& documentID = std::nullopt;
std::uint32_t flags = 0;
std::optional<jtx::msig> const& msig = std::nullopt;
std::optional<jtx::seq> seq = std::nullopt;
std::uint32_t fee = 10;
int fee = 10;
std::optional<ter> const& err = std::nullopt;
};
@@ -123,12 +146,11 @@ public:
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::optional<AnyValue> const& baseAsset,
std::optional<AnyValue> const& quoteAsset,
std::optional<OraclesData> const& oracles = std::nullopt,
std::optional<AnyValue> const& trim = std::nullopt,
std::optional<AnyValue> const& timeTreshold = std::nullopt);
std::uint32_t
documentID() const
@@ -154,8 +176,8 @@ public:
static Json::Value
ledgerEntry(
Env& env,
AccountID const& account,
std::variant<std::uint32_t, std::string> const& documentID,
std::optional<std::variant<AccountID, std::string>> const& account,
std::optional<AnyValue> const& documentID,
std::optional<std::string> const& index = std::nullopt);
Json::Value

View File

@@ -22,6 +22,7 @@
#include <test/jtx/Oracle.h>
#include <boost/lexical_cast/try_lexical_convert.hpp>
#include <boost/regex.hpp>
#include <vector>
@@ -43,8 +44,8 @@ Oracle::Oracle(Env& env, CreateArg const& arg, bool submit)
env_.close(now + testStartTime - epoch_offset);
if (arg.owner)
owner_ = *arg.owner;
if (arg.documentID)
documentID_ = *arg.documentID;
if (arg.documentID && validDocumentID(*arg.documentID))
documentID_ = asUInt(*arg.documentID);
if (submit)
set(arg);
}
@@ -55,13 +56,15 @@ 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_);
toJson(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());
if (arg.flags != 0)
jv[jss::Flags] = arg.flags;
submit(jv, arg.msig, arg.seq, arg.err);
}
@@ -142,12 +145,11 @@ Oracle::expectLastUpdateTime(std::uint32_t lastUpdateTime) const
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)
std::optional<AnyValue> const& baseAsset,
std::optional<AnyValue> const& quoteAsset,
std::optional<OraclesData> const& oracles,
std::optional<AnyValue> const& trim,
std::optional<AnyValue> const& timeThreshold)
{
Json::Value jv;
Json::Value jvOracles(Json::arrayValue);
@@ -156,26 +158,34 @@ Oracle::aggregatePrice(
for (auto const& id : *oracles)
{
Json::Value oracle;
oracle[jss::account] = to_string(id.first.id());
oracle[jss::oracle_document_id] = id.second;
if (id.first)
oracle[jss::account] = to_string((*id.first).id());
if (id.second)
toJson(oracle[jss::oracle_document_id], *id.second);
jvOracles.append(oracle);
}
jv[jss::oracles] = jvOracles;
}
if (trim)
jv[jss::trim] = *trim;
toJson(jv[jss::trim], *trim);
if (baseAsset)
jv[jss::base_asset] = *baseAsset;
toJson(jv[jss::base_asset], *baseAsset);
if (quoteAsset)
jv[jss::quote_asset] = *quoteAsset;
toJson(jv[jss::quote_asset], *quoteAsset);
if (timeThreshold)
jv[jss::time_threshold] = *timeThreshold;
toJson(jv[jss::time_threshold], *timeThreshold);
// Convert "%None%" to None
auto str = to_string(jv);
str = boost::regex_replace(str, boost::regex(NonePattern), UnquotedNone);
auto jr = env.rpc("json", "get_aggregate_price", str);
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];
if (jr.isObject())
{
if (jr.isMember(jss::result) && jr[jss::result].isMember(jss::status))
return jr[jss::result];
else if (jr.isMember(jss::error))
return jr;
}
return Json::nullValue;
}
@@ -186,17 +196,24 @@ Oracle::set(UpdateArg const& arg)
Json::Value jv;
if (arg.owner)
owner_ = *arg.owner;
if (arg.documentID)
documentID_ = *arg.documentID;
if (arg.documentID &&
std::holds_alternative<std::uint32_t>(*arg.documentID))
{
documentID_ = std::get<std::uint32_t>(*arg.documentID);
jv[jss::OracleDocumentID] = documentID_;
}
else if (arg.documentID)
toJson(jv[jss::OracleDocumentID], *arg.documentID);
else
jv[jss::OracleDocumentID] = 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);
toJsonHex(jv[jss::AssetClass], *arg.assetClass);
if (arg.provider)
jv[jss::Provider] = strHex(*arg.provider);
toJsonHex(jv[jss::Provider], *arg.provider);
if (arg.uri)
jv[jss::URI] = strHex(*arg.uri);
toJsonHex(jv[jss::URI], *arg.uri);
if (arg.flags != 0)
jv[jss::Flags] = arg.flags;
if (Oracle::fee != 0)
@@ -207,8 +224,14 @@ Oracle::set(UpdateArg const& arg)
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);
{
if (std::holds_alternative<std::uint32_t>(*arg.lastUpdateTime))
jv[jss::LastUpdateTime] = to_string(
testStartTime.count() +
std::get<std::uint32_t>(*arg.lastUpdateTime));
else
toJson(jv[jss::LastUpdateTime], *arg.lastUpdateTime);
}
else
jv[jss::LastUpdateTime] = to_string(
duration_cast<seconds>(
@@ -263,18 +286,22 @@ Oracle::set(CreateArg const& arg)
Json::Value
Oracle::ledgerEntry(
Env& env,
AccountID const& account,
std::variant<std::uint32_t, std::string> const& documentID,
std::optional<std::variant<AccountID, std::string>> const& account,
std::optional<AnyValue> 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 (account)
{
if (std::holds_alternative<AccountID>(*account))
jvParams[jss::oracle][jss::account] =
to_string(std::get<AccountID>(*account));
else
jvParams[jss::oracle][jss::account] =
std::get<std::string>(*account);
}
if (documentID)
toJson(jvParams[jss::oracle][jss::oracle_document_id], *documentID);
if (index)
{
std::uint32_t i;
@@ -283,7 +310,68 @@ Oracle::ledgerEntry(
else
jvParams[jss::oracle][jss::ledger_index] = *index;
}
return env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
// Convert "%None%" to None
auto str = to_string(jvParams);
str = boost::regex_replace(str, boost::regex(NonePattern), UnquotedNone);
auto jr = env.rpc("json", "ledger_entry", str);
if (jr.isObject())
{
if (jr.isMember(jss::result) && jr[jss::result].isMember(jss::status))
return jr[jss::result];
else if (jr.isMember(jss::error))
return jr;
}
return Json::nullValue;
}
void
toJson(Json::Value& jv, AnyValue const& v)
{
std::visit([&](auto&& arg) { jv = arg; }, v);
}
void
toJsonHex(Json::Value& jv, AnyValue const& v)
{
std::visit(
[&]<typename T>(T&& arg) {
if constexpr (std::is_same_v<T, std::string const&>)
{
if (arg.starts_with("##"))
jv = arg.substr(2);
else
jv = strHex(arg);
}
else
jv = arg;
},
v);
}
std::uint32_t
asUInt(AnyValue const& v)
{
Json::Value jv;
toJson(jv, v);
return jv.asUInt();
}
bool
validDocumentID(AnyValue const& v)
{
try
{
Json::Value jv;
toJson(jv, v);
jv.asUInt();
jv.isNumeric();
return true;
}
catch (...)
{
}
return false;
}
} // namespace oracle

View File

@@ -35,10 +35,9 @@ public:
{
testcase("Errors");
using namespace jtx;
using Oracles = std::vector<std::pair<Account, std::uint32_t>>;
Account const owner{"owner"};
Account const some{"some"};
static Oracles oracles = {{owner, 1}};
static OraclesData oracles = {{owner, 1}};
{
Env env(*this);
@@ -55,6 +54,32 @@ public:
ret[jss::error_message].asString() ==
"Missing field 'quote_asset'.");
// invalid base_asset, quote_asset
std::vector<AnyValue> invalidAsset = {
NoneTag,
1,
-1,
1.2,
"",
"invalid",
"a",
"ab",
"A",
"AB",
"ABCD",
"010101",
"012345678901234567890123456789012345678",
"012345678901234567890123456789012345678G"};
for (auto const& v : invalidAsset)
{
ret = Oracle::aggregatePrice(env, "USD", v, oracles);
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
ret = Oracle::aggregatePrice(env, v, "USD", oracles);
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
ret = Oracle::aggregatePrice(env, v, v, oracles);
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
}
// missing oracles array
ret = Oracle::aggregatePrice(env, "XRP", "USD");
BEAST_EXPECT(
@@ -62,16 +87,39 @@ public:
"Missing field 'oracles'.");
// empty oracles array
ret = Oracle::aggregatePrice(env, "XRP", "USD", Oracles{});
ret = Oracle::aggregatePrice(env, "XRP", "USD", OraclesData{});
BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed");
// no token pairs found
ret = Oracle::aggregatePrice(env, "YAN", "USD", oracles);
BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
// invalid oracle document id
// id doesn't exist
ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{owner, 2}}});
BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
// invalid values
std::vector<AnyValue> invalidDocument = {
NoneTag, 1.2, -1, "", "none", "1.2"};
for (auto const& v : invalidDocument)
{
ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{owner, v}}});
Json::Value jv;
toJson(jv, v);
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
}
// missing document id
ret = Oracle::aggregatePrice(
env, "XRP", "USD", {{{owner, std::nullopt}}});
BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed");
// invalid owner
ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{some, 1}}});
BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
// missing account
ret = Oracle::aggregatePrice(
env, "XRP", "USD", {{{std::nullopt, 1}}});
BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed");
// oracles have wrong asset pair
env.fund(XRP(1'000), owner);
@@ -82,18 +130,35 @@ public:
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");
std::vector<AnyValue> invalidTrim = {
NoneTag, 0, 26, -1, 1.2, "", "none", "1.2"};
for (auto const& v : invalidTrim)
{
ret = Oracle::aggregatePrice(
env, "XRP", "USD", {{{owner, oracle.documentID()}}}, v);
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
}
// invalid time threshold value
std::vector<AnyValue> invalidTime = {
NoneTag, -1, 1.2, "", "none", "1.2"};
for (auto const& v : invalidTime)
{
ret = Oracle::aggregatePrice(
env,
"XRP",
"USD",
{{{owner, oracle.documentID()}}},
std::nullopt,
v);
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
}
}
// too many oracles
{
Env env(*this);
std::vector<std::pair<Account, std::uint32_t>> oracles;
OraclesData oracles;
for (int i = 0; i < 201; ++i)
{
Account const owner(std::to_string(i));
@@ -132,7 +197,7 @@ public:
// or time threshold
{
Env env(*this);
std::vector<std::pair<Account, std::uint32_t>> oracles;
OraclesData oracles;
prep(env, oracles);
// entire and trimmed stats
auto ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles);
@@ -148,7 +213,7 @@ public:
// Aggregate data set includes all price oracle instances
{
Env env(*this);
std::vector<std::pair<Account, std::uint32_t>> oracles;
OraclesData oracles;
prep(env, oracles);
// entire and trimmed stats
auto ret =
@@ -171,14 +236,14 @@ public:
// updated ledgers
{
Env env(*this);
std::vector<std::pair<Account, std::uint32_t>> oracles;
OraclesData oracles;
prep(env, oracles);
for (int i = 0; i < 3; ++i)
{
Oracle oracle(
env,
{.owner = oracles[i].first,
.documentID = oracles[i].second},
.documentID = asUInt(*oracles[i].second)},
false);
// push XRP/USD by more than three ledgers, so this price
// oracle is not included in the dataset
@@ -191,7 +256,7 @@ public:
Oracle oracle(
env,
{.owner = oracles[i].first,
.documentID = oracles[i].second},
.documentID = asUInt(*oracles[i].second)},
false);
// push XRP/USD by two ledgers, so this price
// is included in the dataset
@@ -201,7 +266,7 @@ public:
// entire and trimmed stats
auto ret =
Oracle::aggregatePrice(env, "XRP", "USD", oracles, 20, 200);
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(
@@ -219,14 +284,14 @@ public:
// Reduced data set because of the time threshold
{
Env env(*this);
std::vector<std::pair<Account, std::uint32_t>> oracles;
OraclesData oracles;
prep(env, oracles);
for (int i = 0; i < oracles.size(); ++i)
{
Oracle oracle(
env,
{.owner = oracles[i].first,
.documentID = oracles[i].second},
.documentID = asUInt(*oracles[i].second)},
false);
// push XRP/USD by two ledgers, so this price
// is included in the dataset

View File

@@ -26,6 +26,7 @@
#include <ripple/protocol/STXChainBridge.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
#include <test/jtx/Oracle.h>
#include <test/jtx/attester.h>
#include <test/jtx/multisign.h>
#include <test/jtx/xchain_bridge.h>
@@ -2279,6 +2280,87 @@ class LedgerRPC_test : public beast::unit_test::suite
}
}
void
testInvalidOracleLedgerEntry()
{
testcase("Invalid Oracle Ledger Entry");
using namespace ripple::test::jtx;
using namespace ripple::test::jtx::oracle;
Env env(*this);
Account const owner("owner");
env.fund(XRP(1'000), owner);
Oracle oracle(env, {.owner = owner});
// Malformed document id
auto res = Oracle::ledgerEntry(env, owner, NoneTag);
BEAST_EXPECT(res[jss::error].asString() == "invalidParams");
std::vector<AnyValue> invalid = {-1, 1.2, "", "Invalid"};
for (auto const& v : invalid)
{
auto const res = Oracle::ledgerEntry(env, owner, v);
BEAST_EXPECT(res[jss::error].asString() == "malformedDocumentID");
}
// Missing document id
res = Oracle::ledgerEntry(env, owner, std::nullopt);
BEAST_EXPECT(res[jss::error].asString() == "malformedRequest");
// Missing account
res = Oracle::ledgerEntry(env, std::nullopt, 1);
BEAST_EXPECT(res[jss::error].asString() == "malformedRequest");
// Malformed account
std::string malfAccount = to_string(owner.id());
malfAccount.replace(10, 1, 1, '!');
res = Oracle::ledgerEntry(env, malfAccount, 1);
BEAST_EXPECT(res[jss::error].asString() == "malformedAddress");
}
void
testOracleLedgerEntry()
{
testcase("Oracle Ledger Entry");
using namespace ripple::test::jtx;
using namespace ripple::test::jtx::oracle;
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
@@ -2304,6 +2386,8 @@ public:
testQueue();
testLedgerAccountsOption();
testLedgerEntryDID();
testInvalidOracleLedgerEntry();
testOracleLedgerEntry();
forAllApiVersions(std::bind_front(
&LedgerRPC_test::testLedgerEntryInvalidParams, this));