mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-20 11:05:54 +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
@@ -98,6 +98,7 @@ target_sources (xrpl_core PRIVATE
|
||||
src/ripple/protocol/impl/STArray.cpp
|
||||
src/ripple/protocol/impl/STBase.cpp
|
||||
src/ripple/protocol/impl/STBlob.cpp
|
||||
src/ripple/protocol/impl/STCurrency.cpp
|
||||
src/ripple/protocol/impl/STInteger.cpp
|
||||
src/ripple/protocol/impl/STLedgerEntry.cpp
|
||||
src/ripple/protocol/impl/STObject.cpp
|
||||
@@ -553,6 +554,7 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/app/tx/impl/CreateOffer.cpp
|
||||
src/ripple/app/tx/impl/CreateTicket.cpp
|
||||
src/ripple/app/tx/impl/DeleteAccount.cpp
|
||||
src/ripple/app/tx/impl/DeleteOracle.cpp
|
||||
src/ripple/app/tx/impl/DepositPreauth.cpp
|
||||
src/ripple/app/tx/impl/DID.cpp
|
||||
src/ripple/app/tx/impl/Escrow.cpp
|
||||
@@ -566,6 +568,7 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/app/tx/impl/PayChan.cpp
|
||||
src/ripple/app/tx/impl/Payment.cpp
|
||||
src/ripple/app/tx/impl/SetAccount.cpp
|
||||
src/ripple/app/tx/impl/SetOracle.cpp
|
||||
src/ripple/app/tx/impl/SetRegularKey.cpp
|
||||
src/ripple/app/tx/impl/SetSignerList.cpp
|
||||
src/ripple/app/tx/impl/SetTrust.cpp
|
||||
@@ -721,6 +724,7 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/rpc/handlers/FetchInfo.cpp
|
||||
src/ripple/rpc/handlers/GatewayBalances.cpp
|
||||
src/ripple/rpc/handlers/GetCounts.cpp
|
||||
src/ripple/rpc/handlers/GetAggregatePrice.cpp
|
||||
src/ripple/rpc/handlers/LedgerAccept.cpp
|
||||
src/ripple/rpc/handlers/LedgerCleanerHandler.cpp
|
||||
src/ripple/rpc/handlers/LedgerClosed.cpp
|
||||
@@ -840,6 +844,7 @@ if (tests)
|
||||
src/test/app/NFTokenDir_test.cpp
|
||||
src/test/app/OfferStream_test.cpp
|
||||
src/test/app/Offer_test.cpp
|
||||
src/test/app/Oracle_test.cpp
|
||||
src/test/app/OversizeMeta_test.cpp
|
||||
src/test/app/Path_test.cpp
|
||||
src/test/app/PayChan_test.cpp
|
||||
@@ -964,6 +969,7 @@ if (tests)
|
||||
src/test/jtx/impl/AMMTest.cpp
|
||||
src/test/jtx/impl/Env.cpp
|
||||
src/test/jtx/impl/JSONRPCClient.cpp
|
||||
src/test/jtx/impl/Oracle.cpp
|
||||
src/test/jtx/impl/TestHelpers.cpp
|
||||
src/test/jtx/impl/WSClient.cpp
|
||||
src/test/jtx/impl/acctdelete.cpp
|
||||
@@ -1089,6 +1095,7 @@ if (tests)
|
||||
src/test/rpc/DeliveredAmount_test.cpp
|
||||
src/test/rpc/Feature_test.cpp
|
||||
src/test/rpc/GatewayBalances_test.cpp
|
||||
src/test/rpc/GetAggregatePrice_test.cpp
|
||||
src/test/rpc/GetCounts_test.cpp
|
||||
src/test/rpc/JSONRPC_test.cpp
|
||||
src/test/rpc/KeyGeneration_test.cpp
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <ripple/app/tx/impl/DID.h>
|
||||
#include <ripple/app/tx/impl/DeleteAccount.h>
|
||||
#include <ripple/app/tx/impl/DeleteOracle.h>
|
||||
#include <ripple/app/tx/impl/DepositPreauth.h>
|
||||
#include <ripple/app/tx/impl/SetSignerList.h>
|
||||
#include <ripple/app/tx/impl/details/NFTokenUtils.h>
|
||||
@@ -146,6 +147,18 @@ removeDIDFromLedger(
|
||||
return DIDDelete::deleteSLE(view, sleDel, account, j);
|
||||
}
|
||||
|
||||
TER
|
||||
removeOracleFromLedger(
|
||||
Application&,
|
||||
ApplyView& view,
|
||||
AccountID const& account,
|
||||
uint256 const&,
|
||||
std::shared_ptr<SLE> const& sleDel,
|
||||
beast::Journal j)
|
||||
{
|
||||
return DeleteOracle::deleteOracle(view, sleDel, account, j);
|
||||
}
|
||||
|
||||
// Return nullptr if the LedgerEntryType represents an obligation that can't
|
||||
// be deleted. Otherwise return the pointer to the function that can delete
|
||||
// the non-obligation
|
||||
@@ -166,6 +179,8 @@ nonObligationDeleter(LedgerEntryType t)
|
||||
return removeNFTokenOfferFromLedger;
|
||||
case ltDID:
|
||||
return removeDIDFromLedger;
|
||||
case ltORACLE:
|
||||
return removeOracleFromLedger;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
110
src/ripple/app/tx/impl/DeleteOracle.cpp
Normal file
110
src/ripple/app/tx/impl/DeleteOracle.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/tx/impl/DeleteOracle.h>
|
||||
#include <ripple/ledger/Sandbox.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Rules.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
NotTEC
|
||||
DeleteOracle::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featurePriceOracle))
|
||||
return temDISABLED;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (ctx.tx.getFlags() & tfUniversalMask)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Oracle Delete: invalid flags.";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
TER
|
||||
DeleteOracle::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
if (!ctx.view.exists(keylet::account(ctx.tx.getAccountID(sfAccount))))
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
if (auto const sle = ctx.view.read(keylet::oracle(
|
||||
ctx.tx.getAccountID(sfAccount), ctx.tx[sfOracleDocumentID]));
|
||||
!sle)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Oracle Delete: Oracle does not exist.";
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
else if (ctx.tx.getAccountID(sfAccount) != sle->getAccountID(sfOwner))
|
||||
{
|
||||
// this can't happen because of the above check
|
||||
JLOG(ctx.j.debug()) << "Oracle Delete: invalid account.";
|
||||
return tecINTERNAL;
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
DeleteOracle::deleteOracle(
|
||||
ApplyView& view,
|
||||
std::shared_ptr<SLE> const& sle,
|
||||
AccountID const& account,
|
||||
beast::Journal j)
|
||||
{
|
||||
if (!sle)
|
||||
return tesSUCCESS;
|
||||
|
||||
if (!view.dirRemove(
|
||||
keylet::ownerDir(account), (*sle)[sfOwnerNode], sle->key(), true))
|
||||
{
|
||||
JLOG(j.fatal()) << "Unable to delete Oracle from owner.";
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
auto const sleOwner = view.peek(keylet::account(account));
|
||||
if (!sleOwner)
|
||||
return tecINTERNAL;
|
||||
|
||||
auto const count =
|
||||
sle->getFieldArray(sfPriceDataSeries).size() > 5 ? -2 : -1;
|
||||
|
||||
adjustOwnerCount(view, sleOwner, count, j);
|
||||
|
||||
view.erase(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
DeleteOracle::doApply()
|
||||
{
|
||||
if (auto sle = ctx_.view().peek(
|
||||
keylet::oracle(account_, ctx_.tx[sfOracleDocumentID])))
|
||||
return deleteOracle(ctx_.view(), sle, account_, j_);
|
||||
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
64
src/ripple/app/tx/impl/DeleteOracle.h
Normal file
64
src/ripple/app/tx/impl/DeleteOracle.h
Normal file
@@ -0,0 +1,64 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_DELETEORACLE_H_INCLUDED
|
||||
#define RIPPLE_TX_DELETEORACLE_H_INCLUDED
|
||||
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/**
|
||||
Price Oracle is a system that acts as a bridge between
|
||||
a blockchain network and the external world, providing off-chain price data
|
||||
to decentralized applications (dApps) on the blockchain. This implementation
|
||||
conforms to the requirements specified in the XLS-47d.
|
||||
|
||||
The DeleteOracle transactor implements the deletion of Oracle objects.
|
||||
*/
|
||||
|
||||
class DeleteOracle : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit DeleteOracle(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
static TER
|
||||
deleteOracle(
|
||||
ApplyView& view,
|
||||
std::shared_ptr<SLE> const& sle,
|
||||
AccountID const& account,
|
||||
beast::Journal j);
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif // RIPPLE_TX_DELETEORACLE_H_INCLUDED
|
||||
@@ -392,6 +392,7 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltXCHAIN_OWNED_CLAIM_ID:
|
||||
case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID:
|
||||
case ltDID:
|
||||
case ltORACLE:
|
||||
break;
|
||||
default:
|
||||
invalidTypeAdded_ = true;
|
||||
|
||||
312
src/ripple/app/tx/impl/SetOracle.cpp
Normal file
312
src/ripple/app/tx/impl/SetOracle.cpp
Normal file
@@ -0,0 +1,312 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/tx/impl/SetOracle.h>
|
||||
#include <ripple/basics/UnorderedContainers.h>
|
||||
#include <ripple/ledger/Sandbox.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/InnerObjectFormats.h>
|
||||
#include <ripple/protocol/Rules.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
#include <ripple/protocol/digest.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
static inline std::pair<Currency, Currency>
|
||||
tokenPairKey(STObject const& pair)
|
||||
{
|
||||
return std::make_pair(
|
||||
pair.getFieldCurrency(sfBaseAsset).currency(),
|
||||
pair.getFieldCurrency(sfQuoteAsset).currency());
|
||||
}
|
||||
|
||||
NotTEC
|
||||
SetOracle::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featurePriceOracle))
|
||||
return temDISABLED;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (ctx.tx.getFlags() & tfUniversalMask)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
auto const& dataSeries = ctx.tx.getFieldArray(sfPriceDataSeries);
|
||||
if (dataSeries.empty())
|
||||
return temARRAY_EMPTY;
|
||||
if (dataSeries.size() > maxOracleDataSeries)
|
||||
return temARRAY_TOO_LARGE;
|
||||
|
||||
auto isInvalidLength = [&](auto const& sField, std::size_t length) {
|
||||
return ctx.tx.isFieldPresent(sField) &&
|
||||
(ctx.tx[sField].length() == 0 || ctx.tx[sField].length() > length);
|
||||
};
|
||||
|
||||
if (isInvalidLength(sfProvider, maxOracleProvider) ||
|
||||
isInvalidLength(sfURI, maxOracleURI) ||
|
||||
isInvalidLength(sfAssetClass, maxOracleSymbolClass))
|
||||
return temMALFORMED;
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
TER
|
||||
SetOracle::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const sleSetter =
|
||||
ctx.view.read(keylet::account(ctx.tx.getAccountID(sfAccount)));
|
||||
if (!sleSetter)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
// lastUpdateTime must be within maxLastUpdateTimeDelta seconds
|
||||
// of the last closed ledger
|
||||
using namespace std::chrono;
|
||||
std::size_t const closeTime =
|
||||
duration_cast<seconds>(ctx.view.info().closeTime.time_since_epoch())
|
||||
.count();
|
||||
std::size_t const lastUpdateTime = ctx.tx[sfLastUpdateTime];
|
||||
if (lastUpdateTime < epoch_offset.count())
|
||||
return tecINVALID_UPDATE_TIME;
|
||||
std::size_t const lastUpdateTimeEpoch =
|
||||
lastUpdateTime - epoch_offset.count();
|
||||
if (closeTime < maxLastUpdateTimeDelta)
|
||||
Throw<std::runtime_error>(
|
||||
"Oracle: close time is less than maxLastUpdateTimeDelta");
|
||||
if (lastUpdateTimeEpoch < (closeTime - maxLastUpdateTimeDelta) ||
|
||||
lastUpdateTimeEpoch > (closeTime + maxLastUpdateTimeDelta))
|
||||
return tecINVALID_UPDATE_TIME;
|
||||
|
||||
auto const sle = ctx.view.read(keylet::oracle(
|
||||
ctx.tx.getAccountID(sfAccount), ctx.tx[sfOracleDocumentID]));
|
||||
|
||||
// token pairs to add/update
|
||||
hash_set<std::pair<Currency, Currency>> pairs;
|
||||
// token pairs to delete. if a token pair doesn't include
|
||||
// the price then this pair should be deleted from the object.
|
||||
hash_set<std::pair<Currency, Currency>> pairsDel;
|
||||
for (auto const& entry : ctx.tx.getFieldArray(sfPriceDataSeries))
|
||||
{
|
||||
if (entry[sfBaseAsset] == entry[sfQuoteAsset])
|
||||
return temMALFORMED;
|
||||
auto const key = tokenPairKey(entry);
|
||||
if (pairs.contains(key) || pairsDel.contains(key))
|
||||
return temMALFORMED;
|
||||
if (entry[~sfScale] > maxPriceScale)
|
||||
return temMALFORMED;
|
||||
if (entry.isFieldPresent(sfAssetPrice))
|
||||
pairs.emplace(key);
|
||||
else if (sle)
|
||||
pairsDel.emplace(key);
|
||||
else
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// Lambda is used to check if the value of a field, passed
|
||||
// in the transaction, is equal to the value of that field
|
||||
// in the on-ledger object.
|
||||
auto isConsistent = [&ctx, &sle](auto const& field) {
|
||||
auto const v = ctx.tx[~field];
|
||||
return !v || *v == (*sle)[field];
|
||||
};
|
||||
|
||||
std::uint32_t adjustReserve = 0;
|
||||
if (sle)
|
||||
{
|
||||
// update
|
||||
// Account is the Owner since we can get sle
|
||||
|
||||
// lastUpdateTime must be more recent than the previous one
|
||||
if (ctx.tx[sfLastUpdateTime] <= (*sle)[sfLastUpdateTime])
|
||||
return tecINVALID_UPDATE_TIME;
|
||||
|
||||
if (!isConsistent(sfProvider) || !isConsistent(sfAssetClass))
|
||||
return temMALFORMED;
|
||||
|
||||
for (auto const& entry : sle->getFieldArray(sfPriceDataSeries))
|
||||
{
|
||||
auto const key = tokenPairKey(entry);
|
||||
if (!pairs.contains(key))
|
||||
{
|
||||
if (pairsDel.contains(key))
|
||||
pairsDel.erase(key);
|
||||
else
|
||||
pairs.emplace(key);
|
||||
}
|
||||
}
|
||||
if (!pairsDel.empty())
|
||||
return tecTOKEN_PAIR_NOT_FOUND;
|
||||
|
||||
auto const oldCount =
|
||||
sle->getFieldArray(sfPriceDataSeries).size() > 5 ? 2 : 1;
|
||||
auto const newCount = pairs.size() > 5 ? 2 : 1;
|
||||
adjustReserve = newCount - oldCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
// create
|
||||
|
||||
if (!ctx.tx.isFieldPresent(sfProvider) ||
|
||||
!ctx.tx.isFieldPresent(sfAssetClass))
|
||||
return temMALFORMED;
|
||||
adjustReserve = pairs.size() > 5 ? 2 : 1;
|
||||
}
|
||||
|
||||
if (pairs.empty())
|
||||
return tecARRAY_EMPTY;
|
||||
if (pairs.size() > maxOracleDataSeries)
|
||||
return tecARRAY_TOO_LARGE;
|
||||
|
||||
auto const reserve = ctx.view.fees().accountReserve(
|
||||
sleSetter->getFieldU32(sfOwnerCount) + adjustReserve);
|
||||
auto const& balance = sleSetter->getFieldAmount(sfBalance);
|
||||
|
||||
if (balance < reserve)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
static bool
|
||||
adjustOwnerCount(ApplyContext& ctx, int count)
|
||||
{
|
||||
if (auto const sleAccount =
|
||||
ctx.view().peek(keylet::account(ctx.tx[sfAccount])))
|
||||
{
|
||||
adjustOwnerCount(ctx.view(), sleAccount, count, ctx.journal);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void
|
||||
setPriceDataInnerObjTemplate(STObject& obj)
|
||||
{
|
||||
if (SOTemplate const* elements =
|
||||
InnerObjectFormats::getInstance().findSOTemplateBySField(
|
||||
sfPriceData))
|
||||
obj.set(*elements);
|
||||
}
|
||||
|
||||
TER
|
||||
SetOracle::doApply()
|
||||
{
|
||||
auto const oracleID = keylet::oracle(account_, ctx_.tx[sfOracleDocumentID]);
|
||||
|
||||
if (auto sle = ctx_.view().peek(oracleID))
|
||||
{
|
||||
// update
|
||||
// the token pair that doesn't have their price updated will not
|
||||
// include neither price nor scale in the updated PriceDataSeries
|
||||
|
||||
hash_map<std::pair<Currency, Currency>, STObject> pairs;
|
||||
// collect current token pairs
|
||||
for (auto const& entry : sle->getFieldArray(sfPriceDataSeries))
|
||||
{
|
||||
STObject priceData{sfPriceData};
|
||||
setPriceDataInnerObjTemplate(priceData);
|
||||
priceData.setFieldCurrency(
|
||||
sfBaseAsset, entry.getFieldCurrency(sfBaseAsset));
|
||||
priceData.setFieldCurrency(
|
||||
sfQuoteAsset, entry.getFieldCurrency(sfQuoteAsset));
|
||||
pairs.emplace(tokenPairKey(entry), std::move(priceData));
|
||||
}
|
||||
auto const oldCount = pairs.size() > 5 ? 2 : 1;
|
||||
// update/add/delete pairs
|
||||
for (auto const& entry : ctx_.tx.getFieldArray(sfPriceDataSeries))
|
||||
{
|
||||
auto const key = tokenPairKey(entry);
|
||||
if (!entry.isFieldPresent(sfAssetPrice))
|
||||
{
|
||||
// delete token pair
|
||||
pairs.erase(key);
|
||||
}
|
||||
else if (auto iter = pairs.find(key); iter != pairs.end())
|
||||
{
|
||||
// update the price
|
||||
iter->second.setFieldU64(
|
||||
sfAssetPrice, entry.getFieldU64(sfAssetPrice));
|
||||
if (entry.isFieldPresent(sfScale))
|
||||
iter->second.setFieldU8(sfScale, entry.getFieldU8(sfScale));
|
||||
}
|
||||
else
|
||||
{
|
||||
// add a token pair with the price
|
||||
STObject priceData{sfPriceData};
|
||||
setPriceDataInnerObjTemplate(priceData);
|
||||
priceData.setFieldCurrency(
|
||||
sfBaseAsset, entry.getFieldCurrency(sfBaseAsset));
|
||||
priceData.setFieldCurrency(
|
||||
sfQuoteAsset, entry.getFieldCurrency(sfQuoteAsset));
|
||||
priceData.setFieldU64(
|
||||
sfAssetPrice, entry.getFieldU64(sfAssetPrice));
|
||||
if (entry.isFieldPresent(sfScale))
|
||||
priceData.setFieldU8(sfScale, entry.getFieldU8(sfScale));
|
||||
pairs.emplace(key, std::move(priceData));
|
||||
}
|
||||
}
|
||||
STArray updatedSeries;
|
||||
for (auto const& iter : pairs)
|
||||
updatedSeries.push_back(std::move(iter.second));
|
||||
sle->setFieldArray(sfPriceDataSeries, updatedSeries);
|
||||
if (ctx_.tx.isFieldPresent(sfURI))
|
||||
sle->setFieldVL(sfURI, ctx_.tx[sfURI]);
|
||||
sle->setFieldU32(sfLastUpdateTime, ctx_.tx[sfLastUpdateTime]);
|
||||
|
||||
auto const newCount = pairs.size() > 5 ? 2 : 1;
|
||||
auto const adjust = newCount - oldCount;
|
||||
if (adjust != 0 && !adjustOwnerCount(ctx_, adjust))
|
||||
return tefINTERNAL;
|
||||
|
||||
ctx_.view().update(sle);
|
||||
}
|
||||
else
|
||||
{
|
||||
// create
|
||||
|
||||
sle = std::make_shared<SLE>(oracleID);
|
||||
sle->setAccountID(sfOwner, ctx_.tx.getAccountID(sfAccount));
|
||||
sle->setFieldVL(sfProvider, ctx_.tx[sfProvider]);
|
||||
if (ctx_.tx.isFieldPresent(sfURI))
|
||||
sle->setFieldVL(sfURI, ctx_.tx[sfURI]);
|
||||
auto const& series = ctx_.tx.getFieldArray(sfPriceDataSeries);
|
||||
sle->setFieldArray(sfPriceDataSeries, series);
|
||||
sle->setFieldVL(sfAssetClass, ctx_.tx[sfAssetClass]);
|
||||
sle->setFieldU32(sfLastUpdateTime, ctx_.tx[sfLastUpdateTime]);
|
||||
|
||||
auto page = ctx_.view().dirInsert(
|
||||
keylet::ownerDir(account_), sle->key(), describeOwnerDir(account_));
|
||||
if (!page)
|
||||
return tecDIR_FULL;
|
||||
|
||||
(*sle)[sfOwnerNode] = *page;
|
||||
|
||||
auto const count = series.size() > 5 ? 2 : 1;
|
||||
if (!adjustOwnerCount(ctx_, count))
|
||||
return tefINTERNAL;
|
||||
|
||||
ctx_.view().insert(sle);
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
57
src/ripple/app/tx/impl/SetOracle.h
Normal file
57
src/ripple/app/tx/impl/SetOracle.h
Normal file
@@ -0,0 +1,57 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_SETORACLE_H_INCLUDED
|
||||
#define RIPPLE_TX_SETORACLE_H_INCLUDED
|
||||
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/**
|
||||
Price Oracle is a system that acts as a bridge between
|
||||
a blockchain network and the external world, providing off-chain price data
|
||||
to decentralized applications (dApps) on the blockchain. This implementation
|
||||
conforms to the requirements specified in the XLS-47d.
|
||||
|
||||
The SetOracle transactor implements creating or updating Oracle objects.
|
||||
*/
|
||||
|
||||
class SetOracle : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit SetOracle(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif // RIPPLE_TX_SETORACLE_H_INCLUDED
|
||||
@@ -35,6 +35,7 @@
|
||||
#include <ripple/app/tx/impl/CreateTicket.h>
|
||||
#include <ripple/app/tx/impl/DID.h>
|
||||
#include <ripple/app/tx/impl/DeleteAccount.h>
|
||||
#include <ripple/app/tx/impl/DeleteOracle.h>
|
||||
#include <ripple/app/tx/impl/DepositPreauth.h>
|
||||
#include <ripple/app/tx/impl/Escrow.h>
|
||||
#include <ripple/app/tx/impl/NFTokenAcceptOffer.h>
|
||||
@@ -45,6 +46,7 @@
|
||||
#include <ripple/app/tx/impl/PayChan.h>
|
||||
#include <ripple/app/tx/impl/Payment.h>
|
||||
#include <ripple/app/tx/impl/SetAccount.h>
|
||||
#include <ripple/app/tx/impl/SetOracle.h>
|
||||
#include <ripple/app/tx/impl/SetRegularKey.h>
|
||||
#include <ripple/app/tx/impl/SetSignerList.h>
|
||||
#include <ripple/app/tx/impl/SetTrust.h>
|
||||
@@ -159,6 +161,10 @@ with_txn_type(TxType txnType, F&& f)
|
||||
return f.template operator()<DIDSet>();
|
||||
case ttDID_DELETE:
|
||||
return f.template operator()<DIDDelete>();
|
||||
case ttORACLE_SET:
|
||||
return f.template operator()<SetOracle>();
|
||||
case ttORACLE_DELETE:
|
||||
return f.template operator()<DeleteOracle>();
|
||||
default:
|
||||
throw UnknownTxnType(txnType);
|
||||
}
|
||||
|
||||
@@ -145,7 +145,11 @@ enum error_code_i {
|
||||
// AMM
|
||||
rpcISSUE_MALFORMED = 93,
|
||||
|
||||
rpcLAST = rpcISSUE_MALFORMED // rpcLAST should always equal the last code.=
|
||||
// Oracle
|
||||
rpcORACLE_MALFORMED = 94,
|
||||
|
||||
rpcLAST =
|
||||
rpcORACLE_MALFORMED // rpcLAST should always equal the last code.=
|
||||
};
|
||||
|
||||
/** Codes returned in the `warnings` array of certain RPC commands.
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace detail {
|
||||
// Feature.cpp. Because it's only used to reserve storage, and determine how
|
||||
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
|
||||
// the actual number of amendments. A LogicError on startup will verify this.
|
||||
static constexpr std::size_t numFeatures = 67;
|
||||
static constexpr std::size_t numFeatures = 68;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -354,6 +354,7 @@ extern uint256 const featureDID;
|
||||
extern uint256 const fixFillOrKill;
|
||||
extern uint256 const fixNFTokenReserve;
|
||||
extern uint256 const fixInnerObjTemplate;
|
||||
extern uint256 const featurePriceOracle;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -283,6 +283,9 @@ xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq);
|
||||
Keylet
|
||||
did(AccountID const& account) noexcept;
|
||||
|
||||
Keylet
|
||||
oracle(AccountID const& account, std::uint32_t const& documentID) noexcept;
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
// Everything below is deprecated and should be removed in favor of keylets:
|
||||
|
||||
@@ -192,6 +192,11 @@ enum LedgerEntryType : std::uint16_t
|
||||
*/
|
||||
ltDID = 0x0049,
|
||||
|
||||
/** A ledger object which tracks Oracle
|
||||
\sa keylet::oracle
|
||||
*/
|
||||
ltORACLE = 0x0080,
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
/** A special type, matching any ledger entry type.
|
||||
|
||||
|
||||
@@ -109,6 +109,31 @@ using TxID = uint256;
|
||||
*/
|
||||
std::uint16_t constexpr maxDeletableAMMTrustLines = 512;
|
||||
|
||||
/** The maximum length of a URI inside an Oracle */
|
||||
std::size_t constexpr maxOracleURI = 256;
|
||||
|
||||
/** The maximum length of a Provider inside an Oracle */
|
||||
std::size_t constexpr maxOracleProvider = 256;
|
||||
|
||||
/** The maximum size of a data series array inside an Oracle */
|
||||
std::size_t constexpr maxOracleDataSeries = 10;
|
||||
|
||||
/** The maximum length of a SymbolClass inside an Oracle */
|
||||
std::size_t constexpr maxOracleSymbolClass = 16;
|
||||
|
||||
/** The maximum allowed time difference between lastUpdateTime and the time
|
||||
of the last closed ledger
|
||||
*/
|
||||
std::size_t constexpr maxLastUpdateTimeDelta = 300;
|
||||
|
||||
/** The maximum price scaling factor
|
||||
*/
|
||||
std::size_t constexpr maxPriceScale = 20;
|
||||
|
||||
/** The maximum percentage of outliers to trim
|
||||
*/
|
||||
std::size_t constexpr maxTrim = 25;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
|
||||
@@ -51,6 +51,7 @@ template <class>
|
||||
class STInteger;
|
||||
class STXChainBridge;
|
||||
class STVector256;
|
||||
class STCurrency;
|
||||
|
||||
#pragma push_macro("XMACRO")
|
||||
#undef XMACRO
|
||||
@@ -85,6 +86,7 @@ class STVector256;
|
||||
STYPE(STI_UINT512, 23) \
|
||||
STYPE(STI_ISSUE, 24) \
|
||||
STYPE(STI_XCHAIN_BRIDGE, 25) \
|
||||
STYPE(STI_CURRENCY, 26) \
|
||||
\
|
||||
/* high-level types */ \
|
||||
/* cannot be serialized inside other types */ \
|
||||
@@ -346,6 +348,7 @@ using SF_UINT512 = TypedField<STBitString<512>>;
|
||||
using SF_ACCOUNT = TypedField<STAccount>;
|
||||
using SF_AMOUNT = TypedField<STAmount>;
|
||||
using SF_ISSUE = TypedField<STIssue>;
|
||||
using SF_CURRENCY = TypedField<STCurrency>;
|
||||
using SF_VL = TypedField<STBlob>;
|
||||
using SF_VECTOR256 = TypedField<STVector256>;
|
||||
using SF_XCHAIN_BRIDGE = TypedField<STXChainBridge>;
|
||||
@@ -364,6 +367,7 @@ extern SF_UINT8 const sfCloseResolution;
|
||||
extern SF_UINT8 const sfMethod;
|
||||
extern SF_UINT8 const sfTransactionResult;
|
||||
extern SF_UINT8 const sfWasLockingChainSend;
|
||||
extern SF_UINT8 const sfScale;
|
||||
|
||||
// 8-bit integers (uncommon)
|
||||
extern SF_UINT8 const sfTickSize;
|
||||
@@ -400,6 +404,7 @@ extern SF_UINT32 const sfTransferRate;
|
||||
extern SF_UINT32 const sfWalletSize;
|
||||
extern SF_UINT32 const sfOwnerCount;
|
||||
extern SF_UINT32 const sfDestinationTag;
|
||||
extern SF_UINT32 const sfLastUpdateTime;
|
||||
|
||||
// 32-bit integers (uncommon)
|
||||
extern SF_UINT32 const sfHighQualityIn;
|
||||
@@ -435,6 +440,7 @@ extern SF_UINT32 const sfHookStateCount;
|
||||
extern SF_UINT32 const sfEmitGeneration;
|
||||
extern SF_UINT32 const sfVoteWeight;
|
||||
extern SF_UINT32 const sfFirstNFTokenSequence;
|
||||
extern SF_UINT32 const sfOracleDocumentID;
|
||||
|
||||
// 64-bit integers (common)
|
||||
extern SF_UINT64 const sfIndexNext;
|
||||
@@ -459,6 +465,7 @@ extern SF_UINT64 const sfReferenceCount;
|
||||
extern SF_UINT64 const sfXChainClaimID;
|
||||
extern SF_UINT64 const sfXChainAccountCreateCount;
|
||||
extern SF_UINT64 const sfXChainAccountClaimCount;
|
||||
extern SF_UINT64 const sfAssetPrice;
|
||||
|
||||
// 128-bit
|
||||
extern SF_UINT128 const sfEmailHash;
|
||||
@@ -554,6 +561,8 @@ extern SF_VL const sfMemoData;
|
||||
extern SF_VL const sfMemoFormat;
|
||||
extern SF_VL const sfDIDDocument;
|
||||
extern SF_VL const sfData;
|
||||
extern SF_VL const sfAssetClass;
|
||||
extern SF_VL const sfProvider;
|
||||
|
||||
// variable length (uncommon)
|
||||
extern SF_VL const sfFulfillment;
|
||||
@@ -590,6 +599,10 @@ extern SF_ACCOUNT const sfIssuingChainDoor;
|
||||
// path set
|
||||
extern SField const sfPaths;
|
||||
|
||||
// currency
|
||||
extern SF_CURRENCY const sfBaseAsset;
|
||||
extern SF_CURRENCY const sfQuoteAsset;
|
||||
|
||||
// issue
|
||||
extern SF_ISSUE const sfAsset;
|
||||
extern SF_ISSUE const sfAsset2;
|
||||
@@ -623,6 +636,7 @@ extern SField const sfHook;
|
||||
extern SField const sfVoteEntry;
|
||||
extern SField const sfAuctionSlot;
|
||||
extern SField const sfAuthAccount;
|
||||
extern SField const sfPriceData;
|
||||
|
||||
extern SField const sfSigner;
|
||||
extern SField const sfMajority;
|
||||
@@ -651,6 +665,7 @@ extern SField const sfNFTokens;
|
||||
extern SField const sfHooks;
|
||||
extern SField const sfVoteSlots;
|
||||
extern SField const sfAuthAccounts;
|
||||
extern SField const sfPriceDataSeries;
|
||||
|
||||
// array of objects (uncommon)
|
||||
extern SField const sfMajorities;
|
||||
|
||||
138
src/ripple/protocol/STCurrency.h
Normal file
138
src/ripple/protocol/STCurrency.h
Normal file
@@ -0,0 +1,138 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_PROTOCOL_STCURRENCY_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_STCURRENCY_H_INCLUDED
|
||||
|
||||
#include <ripple/basics/CountedObject.h>
|
||||
#include <ripple/protocol/SField.h>
|
||||
#include <ripple/protocol/STBase.h>
|
||||
#include <ripple/protocol/Serializer.h>
|
||||
#include <ripple/protocol/UintTypes.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class STCurrency final : public STBase
|
||||
{
|
||||
private:
|
||||
Currency currency_{};
|
||||
|
||||
public:
|
||||
using value_type = Currency;
|
||||
|
||||
STCurrency() = default;
|
||||
|
||||
explicit STCurrency(SerialIter& sit, SField const& name);
|
||||
|
||||
explicit STCurrency(SField const& name, Currency const& currency);
|
||||
|
||||
explicit STCurrency(SField const& name);
|
||||
|
||||
Currency const&
|
||||
currency() const;
|
||||
|
||||
Currency const&
|
||||
value() const noexcept;
|
||||
|
||||
void
|
||||
setCurrency(Currency const& currency);
|
||||
|
||||
SerializedTypeID
|
||||
getSType() const override;
|
||||
|
||||
std::string
|
||||
getText() const override;
|
||||
|
||||
Json::Value getJson(JsonOptions) const override;
|
||||
|
||||
void
|
||||
add(Serializer& s) const override;
|
||||
|
||||
bool
|
||||
isEquivalent(const STBase& t) const override;
|
||||
|
||||
bool
|
||||
isDefault() const override;
|
||||
|
||||
private:
|
||||
static std::unique_ptr<STCurrency>
|
||||
construct(SerialIter&, SField const& name);
|
||||
|
||||
STBase*
|
||||
copy(std::size_t n, void* buf) const override;
|
||||
STBase*
|
||||
move(std::size_t n, void* buf) override;
|
||||
|
||||
friend class detail::STVar;
|
||||
};
|
||||
|
||||
STCurrency
|
||||
currencyFromJson(SField const& name, Json::Value const& v);
|
||||
|
||||
inline Currency const&
|
||||
STCurrency::currency() const
|
||||
{
|
||||
return currency_;
|
||||
}
|
||||
|
||||
inline Currency const&
|
||||
STCurrency::value() const noexcept
|
||||
{
|
||||
return currency_;
|
||||
}
|
||||
|
||||
inline void
|
||||
STCurrency::setCurrency(Currency const& currency)
|
||||
{
|
||||
currency_ = currency;
|
||||
}
|
||||
|
||||
inline bool
|
||||
operator==(STCurrency const& lhs, STCurrency const& rhs)
|
||||
{
|
||||
return lhs.currency() == rhs.currency();
|
||||
}
|
||||
|
||||
inline bool
|
||||
operator!=(STCurrency const& lhs, STCurrency const& rhs)
|
||||
{
|
||||
return !operator==(lhs, rhs);
|
||||
}
|
||||
|
||||
inline bool
|
||||
operator<(STCurrency const& lhs, STCurrency const& rhs)
|
||||
{
|
||||
return lhs.currency() < rhs.currency();
|
||||
}
|
||||
|
||||
inline bool
|
||||
operator==(STCurrency const& lhs, Currency const& rhs)
|
||||
{
|
||||
return lhs.currency() == rhs;
|
||||
}
|
||||
|
||||
inline bool
|
||||
operator<(STCurrency const& lhs, Currency const& rhs)
|
||||
{
|
||||
return lhs.currency() < rhs;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <ripple/protocol/SOTemplate.h>
|
||||
#include <ripple/protocol/STAmount.h>
|
||||
#include <ripple/protocol/STBase.h>
|
||||
#include <ripple/protocol/STCurrency.h>
|
||||
#include <ripple/protocol/STIssue.h>
|
||||
#include <ripple/protocol/STPathSet.h>
|
||||
#include <ripple/protocol/STVector256.h>
|
||||
@@ -241,6 +242,8 @@ public:
|
||||
getFieldV256(SField const& field) const;
|
||||
const STArray&
|
||||
getFieldArray(SField const& field) const;
|
||||
const STCurrency&
|
||||
getFieldCurrency(SField const& field) const;
|
||||
|
||||
/** Get the value of a field.
|
||||
@param A TypedField built from an SField value representing the desired
|
||||
@@ -370,6 +373,8 @@ public:
|
||||
void
|
||||
setFieldIssue(SField const& field, STIssue const&);
|
||||
void
|
||||
setFieldCurrency(SField const& field, STCurrency const&);
|
||||
void
|
||||
setFieldPathSet(SField const& field, STPathSet const&);
|
||||
void
|
||||
setFieldV256(SField const& field, STVector256 const& v);
|
||||
|
||||
@@ -135,6 +135,9 @@ enum TEMcodes : TERUnderlyingType {
|
||||
temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT,
|
||||
|
||||
temEMPTY_DID,
|
||||
|
||||
temARRAY_EMPTY,
|
||||
temARRAY_TOO_LARGE,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -330,7 +333,11 @@ enum TECcodes : TERUnderlyingType {
|
||||
tecXCHAIN_SELF_COMMIT = 184,
|
||||
tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR = 185,
|
||||
tecXCHAIN_CREATE_ACCOUNT_DISABLED = 186,
|
||||
tecEMPTY_DID = 187
|
||||
tecEMPTY_DID = 187,
|
||||
tecINVALID_UPDATE_TIME = 188,
|
||||
tecTOKEN_PAIR_NOT_FOUND = 189,
|
||||
tecARRAY_EMPTY = 190,
|
||||
tecARRAY_TOO_LARGE = 191
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -191,6 +191,12 @@ enum TxType : std::uint16_t
|
||||
ttDID_DELETE = 50,
|
||||
|
||||
|
||||
/** This transaction type creates an Oracle instance */
|
||||
ttORACLE_SET = 51,
|
||||
|
||||
/** This transaction type deletes an Oracle instance */
|
||||
ttORACLE_DELETE = 52,
|
||||
|
||||
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||
|
||||
For details, see: https://xrpl.org/amendments.html
|
||||
|
||||
@@ -109,7 +109,8 @@ constexpr static ErrorInfo unorderedErrorInfos[]{
|
||||
{rpcSTREAM_MALFORMED, "malformedStream", "Stream malformed.", 400},
|
||||
{rpcTOO_BUSY, "tooBusy", "The server is too busy to help you now.", 503},
|
||||
{rpcTXN_NOT_FOUND, "txnNotFound", "Transaction not found.", 404},
|
||||
{rpcUNKNOWN_COMMAND, "unknownCmd", "Unknown method.", 405}};
|
||||
{rpcUNKNOWN_COMMAND, "unknownCmd", "Unknown method.", 405},
|
||||
{rpcORACLE_MALFORMED, "oracleMalformed", "Oracle request is malformed.", 400}};
|
||||
// clang-format on
|
||||
|
||||
// Sort and validate unorderedErrorInfos at compile time. Should be
|
||||
|
||||
@@ -461,6 +461,7 @@ REGISTER_FEATURE(DID, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FIX(fixFillOrKill, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixNFTokenReserve, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX(fixInnerObjTemplate, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(PriceOracle, Supported::yes, VoteBehavior::DefaultNo);
|
||||
|
||||
// The following amendments are obsolete, but must remain supported
|
||||
// because they could potentially get enabled.
|
||||
|
||||
@@ -72,6 +72,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
XCHAIN_CLAIM_ID = 'Q',
|
||||
XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K',
|
||||
DID = 'I',
|
||||
ORACLE = 'R',
|
||||
|
||||
// No longer used or supported. Left here to reserve the space
|
||||
// to avoid accidental reuse.
|
||||
@@ -444,6 +445,12 @@ did(AccountID const& account) noexcept
|
||||
return {ltDID, indexHash(LedgerNameSpace::DID, account)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
oracle(AccountID const& account, std::uint32_t const& documentID) noexcept
|
||||
{
|
||||
return {ltORACLE, indexHash(LedgerNameSpace::ORACLE, account, documentID)};
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -138,6 +138,15 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{
|
||||
{sfAccount, soeREQUIRED},
|
||||
});
|
||||
|
||||
add(sfPriceData.jsonName.c_str(),
|
||||
sfPriceData.getCode(),
|
||||
{
|
||||
{sfBaseAsset, soeREQUIRED},
|
||||
{sfQuoteAsset, soeREQUIRED},
|
||||
{sfAssetPrice, soeOPTIONAL},
|
||||
{sfScale, soeDEFAULT},
|
||||
});
|
||||
}
|
||||
|
||||
InnerObjectFormats const&
|
||||
|
||||
@@ -339,6 +339,22 @@ LedgerFormats::LedgerFormats()
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED}
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Oracle,
|
||||
ltORACLE,
|
||||
{
|
||||
{sfOwner, soeREQUIRED},
|
||||
{sfProvider, soeREQUIRED},
|
||||
{sfPriceDataSeries, soeREQUIRED},
|
||||
{sfAssetClass, soeREQUIRED},
|
||||
{sfLastUpdateTime, soeREQUIRED},
|
||||
{sfURI, soeOPTIONAL},
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED}
|
||||
},
|
||||
commonFields);
|
||||
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfMetadata, "Metadata", METADATA
|
||||
CONSTRUCT_TYPED_SFIELD(sfCloseResolution, "CloseResolution", UINT8, 1);
|
||||
CONSTRUCT_TYPED_SFIELD(sfMethod, "Method", UINT8, 2);
|
||||
CONSTRUCT_TYPED_SFIELD(sfTransactionResult, "TransactionResult", UINT8, 3);
|
||||
CONSTRUCT_TYPED_SFIELD(sfScale, "Scale", UINT8, 4);
|
||||
|
||||
// 8-bit integers (uncommon)
|
||||
CONSTRUCT_TYPED_SFIELD(sfTickSize, "TickSize", UINT8, 16);
|
||||
@@ -128,6 +129,7 @@ CONSTRUCT_TYPED_SFIELD(sfTransferRate, "TransferRate", UINT32,
|
||||
CONSTRUCT_TYPED_SFIELD(sfWalletSize, "WalletSize", UINT32, 12);
|
||||
CONSTRUCT_TYPED_SFIELD(sfOwnerCount, "OwnerCount", UINT32, 13);
|
||||
CONSTRUCT_TYPED_SFIELD(sfDestinationTag, "DestinationTag", UINT32, 14);
|
||||
CONSTRUCT_TYPED_SFIELD(sfLastUpdateTime, "LastUpdateTime", UINT32, 15);
|
||||
|
||||
// 32-bit integers (uncommon)
|
||||
CONSTRUCT_TYPED_SFIELD(sfHighQualityIn, "HighQualityIn", UINT32, 16);
|
||||
@@ -164,6 +166,7 @@ CONSTRUCT_TYPED_SFIELD(sfEmitGeneration, "EmitGeneration", UINT32,
|
||||
// 47 is reserved for LockCount(Hooks)
|
||||
CONSTRUCT_TYPED_SFIELD(sfVoteWeight, "VoteWeight", UINT32, 48);
|
||||
CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 50);
|
||||
CONSTRUCT_TYPED_SFIELD(sfOracleDocumentID, "OracleDocumentID", UINT32, 51);
|
||||
|
||||
// 64-bit integers (common)
|
||||
CONSTRUCT_TYPED_SFIELD(sfIndexNext, "IndexNext", UINT64, 1);
|
||||
@@ -188,6 +191,7 @@ CONSTRUCT_TYPED_SFIELD(sfReferenceCount, "ReferenceCount", U
|
||||
CONSTRUCT_TYPED_SFIELD(sfXChainClaimID, "XChainClaimID", UINT64, 20);
|
||||
CONSTRUCT_TYPED_SFIELD(sfXChainAccountCreateCount, "XChainAccountCreateCount", UINT64, 21);
|
||||
CONSTRUCT_TYPED_SFIELD(sfXChainAccountClaimCount, "XChainAccountClaimCount", UINT64, 22);
|
||||
CONSTRUCT_TYPED_SFIELD(sfAssetPrice, "AssetPrice", UINT64, 23);
|
||||
|
||||
// 128-bit
|
||||
CONSTRUCT_TYPED_SFIELD(sfEmailHash, "EmailHash", UINT128, 1);
|
||||
@@ -300,6 +304,8 @@ CONSTRUCT_TYPED_SFIELD(sfHookParameterName, "HookParameterName", VL,
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookParameterValue, "HookParameterValue", VL, 25);
|
||||
CONSTRUCT_TYPED_SFIELD(sfDIDDocument, "DIDDocument", VL, 26);
|
||||
CONSTRUCT_TYPED_SFIELD(sfData, "Data", VL, 27);
|
||||
CONSTRUCT_TYPED_SFIELD(sfAssetClass, "AssetClass", VL, 28);
|
||||
CONSTRUCT_TYPED_SFIELD(sfProvider, "Provider", VL, 29);
|
||||
|
||||
// account
|
||||
CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1);
|
||||
@@ -331,6 +337,10 @@ CONSTRUCT_TYPED_SFIELD(sfNFTokenOffers, "NFTokenOffers", VECTOR25
|
||||
// path set
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfPaths, "Paths", PATHSET, 1);
|
||||
|
||||
// currency
|
||||
CONSTRUCT_TYPED_SFIELD(sfBaseAsset, "BaseAsset", CURRENCY, 1);
|
||||
CONSTRUCT_TYPED_SFIELD(sfQuoteAsset, "QuoteAsset", CURRENCY, 2);
|
||||
|
||||
// issue
|
||||
CONSTRUCT_TYPED_SFIELD(sfLockingChainIssue, "LockingChainIssue", ISSUE, 1);
|
||||
CONSTRUCT_TYPED_SFIELD(sfIssuingChainIssue, "IssuingChainIssue", ISSUE, 2);
|
||||
@@ -379,6 +389,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement,
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement,
|
||||
"XChainCreateAccountAttestationCollectionElement",
|
||||
OBJECT, 31);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfPriceData, "PriceData", OBJECT, 32);
|
||||
|
||||
// array of objects
|
||||
// ARRAY/1 is reserved for end of array
|
||||
@@ -406,7 +417,8 @@ CONSTRUCT_UNTYPED_SFIELD(sfXChainClaimAttestations,
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfXChainCreateAccountAttestations,
|
||||
"XChainCreateAccountAttestations",
|
||||
ARRAY, 22);
|
||||
// 23 and 24 are unused and available for use
|
||||
// 23 is unused and available for use
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfPriceDataSeries, "PriceDataSeries", ARRAY, 24);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfAuthAccounts, "AuthAccounts", ARRAY, 25);
|
||||
|
||||
// clang-format on
|
||||
|
||||
114
src/ripple/protocol/impl/STCurrency.cpp
Normal file
114
src/ripple/protocol/impl/STCurrency.cpp
Normal file
@@ -0,0 +1,114 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/STCurrency.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
|
||||
#include <ripple/basics/contract.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
STCurrency::STCurrency(SField const& name) : STBase{name}
|
||||
{
|
||||
}
|
||||
|
||||
STCurrency::STCurrency(SerialIter& sit, SField const& name) : STBase{name}
|
||||
{
|
||||
currency_ = sit.get160();
|
||||
}
|
||||
|
||||
STCurrency::STCurrency(SField const& name, Currency const& currency)
|
||||
: STBase{name}, currency_{currency}
|
||||
{
|
||||
}
|
||||
|
||||
SerializedTypeID
|
||||
STCurrency::getSType() const
|
||||
{
|
||||
return STI_CURRENCY;
|
||||
}
|
||||
|
||||
std::string
|
||||
STCurrency::getText() const
|
||||
{
|
||||
return to_string(currency_);
|
||||
}
|
||||
|
||||
Json::Value STCurrency::getJson(JsonOptions) const
|
||||
{
|
||||
return to_string(currency_);
|
||||
}
|
||||
|
||||
void
|
||||
STCurrency::add(Serializer& s) const
|
||||
{
|
||||
s.addBitString(currency_);
|
||||
}
|
||||
|
||||
bool
|
||||
STCurrency::isEquivalent(const STBase& t) const
|
||||
{
|
||||
const STCurrency* v = dynamic_cast<const STCurrency*>(&t);
|
||||
return v && (*v == *this);
|
||||
}
|
||||
|
||||
bool
|
||||
STCurrency::isDefault() const
|
||||
{
|
||||
return isXRP(currency_);
|
||||
}
|
||||
|
||||
std::unique_ptr<STCurrency>
|
||||
STCurrency::construct(SerialIter& sit, SField const& name)
|
||||
{
|
||||
return std::make_unique<STCurrency>(sit, name);
|
||||
}
|
||||
|
||||
STBase*
|
||||
STCurrency::copy(std::size_t n, void* buf) const
|
||||
{
|
||||
return emplace(n, buf, *this);
|
||||
}
|
||||
|
||||
STBase*
|
||||
STCurrency::move(std::size_t n, void* buf)
|
||||
{
|
||||
return emplace(n, buf, std::move(*this));
|
||||
}
|
||||
|
||||
STCurrency
|
||||
currencyFromJson(SField const& name, Json::Value const& v)
|
||||
{
|
||||
if (!v.isString())
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"currencyFromJson currency must be a string Json value");
|
||||
}
|
||||
|
||||
auto const currency = to_currency(v.asString());
|
||||
if (currency == badCurrency() || currency == noCurrency())
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"currencyFromJson currency must be a valid currency");
|
||||
}
|
||||
|
||||
return STCurrency{name, currency};
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <ripple/protocol/STAccount.h>
|
||||
#include <ripple/protocol/STArray.h>
|
||||
#include <ripple/protocol/STBlob.h>
|
||||
#include <ripple/protocol/STCurrency.h>
|
||||
#include <ripple/protocol/STObject.h>
|
||||
|
||||
namespace ripple {
|
||||
@@ -642,6 +643,13 @@ STObject::getFieldArray(SField const& field) const
|
||||
return getFieldByConstRef<STArray>(field, empty);
|
||||
}
|
||||
|
||||
STCurrency const&
|
||||
STObject::getFieldCurrency(SField const& field) const
|
||||
{
|
||||
static STCurrency const empty{};
|
||||
return getFieldByConstRef<STCurrency>(field, empty);
|
||||
}
|
||||
|
||||
void
|
||||
STObject::set(std::unique_ptr<STBase> v)
|
||||
{
|
||||
@@ -730,6 +738,12 @@ STObject::setFieldAmount(SField const& field, STAmount const& v)
|
||||
setFieldUsingAssignment(field, v);
|
||||
}
|
||||
|
||||
void
|
||||
STObject::setFieldCurrency(SField const& field, STCurrency const& v)
|
||||
{
|
||||
setFieldUsingAssignment(field, v);
|
||||
}
|
||||
|
||||
void
|
||||
STObject::setFieldIssue(SField const& field, STIssue const& v)
|
||||
{
|
||||
|
||||
@@ -760,6 +760,19 @@ parseLeaf(
|
||||
}
|
||||
break;
|
||||
|
||||
case STI_CURRENCY:
|
||||
try
|
||||
{
|
||||
ret = detail::make_stvar<STCurrency>(
|
||||
currencyFromJson(field, value));
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
error = invalid_data(json_name, fieldName);
|
||||
return ret;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
error = bad_type(json_name, fieldName);
|
||||
return ret;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <ripple/protocol/STBase.h>
|
||||
#include <ripple/protocol/STBitString.h>
|
||||
#include <ripple/protocol/STBlob.h>
|
||||
#include <ripple/protocol/STCurrency.h>
|
||||
#include <ripple/protocol/STInteger.h>
|
||||
#include <ripple/protocol/STIssue.h>
|
||||
#include <ripple/protocol/STObject.h>
|
||||
@@ -167,6 +168,9 @@ STVar::STVar(SerialIter& sit, SField const& name, int depth)
|
||||
case STI_XCHAIN_BRIDGE:
|
||||
construct<STXChainBridge>(sit, name);
|
||||
return;
|
||||
case STI_CURRENCY:
|
||||
construct<STCurrency>(sit, name);
|
||||
return;
|
||||
default:
|
||||
Throw<std::runtime_error>("Unknown object type");
|
||||
}
|
||||
@@ -228,6 +232,9 @@ STVar::STVar(SerializedTypeID id, SField const& name)
|
||||
case STI_XCHAIN_BRIDGE:
|
||||
construct<STXChainBridge>(name);
|
||||
return;
|
||||
case STI_CURRENCY:
|
||||
construct<STCurrency>(name);
|
||||
return;
|
||||
default:
|
||||
Throw<std::runtime_error>("Unknown object type");
|
||||
}
|
||||
|
||||
@@ -111,6 +111,10 @@ transResults()
|
||||
MAKE_ERROR(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR, "Bad public key account pair in an xchain transaction."),
|
||||
MAKE_ERROR(tecXCHAIN_CREATE_ACCOUNT_DISABLED, "This bridge does not support account creation."),
|
||||
MAKE_ERROR(tecEMPTY_DID, "The DID object did not have a URI or DIDDocument field."),
|
||||
MAKE_ERROR(tecINVALID_UPDATE_TIME, "The Oracle object has invalid LastUpdateTime field."),
|
||||
MAKE_ERROR(tecTOKEN_PAIR_NOT_FOUND, "Token pair is not found in Oracle object."),
|
||||
MAKE_ERROR(tecARRAY_EMPTY, "Array is empty."),
|
||||
MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."),
|
||||
|
||||
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
|
||||
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
|
||||
@@ -197,6 +201,8 @@ transResults()
|
||||
MAKE_ERROR(temXCHAIN_BRIDGE_NONDOOR_OWNER, "Malformed: Bridge owner must be one of the door accounts."),
|
||||
MAKE_ERROR(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT, "Malformed: Bad min account create amount."),
|
||||
MAKE_ERROR(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT, "Malformed: Bad reward amount."),
|
||||
MAKE_ERROR(temARRAY_EMPTY, "Malformed: Array is empty."),
|
||||
MAKE_ERROR(temARRAY_TOO_LARGE, "Malformed: Array is too large."),
|
||||
|
||||
MAKE_ERROR(terRETRY, "Retry transaction."),
|
||||
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),
|
||||
|
||||
@@ -483,6 +483,25 @@ TxFormats::TxFormats()
|
||||
commonFields);
|
||||
|
||||
add(jss::DIDDelete, ttDID_DELETE, {}, commonFields);
|
||||
|
||||
add(jss::OracleSet,
|
||||
ttORACLE_SET,
|
||||
{
|
||||
{sfOracleDocumentID, soeREQUIRED},
|
||||
{sfProvider, soeOPTIONAL},
|
||||
{sfURI, soeOPTIONAL},
|
||||
{sfAssetClass, soeOPTIONAL},
|
||||
{sfLastUpdateTime, soeREQUIRED},
|
||||
{sfPriceDataSeries, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::OracleDelete,
|
||||
ttORACLE_DELETE,
|
||||
{
|
||||
{sfOracleDocumentID, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
}
|
||||
|
||||
TxFormats const&
|
||||
|
||||
@@ -60,8 +60,11 @@ JSS(Amount); // in: TransactionSign; field.
|
||||
JSS(Amount2); // in/out: AMM IOU/XRP pool, deposit, withdraw amount
|
||||
JSS(Asset); // in: AMM Asset1
|
||||
JSS(Asset2); // in: AMM Asset2
|
||||
JSS(AssetClass); // in: Oracle
|
||||
JSS(AssetPrice); // in: Oracle
|
||||
JSS(AuthAccount); // in: AMM Auction Slot
|
||||
JSS(AuthAccounts); // in: AMM Auction Slot
|
||||
JSS(BaseAsset); // in: Oracle
|
||||
JSS(Bridge); // ledger type.
|
||||
JSS(Check); // ledger type.
|
||||
JSS(CheckCancel); // transaction type.
|
||||
@@ -89,6 +92,7 @@ JSS(Flags); // in/out: TransactionSign; field.
|
||||
JSS(incomplete_shards); // out: OverlayImpl, PeerImp
|
||||
JSS(Invalid); //
|
||||
JSS(LastLedgerSequence); // in: TransactionSign; field
|
||||
JSS(LastUpdateTime); // field.
|
||||
JSS(LedgerHashes); // ledger type.
|
||||
JSS(LimitAmount); // field.
|
||||
JSS(BidMax); // in: AMM Bid
|
||||
@@ -108,16 +112,26 @@ JSS(Offer); // ledger type.
|
||||
JSS(OfferCancel); // transaction type.
|
||||
JSS(OfferCreate); // transaction type.
|
||||
JSS(OfferSequence); // field.
|
||||
JSS(Oracle); // ledger type.
|
||||
JSS(OracleDelete); // transaction type.
|
||||
JSS(OracleDocumentID); // field
|
||||
JSS(OracleSet); // transaction type.
|
||||
JSS(Owner); // field
|
||||
JSS(Paths); // in/out: TransactionSign
|
||||
JSS(PayChannel); // ledger type.
|
||||
JSS(Payment); // transaction type.
|
||||
JSS(PaymentChannelClaim); // transaction type.
|
||||
JSS(PaymentChannelCreate); // transaction type.
|
||||
JSS(PaymentChannelFund); // transaction type.
|
||||
JSS(PriceDataSeries); // field.
|
||||
JSS(PriceData); // field.
|
||||
JSS(Provider); // field.
|
||||
JSS(QuoteAsset); // in: Oracle.
|
||||
JSS(RippleState); // ledger type.
|
||||
JSS(SLE_hit_rate); // out: GetCounts.
|
||||
JSS(SetFee); // transaction type.
|
||||
JSS(UNLModify); // transaction type.
|
||||
JSS(Scale); // field.
|
||||
JSS(SettleDelay); // in: TransactionSign
|
||||
JSS(SendMax); // in: TransactionSign
|
||||
JSS(Sequence); // in/out: TransactionSign; field.
|
||||
@@ -135,6 +149,7 @@ JSS(TradingFee); // in/out: AMM trading fee
|
||||
JSS(TransactionType); // in: TransactionSign.
|
||||
JSS(TransferRate); // in: TransferRate.
|
||||
JSS(TrustSet); // transaction type.
|
||||
JSS(URI); // field.
|
||||
JSS(VoteSlots); // out: AMM Vote
|
||||
JSS(XChainAddAccountCreateAttestation); // transaction type.
|
||||
JSS(XChainAddClaimAttestation); // transaction type.
|
||||
@@ -202,6 +217,7 @@ JSS(avg_bps_sent); // out: Peers
|
||||
JSS(balance); // out: AccountLines
|
||||
JSS(balances); // out: GatewayBalances
|
||||
JSS(base); // out: LogLevel
|
||||
JSS(base_asset); // in: get_aggregate_price
|
||||
JSS(base_fee); // out: NetworkOPs
|
||||
JSS(base_fee_xrp); // out: NetworkOPs
|
||||
JSS(bids); // out: Subscribe
|
||||
@@ -299,6 +315,7 @@ JSS(enabled); // out: AmendmentTable
|
||||
JSS(engine_result); // out: NetworkOPs, TransactionSign, Submit
|
||||
JSS(engine_result_code); // out: NetworkOPs, TransactionSign, Submit
|
||||
JSS(engine_result_message); // out: NetworkOPs, TransactionSign, Submit
|
||||
JSS(entire_set); // out: get_aggregate_price
|
||||
JSS(ephemeral_key); // out: ValidatorInfo
|
||||
// in/out: Manifest
|
||||
JSS(error); // out: error
|
||||
@@ -458,6 +475,8 @@ JSS(max_ledger); // in/out: LedgerCleaner
|
||||
JSS(max_queue_size); // out: TxQ
|
||||
JSS(max_spend_drops); // out: AccountInfo
|
||||
JSS(max_spend_drops_total); // out: AccountInfo
|
||||
JSS(mean); // out: get_aggregate_price
|
||||
JSS(median); // out: get_aggregate_price
|
||||
JSS(median_fee); // out: TxQ
|
||||
JSS(median_level); // out: TxQ
|
||||
JSS(message); // error.
|
||||
@@ -515,6 +534,9 @@ JSS(open); // out: handlers/Ledger
|
||||
JSS(open_ledger_cost); // out: SubmitTransaction
|
||||
JSS(open_ledger_fee); // out: TxQ
|
||||
JSS(open_ledger_level); // out: TxQ
|
||||
JSS(oracle); // in: LedgerEntry
|
||||
JSS(oracles); // in: get_aggregate_price
|
||||
JSS(oracle_document_id); // in: get_aggregate_price
|
||||
JSS(owner); // in: LedgerEntry, out: NetworkOPs
|
||||
JSS(owner_funds); // in/out: Ledger, NetworkOPs, AcceptedLedgerTx
|
||||
JSS(page_index);
|
||||
@@ -561,6 +583,7 @@ JSS(queue); // in: AccountInfo
|
||||
JSS(queue_data); // out: AccountInfo
|
||||
JSS(queued); // out: SubmitTransaction
|
||||
JSS(queued_duration_us);
|
||||
JSS(quote_asset); // in: get_aggregate_price
|
||||
JSS(random); // out: Random
|
||||
JSS(raw_meta); // out: AcceptedLedgerTx
|
||||
JSS(receive_currencies); // out: AccountCurrencies
|
||||
@@ -615,12 +638,14 @@ JSS(signing_keys); // out: ValidatorList
|
||||
JSS(signing_time); // out: NetworkOPs
|
||||
JSS(signer_list); // in: AccountObjects
|
||||
JSS(signer_lists); // in/out: AccountInfo
|
||||
JSS(size); // out: get_aggregate_price
|
||||
JSS(snapshot); // in: Subscribe
|
||||
JSS(source_account); // in: PathRequest, RipplePathFind
|
||||
JSS(source_amount); // in: PathRequest, RipplePathFind
|
||||
JSS(source_currencies); // in: PathRequest, RipplePathFind
|
||||
JSS(source_tag); // out: AccountChannels
|
||||
JSS(stand_alone); // out: NetworkOPs
|
||||
JSS(standard_deviation); // out: get_aggregate_price
|
||||
JSS(start); // in: TxHistory
|
||||
JSS(started);
|
||||
JSS(state); // out: Logic.h, ServerState, LedgerData
|
||||
@@ -636,6 +661,7 @@ JSS(sub_index); // in: LedgerEntry
|
||||
JSS(subcommand); // in: PathFind
|
||||
JSS(success); // rpc
|
||||
JSS(supported); // out: AmendmentTableImpl
|
||||
JSS(sync_mode); // in: Submit
|
||||
JSS(system_time_offset); // out: NetworkOPs
|
||||
JSS(tag); // out: Peers
|
||||
JSS(taker); // in: Subscribe, BookOffers
|
||||
@@ -649,9 +675,12 @@ JSS(ticket_count); // out: AccountInfo
|
||||
JSS(ticket_seq); // in: LedgerEntry
|
||||
JSS(time);
|
||||
JSS(timeouts); // out: InboundLedger
|
||||
JSS(time_threshold); // in/out: Oracle aggregate
|
||||
JSS(time_interval); // out: AMM Auction Slot
|
||||
JSS(track); // out: PeerImp
|
||||
JSS(traffic); // out: Overlay
|
||||
JSS(trim); // in: get_aggregate_price
|
||||
JSS(trimmed_set); // out: get_aggregate_price
|
||||
JSS(total); // out: counters
|
||||
JSS(total_bytes_recv); // out: Peers
|
||||
JSS(total_bytes_sent); // out: Peers
|
||||
|
||||
340
src/ripple/rpc/handlers/GetAggregatePrice.cpp
Normal file
340
src/ripple/rpc/handlers/GetAggregatePrice.cpp
Normal file
@@ -0,0 +1,340 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/app/main/Application.h>
|
||||
#include <ripple/json/json_value.h>
|
||||
#include <ripple/ledger/ReadView.h>
|
||||
#include <ripple/protocol/ErrorCodes.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <ripple/rpc/Context.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
|
||||
#include <boost/bimap.hpp>
|
||||
#include <boost/bimap/multiset_of.hpp>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
using namespace boost::bimaps;
|
||||
// sorted descending by lastUpdateTime, ascending by AssetPrice
|
||||
using Prices = bimap<
|
||||
multiset_of<std::uint32_t, std::greater<std::uint32_t>>,
|
||||
multiset_of<STAmount>>;
|
||||
|
||||
/** Calls callback "f" on the ledger-object sle and up to three previous
|
||||
* metadata objects. Stops early if the callback returns true.
|
||||
*/
|
||||
static void
|
||||
iteratePriceData(
|
||||
RPC::JsonContext& context,
|
||||
std::shared_ptr<SLE const> const& sle,
|
||||
std::function<bool(STObject const&)>&& f)
|
||||
{
|
||||
using Meta = std::shared_ptr<STObject const>;
|
||||
constexpr std::uint8_t maxHistory = 3;
|
||||
bool isNew = false;
|
||||
std::uint8_t history = 0;
|
||||
|
||||
// `oracle` points to an object that has an `sfPriceDataSeries` field.
|
||||
// When this function is called, that is a `PriceOracle` ledger object,
|
||||
// but after one iteration of the loop below, it is an `sfNewFields`
|
||||
// / `sfFinalFields` object in a `CreatedNode` / `ModifiedNode` object in
|
||||
// a transaction's metadata.
|
||||
|
||||
// `chain` points to an object that has `sfPreviousTxnID` and
|
||||
// `sfPreviousTxnLgrSeq` fields. When this function is called,
|
||||
// that is the `PriceOracle` ledger object pointed to by `oracle`,
|
||||
// but after one iteration of the loop below, then it is a `ModifiedNode`
|
||||
// / `CreatedNode` object in a transaction's metadata.
|
||||
STObject const* oracle = sle.get();
|
||||
STObject const* chain = oracle;
|
||||
// Use to test an unlikely scenario when CreatedNode / ModifiedNode
|
||||
// for the Oracle is not found in the inner loop
|
||||
STObject const* prevChain = nullptr;
|
||||
|
||||
Meta meta = nullptr;
|
||||
while (true)
|
||||
{
|
||||
if (prevChain == chain)
|
||||
return;
|
||||
|
||||
if (!oracle || f(*oracle) || isNew)
|
||||
return;
|
||||
|
||||
if (++history > maxHistory)
|
||||
return;
|
||||
|
||||
uint256 prevTx = chain->getFieldH256(sfPreviousTxnID);
|
||||
std::uint32_t prevSeq = chain->getFieldU32(sfPreviousTxnLgrSeq);
|
||||
|
||||
auto const ledger = context.ledgerMaster.getLedgerBySeq(prevSeq);
|
||||
if (!ledger)
|
||||
return;
|
||||
|
||||
meta = ledger->txRead(prevTx).second;
|
||||
|
||||
for (STObject const& node : meta->getFieldArray(sfAffectedNodes))
|
||||
{
|
||||
if (node.getFieldU16(sfLedgerEntryType) != ltORACLE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
prevChain = chain;
|
||||
chain = &node;
|
||||
isNew = node.isFieldPresent(sfNewFields);
|
||||
// if a meta is for the new and this is the first
|
||||
// look-up then it's the meta for the tx that
|
||||
// created the current object; i.e. there is no
|
||||
// historical data
|
||||
if (isNew && history == 1)
|
||||
return;
|
||||
|
||||
oracle = isNew
|
||||
? &static_cast<const STObject&>(node.peekAtField(sfNewFields))
|
||||
: &static_cast<const STObject&>(
|
||||
node.peekAtField(sfFinalFields));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return avg, sd, data set size
|
||||
static std::tuple<STAmount, Number, std::uint16_t>
|
||||
getStats(
|
||||
Prices::right_const_iterator const& begin,
|
||||
Prices::right_const_iterator const& end)
|
||||
{
|
||||
STAmount avg{noIssue(), 0, 0};
|
||||
Number sd{0};
|
||||
std::uint16_t const size = std::distance(begin, end);
|
||||
avg = std::accumulate(
|
||||
begin, end, avg, [&](STAmount const& acc, auto const& it) {
|
||||
return acc + it.first;
|
||||
});
|
||||
avg = divide(avg, STAmount{noIssue(), size, 0}, noIssue());
|
||||
if (size > 1)
|
||||
{
|
||||
sd = std::accumulate(
|
||||
begin, end, sd, [&](Number const& acc, auto const& it) {
|
||||
return acc + (it.first - avg) * (it.first - avg);
|
||||
});
|
||||
sd = root2(sd / (size - 1));
|
||||
}
|
||||
return {avg, sd, size};
|
||||
};
|
||||
|
||||
/**
|
||||
* oracles: array of {account, oracle_document_id}
|
||||
* base_asset: is the asset to be priced
|
||||
* quote_asset: is the denomination in which the prices are expressed
|
||||
* trim : percentage of outliers to trim [optional]
|
||||
* time_threshold : defines a range of prices to include based on the timestamp
|
||||
* range - {most recent, most recent - time_threshold} [optional]
|
||||
*/
|
||||
Json::Value
|
||||
doGetAggregatePrice(RPC::JsonContext& context)
|
||||
{
|
||||
Json::Value result;
|
||||
auto const& params(context.params);
|
||||
|
||||
constexpr std::uint16_t maxOracles = 200;
|
||||
if (!params.isMember(jss::oracles))
|
||||
return RPC::missing_field_error(jss::oracles);
|
||||
if (!params[jss::oracles].isArray() || params[jss::oracles].size() == 0 ||
|
||||
params[jss::oracles].size() > maxOracles)
|
||||
{
|
||||
RPC::inject_error(rpcORACLE_MALFORMED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!params.isMember(jss::base_asset))
|
||||
return RPC::missing_field_error(jss::base_asset);
|
||||
|
||||
if (!params.isMember(jss::quote_asset))
|
||||
return RPC::missing_field_error(jss::quote_asset);
|
||||
|
||||
// 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 = [¶ms](
|
||||
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;
|
||||
return params[field].asUInt();
|
||||
}
|
||||
return def;
|
||||
};
|
||||
|
||||
auto const trim = getField(jss::trim);
|
||||
if (std::holds_alternative<error_code_i>(trim))
|
||||
{
|
||||
RPC::inject_error(std::get<error_code_i>(trim), result);
|
||||
return result;
|
||||
}
|
||||
if (params.isMember(jss::trim) &&
|
||||
(std::get<std::uint32_t>(trim) == 0 ||
|
||||
std::get<std::uint32_t>(trim) > maxTrim))
|
||||
{
|
||||
RPC::inject_error(rpcINVALID_PARAMS, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto const timeThreshold = getField(jss::time_threshold, 0);
|
||||
if (std::holds_alternative<error_code_i>(timeThreshold))
|
||||
{
|
||||
RPC::inject_error(std::get<error_code_i>(timeThreshold), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto const& baseAsset = params[jss::base_asset];
|
||||
auto const& quoteAsset = params[jss::quote_asset];
|
||||
|
||||
// Collect the dataset into bimap keyed by lastUpdateTime and
|
||||
// STAmount (Number is int64 and price is uint64)
|
||||
Prices prices;
|
||||
for (auto const& oracle : params[jss::oracles])
|
||||
{
|
||||
if (!oracle.isMember(jss::oracle_document_id) ||
|
||||
!oracle.isMember(jss::account))
|
||||
{
|
||||
RPC::inject_error(rpcORACLE_MALFORMED, result);
|
||||
return result;
|
||||
}
|
||||
auto const documentID = oracle[jss::oracle_document_id].isConvertibleTo(
|
||||
Json::ValueType::uintValue)
|
||||
? std::make_optional(oracle[jss::oracle_document_id].asUInt())
|
||||
: std::nullopt;
|
||||
auto const account =
|
||||
parseBase58<AccountID>(oracle[jss::account].asString());
|
||||
if (!account || account->isZero() || !documentID)
|
||||
{
|
||||
RPC::inject_error(rpcINVALID_PARAMS, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::shared_ptr<ReadView const> ledger;
|
||||
result = RPC::lookupLedger(ledger, context);
|
||||
if (!ledger)
|
||||
return result;
|
||||
|
||||
auto const sle = ledger->read(keylet::oracle(*account, *documentID));
|
||||
iteratePriceData(context, sle, [&](STObject const& node) {
|
||||
auto const& series = node.getFieldArray(sfPriceDataSeries);
|
||||
// find the token pair entry with the price
|
||||
if (auto iter = std::find_if(
|
||||
series.begin(),
|
||||
series.end(),
|
||||
[&](STObject const& o) -> bool {
|
||||
return o.getFieldCurrency(sfBaseAsset).getText() ==
|
||||
baseAsset &&
|
||||
o.getFieldCurrency(sfQuoteAsset).getText() ==
|
||||
quoteAsset &&
|
||||
o.isFieldPresent(sfAssetPrice);
|
||||
});
|
||||
iter != series.end())
|
||||
{
|
||||
auto const price = iter->getFieldU64(sfAssetPrice);
|
||||
auto const scale = iter->isFieldPresent(sfScale)
|
||||
? -static_cast<int>(iter->getFieldU8(sfScale))
|
||||
: 0;
|
||||
prices.insert(Prices::value_type(
|
||||
node.getFieldU32(sfLastUpdateTime),
|
||||
STAmount{noIssue(), price, scale}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (prices.empty())
|
||||
{
|
||||
RPC::inject_error(rpcOBJECT_NOT_FOUND, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// erase outdated data
|
||||
// sorted in descending, therefore begin is the latest, end is the oldest
|
||||
auto const latestTime = prices.left.begin()->first;
|
||||
if (auto const threshold = std::get<std::uint32_t>(timeThreshold))
|
||||
{
|
||||
// threshold defines an acceptable range {max,min} of lastUpdateTime as
|
||||
// {latestTime, latestTime - threshold}, the prices with lastUpdateTime
|
||||
// greater than (latestTime - threshold) are erased.
|
||||
auto const oldestTime = prices.left.rbegin()->first;
|
||||
auto const upperBound =
|
||||
latestTime > threshold ? (latestTime - threshold) : oldestTime;
|
||||
if (upperBound > oldestTime)
|
||||
prices.left.erase(
|
||||
prices.left.upper_bound(upperBound), prices.left.end());
|
||||
|
||||
if (prices.empty())
|
||||
{
|
||||
RPC::inject_error(rpcOBJECT_NOT_FOUND, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
result[jss::time] = latestTime;
|
||||
|
||||
// calculate stats
|
||||
auto const [avg, sd, size] =
|
||||
getStats(prices.right.begin(), prices.right.end());
|
||||
result[jss::entire_set][jss::mean] = avg.getText();
|
||||
result[jss::entire_set][jss::size] = size;
|
||||
result[jss::entire_set][jss::standard_deviation] = to_string(sd);
|
||||
|
||||
auto itAdvance = [&](auto it, int distance) {
|
||||
std::advance(it, distance);
|
||||
return it;
|
||||
};
|
||||
|
||||
auto const median = [&prices, &itAdvance, &size_ = size]() {
|
||||
auto const middle = size_ / 2;
|
||||
if ((size_ % 2) == 0)
|
||||
{
|
||||
static STAmount two{noIssue(), 2, 0};
|
||||
auto it = itAdvance(prices.right.begin(), middle - 1);
|
||||
auto const& a1 = it->first;
|
||||
auto const& a2 = (++it)->first;
|
||||
return divide(a1 + a2, two, noIssue());
|
||||
}
|
||||
return itAdvance(prices.right.begin(), middle)->first;
|
||||
}();
|
||||
result[jss::median] = median.getText();
|
||||
|
||||
if (std::get<std::uint32_t>(trim) != 0)
|
||||
{
|
||||
auto const trimCount =
|
||||
prices.size() * std::get<std::uint32_t>(trim) / 100;
|
||||
|
||||
auto const [avg, sd, size] = getStats(
|
||||
itAdvance(prices.right.begin(), trimCount),
|
||||
itAdvance(prices.right.end(), -trimCount));
|
||||
result[jss::trimmed_set][jss::mean] = avg.getText();
|
||||
result[jss::trimmed_set][jss::size] = size;
|
||||
result[jss::trimmed_set][jss::standard_deviation] = to_string(sd);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -73,6 +73,8 @@ doGatewayBalances(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doGetCounts(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doGetAggregatePrice(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doLedgerAccept(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doLedgerCleaner(RPC::JsonContext&);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <ripple/app/main/Application.h>
|
||||
#include <ripple/basics/StringUtilities.h>
|
||||
#include <ripple/basics/strHex.h>
|
||||
#include <ripple/beast/core/LexicalCast.h>
|
||||
#include <ripple/json/json_errors.h>
|
||||
#include <ripple/ledger/ReadView.h>
|
||||
#include <ripple/net/RPCErr.h>
|
||||
@@ -598,6 +599,51 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
else
|
||||
uNodeIndex = keylet::did(*account).key;
|
||||
}
|
||||
else if (context.params.isMember(jss::oracle))
|
||||
{
|
||||
expectedType = ltORACLE;
|
||||
if (!context.params[jss::oracle].isObject())
|
||||
{
|
||||
if (!uNodeIndex.parseHex(
|
||||
context.params[jss::oracle].asString()))
|
||||
{
|
||||
uNodeIndex = beast::zero;
|
||||
jvResult[jss::error] = "malformedRequest";
|
||||
}
|
||||
}
|
||||
else if (
|
||||
!context.params[jss::oracle].isMember(
|
||||
jss::oracle_document_id) ||
|
||||
!context.params[jss::oracle].isMember(jss::account))
|
||||
{
|
||||
jvResult[jss::error] = "malformedRequest";
|
||||
}
|
||||
else
|
||||
{
|
||||
uNodeIndex = beast::zero;
|
||||
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))
|
||||
return std::make_optional(id.asUInt());
|
||||
else if (id.isString())
|
||||
{
|
||||
std::uint32_t v;
|
||||
if (beast::lexicalCastChecked(v, id.asString()))
|
||||
return std::make_optional(v);
|
||||
}
|
||||
return std::nullopt;
|
||||
}();
|
||||
auto const account =
|
||||
parseBase58<AccountID>(oracle[jss::account].asString());
|
||||
if (!account || account->isZero())
|
||||
jvResult[jss::error] = "malformedAddress";
|
||||
else if (!documentID)
|
||||
jvResult[jss::error] = "malformedDocumentID";
|
||||
else
|
||||
uNodeIndex = keylet::oracle(*account, *documentID).key;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (context.params.isMember("params") &&
|
||||
|
||||
@@ -111,6 +111,10 @@ Handler const handlerArray[]{
|
||||
{"gateway_balances", byRef(&doGatewayBalances), Role::USER, NO_CONDITION},
|
||||
#endif
|
||||
{"get_counts", byRef(&doGetCounts), Role::ADMIN, NO_CONDITION},
|
||||
{"get_aggregate_price",
|
||||
byRef(&doGetAggregatePrice),
|
||||
Role::USER,
|
||||
NO_CONDITION},
|
||||
{"feature", byRef(&doFeature), Role::ADMIN, NO_CONDITION},
|
||||
{"fee", byRef(&doFee), Role::USER, NEEDS_CURRENT_LEDGER},
|
||||
{"fetch_info", byRef(&doFetchInfo), Role::ADMIN, NO_CONDITION},
|
||||
|
||||
@@ -934,7 +934,7 @@ chooseLedgerEntryType(Json::Value const& params)
|
||||
std::pair<RPC::Status, LedgerEntryType> result{RPC::Status::OK, ltANY};
|
||||
if (params.isMember(jss::type))
|
||||
{
|
||||
static constexpr std::array<std::pair<char const*, LedgerEntryType>, 20>
|
||||
static constexpr std::array<std::pair<char const*, LedgerEntryType>, 21>
|
||||
types{
|
||||
{{jss::account, ltACCOUNT_ROOT},
|
||||
{jss::amendments, ltAMENDMENTS},
|
||||
@@ -956,7 +956,8 @@ chooseLedgerEntryType(Json::Value const& params)
|
||||
{jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID},
|
||||
{jss::xchain_owned_create_account_claim_id,
|
||||
ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID},
|
||||
{jss::did, ltDID}}};
|
||||
{jss::did, ltDID},
|
||||
{jss::oracle, ltORACLE}}};
|
||||
|
||||
auto const& p = params[jss::type];
|
||||
if (!p.isString())
|
||||
|
||||
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