mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-20 02:55:50 +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/STArray.cpp
|
||||||
src/ripple/protocol/impl/STBase.cpp
|
src/ripple/protocol/impl/STBase.cpp
|
||||||
src/ripple/protocol/impl/STBlob.cpp
|
src/ripple/protocol/impl/STBlob.cpp
|
||||||
|
src/ripple/protocol/impl/STCurrency.cpp
|
||||||
src/ripple/protocol/impl/STInteger.cpp
|
src/ripple/protocol/impl/STInteger.cpp
|
||||||
src/ripple/protocol/impl/STLedgerEntry.cpp
|
src/ripple/protocol/impl/STLedgerEntry.cpp
|
||||||
src/ripple/protocol/impl/STObject.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/CreateOffer.cpp
|
||||||
src/ripple/app/tx/impl/CreateTicket.cpp
|
src/ripple/app/tx/impl/CreateTicket.cpp
|
||||||
src/ripple/app/tx/impl/DeleteAccount.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/DepositPreauth.cpp
|
||||||
src/ripple/app/tx/impl/DID.cpp
|
src/ripple/app/tx/impl/DID.cpp
|
||||||
src/ripple/app/tx/impl/Escrow.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/PayChan.cpp
|
||||||
src/ripple/app/tx/impl/Payment.cpp
|
src/ripple/app/tx/impl/Payment.cpp
|
||||||
src/ripple/app/tx/impl/SetAccount.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/SetRegularKey.cpp
|
||||||
src/ripple/app/tx/impl/SetSignerList.cpp
|
src/ripple/app/tx/impl/SetSignerList.cpp
|
||||||
src/ripple/app/tx/impl/SetTrust.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/FetchInfo.cpp
|
||||||
src/ripple/rpc/handlers/GatewayBalances.cpp
|
src/ripple/rpc/handlers/GatewayBalances.cpp
|
||||||
src/ripple/rpc/handlers/GetCounts.cpp
|
src/ripple/rpc/handlers/GetCounts.cpp
|
||||||
|
src/ripple/rpc/handlers/GetAggregatePrice.cpp
|
||||||
src/ripple/rpc/handlers/LedgerAccept.cpp
|
src/ripple/rpc/handlers/LedgerAccept.cpp
|
||||||
src/ripple/rpc/handlers/LedgerCleanerHandler.cpp
|
src/ripple/rpc/handlers/LedgerCleanerHandler.cpp
|
||||||
src/ripple/rpc/handlers/LedgerClosed.cpp
|
src/ripple/rpc/handlers/LedgerClosed.cpp
|
||||||
@@ -840,6 +844,7 @@ if (tests)
|
|||||||
src/test/app/NFTokenDir_test.cpp
|
src/test/app/NFTokenDir_test.cpp
|
||||||
src/test/app/OfferStream_test.cpp
|
src/test/app/OfferStream_test.cpp
|
||||||
src/test/app/Offer_test.cpp
|
src/test/app/Offer_test.cpp
|
||||||
|
src/test/app/Oracle_test.cpp
|
||||||
src/test/app/OversizeMeta_test.cpp
|
src/test/app/OversizeMeta_test.cpp
|
||||||
src/test/app/Path_test.cpp
|
src/test/app/Path_test.cpp
|
||||||
src/test/app/PayChan_test.cpp
|
src/test/app/PayChan_test.cpp
|
||||||
@@ -964,6 +969,7 @@ if (tests)
|
|||||||
src/test/jtx/impl/AMMTest.cpp
|
src/test/jtx/impl/AMMTest.cpp
|
||||||
src/test/jtx/impl/Env.cpp
|
src/test/jtx/impl/Env.cpp
|
||||||
src/test/jtx/impl/JSONRPCClient.cpp
|
src/test/jtx/impl/JSONRPCClient.cpp
|
||||||
|
src/test/jtx/impl/Oracle.cpp
|
||||||
src/test/jtx/impl/TestHelpers.cpp
|
src/test/jtx/impl/TestHelpers.cpp
|
||||||
src/test/jtx/impl/WSClient.cpp
|
src/test/jtx/impl/WSClient.cpp
|
||||||
src/test/jtx/impl/acctdelete.cpp
|
src/test/jtx/impl/acctdelete.cpp
|
||||||
@@ -1089,6 +1095,7 @@ if (tests)
|
|||||||
src/test/rpc/DeliveredAmount_test.cpp
|
src/test/rpc/DeliveredAmount_test.cpp
|
||||||
src/test/rpc/Feature_test.cpp
|
src/test/rpc/Feature_test.cpp
|
||||||
src/test/rpc/GatewayBalances_test.cpp
|
src/test/rpc/GatewayBalances_test.cpp
|
||||||
|
src/test/rpc/GetAggregatePrice_test.cpp
|
||||||
src/test/rpc/GetCounts_test.cpp
|
src/test/rpc/GetCounts_test.cpp
|
||||||
src/test/rpc/JSONRPC_test.cpp
|
src/test/rpc/JSONRPC_test.cpp
|
||||||
src/test/rpc/KeyGeneration_test.cpp
|
src/test/rpc/KeyGeneration_test.cpp
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include <ripple/app/tx/impl/DID.h>
|
#include <ripple/app/tx/impl/DID.h>
|
||||||
#include <ripple/app/tx/impl/DeleteAccount.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/DepositPreauth.h>
|
||||||
#include <ripple/app/tx/impl/SetSignerList.h>
|
#include <ripple/app/tx/impl/SetSignerList.h>
|
||||||
#include <ripple/app/tx/impl/details/NFTokenUtils.h>
|
#include <ripple/app/tx/impl/details/NFTokenUtils.h>
|
||||||
@@ -146,6 +147,18 @@ removeDIDFromLedger(
|
|||||||
return DIDDelete::deleteSLE(view, sleDel, account, j);
|
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
|
// Return nullptr if the LedgerEntryType represents an obligation that can't
|
||||||
// be deleted. Otherwise return the pointer to the function that can delete
|
// be deleted. Otherwise return the pointer to the function that can delete
|
||||||
// the non-obligation
|
// the non-obligation
|
||||||
@@ -166,6 +179,8 @@ nonObligationDeleter(LedgerEntryType t)
|
|||||||
return removeNFTokenOfferFromLedger;
|
return removeNFTokenOfferFromLedger;
|
||||||
case ltDID:
|
case ltDID:
|
||||||
return removeDIDFromLedger;
|
return removeDIDFromLedger;
|
||||||
|
case ltORACLE:
|
||||||
|
return removeOracleFromLedger;
|
||||||
default:
|
default:
|
||||||
return nullptr;
|
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_CLAIM_ID:
|
||||||
case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID:
|
case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID:
|
||||||
case ltDID:
|
case ltDID:
|
||||||
|
case ltORACLE:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
invalidTypeAdded_ = true;
|
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/CreateTicket.h>
|
||||||
#include <ripple/app/tx/impl/DID.h>
|
#include <ripple/app/tx/impl/DID.h>
|
||||||
#include <ripple/app/tx/impl/DeleteAccount.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/DepositPreauth.h>
|
||||||
#include <ripple/app/tx/impl/Escrow.h>
|
#include <ripple/app/tx/impl/Escrow.h>
|
||||||
#include <ripple/app/tx/impl/NFTokenAcceptOffer.h>
|
#include <ripple/app/tx/impl/NFTokenAcceptOffer.h>
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
#include <ripple/app/tx/impl/PayChan.h>
|
#include <ripple/app/tx/impl/PayChan.h>
|
||||||
#include <ripple/app/tx/impl/Payment.h>
|
#include <ripple/app/tx/impl/Payment.h>
|
||||||
#include <ripple/app/tx/impl/SetAccount.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/SetRegularKey.h>
|
||||||
#include <ripple/app/tx/impl/SetSignerList.h>
|
#include <ripple/app/tx/impl/SetSignerList.h>
|
||||||
#include <ripple/app/tx/impl/SetTrust.h>
|
#include <ripple/app/tx/impl/SetTrust.h>
|
||||||
@@ -159,6 +161,10 @@ with_txn_type(TxType txnType, F&& f)
|
|||||||
return f.template operator()<DIDSet>();
|
return f.template operator()<DIDSet>();
|
||||||
case ttDID_DELETE:
|
case ttDID_DELETE:
|
||||||
return f.template operator()<DIDDelete>();
|
return f.template operator()<DIDDelete>();
|
||||||
|
case ttORACLE_SET:
|
||||||
|
return f.template operator()<SetOracle>();
|
||||||
|
case ttORACLE_DELETE:
|
||||||
|
return f.template operator()<DeleteOracle>();
|
||||||
default:
|
default:
|
||||||
throw UnknownTxnType(txnType);
|
throw UnknownTxnType(txnType);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,7 +145,11 @@ enum error_code_i {
|
|||||||
// AMM
|
// AMM
|
||||||
rpcISSUE_MALFORMED = 93,
|
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.
|
/** 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
|
// 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
|
// 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.
|
// 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.
|
/** Amendments that this server supports and the default voting behavior.
|
||||||
Whether they are enabled depends on the Rules defined in the validated
|
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 fixFillOrKill;
|
||||||
extern uint256 const fixNFTokenReserve;
|
extern uint256 const fixNFTokenReserve;
|
||||||
extern uint256 const fixInnerObjTemplate;
|
extern uint256 const fixInnerObjTemplate;
|
||||||
|
extern uint256 const featurePriceOracle;
|
||||||
|
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|
||||||
|
|||||||
@@ -283,6 +283,9 @@ xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq);
|
|||||||
Keylet
|
Keylet
|
||||||
did(AccountID const& account) noexcept;
|
did(AccountID const& account) noexcept;
|
||||||
|
|
||||||
|
Keylet
|
||||||
|
oracle(AccountID const& account, std::uint32_t const& documentID) noexcept;
|
||||||
|
|
||||||
} // namespace keylet
|
} // namespace keylet
|
||||||
|
|
||||||
// Everything below is deprecated and should be removed in favor of keylets:
|
// Everything below is deprecated and should be removed in favor of keylets:
|
||||||
|
|||||||
@@ -192,6 +192,11 @@ enum LedgerEntryType : std::uint16_t
|
|||||||
*/
|
*/
|
||||||
ltDID = 0x0049,
|
ltDID = 0x0049,
|
||||||
|
|
||||||
|
/** A ledger object which tracks Oracle
|
||||||
|
\sa keylet::oracle
|
||||||
|
*/
|
||||||
|
ltORACLE = 0x0080,
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
//---------------------------------------------------------------------------
|
||||||
/** A special type, matching any ledger entry type.
|
/** A special type, matching any ledger entry type.
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,31 @@ using TxID = uint256;
|
|||||||
*/
|
*/
|
||||||
std::uint16_t constexpr maxDeletableAMMTrustLines = 512;
|
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
|
} // namespace ripple
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ template <class>
|
|||||||
class STInteger;
|
class STInteger;
|
||||||
class STXChainBridge;
|
class STXChainBridge;
|
||||||
class STVector256;
|
class STVector256;
|
||||||
|
class STCurrency;
|
||||||
|
|
||||||
#pragma push_macro("XMACRO")
|
#pragma push_macro("XMACRO")
|
||||||
#undef XMACRO
|
#undef XMACRO
|
||||||
@@ -85,6 +86,7 @@ class STVector256;
|
|||||||
STYPE(STI_UINT512, 23) \
|
STYPE(STI_UINT512, 23) \
|
||||||
STYPE(STI_ISSUE, 24) \
|
STYPE(STI_ISSUE, 24) \
|
||||||
STYPE(STI_XCHAIN_BRIDGE, 25) \
|
STYPE(STI_XCHAIN_BRIDGE, 25) \
|
||||||
|
STYPE(STI_CURRENCY, 26) \
|
||||||
\
|
\
|
||||||
/* high-level types */ \
|
/* high-level types */ \
|
||||||
/* cannot be serialized inside other types */ \
|
/* cannot be serialized inside other types */ \
|
||||||
@@ -346,6 +348,7 @@ using SF_UINT512 = TypedField<STBitString<512>>;
|
|||||||
using SF_ACCOUNT = TypedField<STAccount>;
|
using SF_ACCOUNT = TypedField<STAccount>;
|
||||||
using SF_AMOUNT = TypedField<STAmount>;
|
using SF_AMOUNT = TypedField<STAmount>;
|
||||||
using SF_ISSUE = TypedField<STIssue>;
|
using SF_ISSUE = TypedField<STIssue>;
|
||||||
|
using SF_CURRENCY = TypedField<STCurrency>;
|
||||||
using SF_VL = TypedField<STBlob>;
|
using SF_VL = TypedField<STBlob>;
|
||||||
using SF_VECTOR256 = TypedField<STVector256>;
|
using SF_VECTOR256 = TypedField<STVector256>;
|
||||||
using SF_XCHAIN_BRIDGE = TypedField<STXChainBridge>;
|
using SF_XCHAIN_BRIDGE = TypedField<STXChainBridge>;
|
||||||
@@ -364,6 +367,7 @@ extern SF_UINT8 const sfCloseResolution;
|
|||||||
extern SF_UINT8 const sfMethod;
|
extern SF_UINT8 const sfMethod;
|
||||||
extern SF_UINT8 const sfTransactionResult;
|
extern SF_UINT8 const sfTransactionResult;
|
||||||
extern SF_UINT8 const sfWasLockingChainSend;
|
extern SF_UINT8 const sfWasLockingChainSend;
|
||||||
|
extern SF_UINT8 const sfScale;
|
||||||
|
|
||||||
// 8-bit integers (uncommon)
|
// 8-bit integers (uncommon)
|
||||||
extern SF_UINT8 const sfTickSize;
|
extern SF_UINT8 const sfTickSize;
|
||||||
@@ -400,6 +404,7 @@ extern SF_UINT32 const sfTransferRate;
|
|||||||
extern SF_UINT32 const sfWalletSize;
|
extern SF_UINT32 const sfWalletSize;
|
||||||
extern SF_UINT32 const sfOwnerCount;
|
extern SF_UINT32 const sfOwnerCount;
|
||||||
extern SF_UINT32 const sfDestinationTag;
|
extern SF_UINT32 const sfDestinationTag;
|
||||||
|
extern SF_UINT32 const sfLastUpdateTime;
|
||||||
|
|
||||||
// 32-bit integers (uncommon)
|
// 32-bit integers (uncommon)
|
||||||
extern SF_UINT32 const sfHighQualityIn;
|
extern SF_UINT32 const sfHighQualityIn;
|
||||||
@@ -435,6 +440,7 @@ extern SF_UINT32 const sfHookStateCount;
|
|||||||
extern SF_UINT32 const sfEmitGeneration;
|
extern SF_UINT32 const sfEmitGeneration;
|
||||||
extern SF_UINT32 const sfVoteWeight;
|
extern SF_UINT32 const sfVoteWeight;
|
||||||
extern SF_UINT32 const sfFirstNFTokenSequence;
|
extern SF_UINT32 const sfFirstNFTokenSequence;
|
||||||
|
extern SF_UINT32 const sfOracleDocumentID;
|
||||||
|
|
||||||
// 64-bit integers (common)
|
// 64-bit integers (common)
|
||||||
extern SF_UINT64 const sfIndexNext;
|
extern SF_UINT64 const sfIndexNext;
|
||||||
@@ -459,6 +465,7 @@ extern SF_UINT64 const sfReferenceCount;
|
|||||||
extern SF_UINT64 const sfXChainClaimID;
|
extern SF_UINT64 const sfXChainClaimID;
|
||||||
extern SF_UINT64 const sfXChainAccountCreateCount;
|
extern SF_UINT64 const sfXChainAccountCreateCount;
|
||||||
extern SF_UINT64 const sfXChainAccountClaimCount;
|
extern SF_UINT64 const sfXChainAccountClaimCount;
|
||||||
|
extern SF_UINT64 const sfAssetPrice;
|
||||||
|
|
||||||
// 128-bit
|
// 128-bit
|
||||||
extern SF_UINT128 const sfEmailHash;
|
extern SF_UINT128 const sfEmailHash;
|
||||||
@@ -554,6 +561,8 @@ extern SF_VL const sfMemoData;
|
|||||||
extern SF_VL const sfMemoFormat;
|
extern SF_VL const sfMemoFormat;
|
||||||
extern SF_VL const sfDIDDocument;
|
extern SF_VL const sfDIDDocument;
|
||||||
extern SF_VL const sfData;
|
extern SF_VL const sfData;
|
||||||
|
extern SF_VL const sfAssetClass;
|
||||||
|
extern SF_VL const sfProvider;
|
||||||
|
|
||||||
// variable length (uncommon)
|
// variable length (uncommon)
|
||||||
extern SF_VL const sfFulfillment;
|
extern SF_VL const sfFulfillment;
|
||||||
@@ -590,6 +599,10 @@ extern SF_ACCOUNT const sfIssuingChainDoor;
|
|||||||
// path set
|
// path set
|
||||||
extern SField const sfPaths;
|
extern SField const sfPaths;
|
||||||
|
|
||||||
|
// currency
|
||||||
|
extern SF_CURRENCY const sfBaseAsset;
|
||||||
|
extern SF_CURRENCY const sfQuoteAsset;
|
||||||
|
|
||||||
// issue
|
// issue
|
||||||
extern SF_ISSUE const sfAsset;
|
extern SF_ISSUE const sfAsset;
|
||||||
extern SF_ISSUE const sfAsset2;
|
extern SF_ISSUE const sfAsset2;
|
||||||
@@ -623,6 +636,7 @@ extern SField const sfHook;
|
|||||||
extern SField const sfVoteEntry;
|
extern SField const sfVoteEntry;
|
||||||
extern SField const sfAuctionSlot;
|
extern SField const sfAuctionSlot;
|
||||||
extern SField const sfAuthAccount;
|
extern SField const sfAuthAccount;
|
||||||
|
extern SField const sfPriceData;
|
||||||
|
|
||||||
extern SField const sfSigner;
|
extern SField const sfSigner;
|
||||||
extern SField const sfMajority;
|
extern SField const sfMajority;
|
||||||
@@ -651,6 +665,7 @@ extern SField const sfNFTokens;
|
|||||||
extern SField const sfHooks;
|
extern SField const sfHooks;
|
||||||
extern SField const sfVoteSlots;
|
extern SField const sfVoteSlots;
|
||||||
extern SField const sfAuthAccounts;
|
extern SField const sfAuthAccounts;
|
||||||
|
extern SField const sfPriceDataSeries;
|
||||||
|
|
||||||
// array of objects (uncommon)
|
// array of objects (uncommon)
|
||||||
extern SField const sfMajorities;
|
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/SOTemplate.h>
|
||||||
#include <ripple/protocol/STAmount.h>
|
#include <ripple/protocol/STAmount.h>
|
||||||
#include <ripple/protocol/STBase.h>
|
#include <ripple/protocol/STBase.h>
|
||||||
|
#include <ripple/protocol/STCurrency.h>
|
||||||
#include <ripple/protocol/STIssue.h>
|
#include <ripple/protocol/STIssue.h>
|
||||||
#include <ripple/protocol/STPathSet.h>
|
#include <ripple/protocol/STPathSet.h>
|
||||||
#include <ripple/protocol/STVector256.h>
|
#include <ripple/protocol/STVector256.h>
|
||||||
@@ -241,6 +242,8 @@ public:
|
|||||||
getFieldV256(SField const& field) const;
|
getFieldV256(SField const& field) const;
|
||||||
const STArray&
|
const STArray&
|
||||||
getFieldArray(SField const& field) const;
|
getFieldArray(SField const& field) const;
|
||||||
|
const STCurrency&
|
||||||
|
getFieldCurrency(SField const& field) const;
|
||||||
|
|
||||||
/** Get the value of a field.
|
/** Get the value of a field.
|
||||||
@param A TypedField built from an SField value representing the desired
|
@param A TypedField built from an SField value representing the desired
|
||||||
@@ -370,6 +373,8 @@ public:
|
|||||||
void
|
void
|
||||||
setFieldIssue(SField const& field, STIssue const&);
|
setFieldIssue(SField const& field, STIssue const&);
|
||||||
void
|
void
|
||||||
|
setFieldCurrency(SField const& field, STCurrency const&);
|
||||||
|
void
|
||||||
setFieldPathSet(SField const& field, STPathSet const&);
|
setFieldPathSet(SField const& field, STPathSet const&);
|
||||||
void
|
void
|
||||||
setFieldV256(SField const& field, STVector256 const& v);
|
setFieldV256(SField const& field, STVector256 const& v);
|
||||||
|
|||||||
@@ -135,6 +135,9 @@ enum TEMcodes : TERUnderlyingType {
|
|||||||
temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT,
|
temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT,
|
||||||
|
|
||||||
temEMPTY_DID,
|
temEMPTY_DID,
|
||||||
|
|
||||||
|
temARRAY_EMPTY,
|
||||||
|
temARRAY_TOO_LARGE,
|
||||||
};
|
};
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -330,7 +333,11 @@ enum TECcodes : TERUnderlyingType {
|
|||||||
tecXCHAIN_SELF_COMMIT = 184,
|
tecXCHAIN_SELF_COMMIT = 184,
|
||||||
tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR = 185,
|
tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR = 185,
|
||||||
tecXCHAIN_CREATE_ACCOUNT_DISABLED = 186,
|
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,
|
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.
|
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||||
|
|
||||||
For details, see: https://xrpl.org/amendments.html
|
For details, see: https://xrpl.org/amendments.html
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ constexpr static ErrorInfo unorderedErrorInfos[]{
|
|||||||
{rpcSTREAM_MALFORMED, "malformedStream", "Stream malformed.", 400},
|
{rpcSTREAM_MALFORMED, "malformedStream", "Stream malformed.", 400},
|
||||||
{rpcTOO_BUSY, "tooBusy", "The server is too busy to help you now.", 503},
|
{rpcTOO_BUSY, "tooBusy", "The server is too busy to help you now.", 503},
|
||||||
{rpcTXN_NOT_FOUND, "txnNotFound", "Transaction not found.", 404},
|
{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
|
// clang-format on
|
||||||
|
|
||||||
// Sort and validate unorderedErrorInfos at compile time. Should be
|
// 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(fixFillOrKill, Supported::yes, VoteBehavior::DefaultNo);
|
||||||
REGISTER_FIX (fixNFTokenReserve, Supported::yes, VoteBehavior::DefaultNo);
|
REGISTER_FIX (fixNFTokenReserve, Supported::yes, VoteBehavior::DefaultNo);
|
||||||
REGISTER_FIX(fixInnerObjTemplate, 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
|
// The following amendments are obsolete, but must remain supported
|
||||||
// because they could potentially get enabled.
|
// because they could potentially get enabled.
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
|||||||
XCHAIN_CLAIM_ID = 'Q',
|
XCHAIN_CLAIM_ID = 'Q',
|
||||||
XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K',
|
XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K',
|
||||||
DID = 'I',
|
DID = 'I',
|
||||||
|
ORACLE = 'R',
|
||||||
|
|
||||||
// No longer used or supported. Left here to reserve the space
|
// No longer used or supported. Left here to reserve the space
|
||||||
// to avoid accidental reuse.
|
// to avoid accidental reuse.
|
||||||
@@ -444,6 +445,12 @@ did(AccountID const& account) noexcept
|
|||||||
return {ltDID, indexHash(LedgerNameSpace::DID, account)};
|
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 keylet
|
||||||
|
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
@@ -138,6 +138,15 @@ InnerObjectFormats::InnerObjectFormats()
|
|||||||
{
|
{
|
||||||
{sfAccount, soeREQUIRED},
|
{sfAccount, soeREQUIRED},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
add(sfPriceData.jsonName.c_str(),
|
||||||
|
sfPriceData.getCode(),
|
||||||
|
{
|
||||||
|
{sfBaseAsset, soeREQUIRED},
|
||||||
|
{sfQuoteAsset, soeREQUIRED},
|
||||||
|
{sfAssetPrice, soeOPTIONAL},
|
||||||
|
{sfScale, soeDEFAULT},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerObjectFormats const&
|
InnerObjectFormats const&
|
||||||
|
|||||||
@@ -339,6 +339,22 @@ LedgerFormats::LedgerFormats()
|
|||||||
{sfPreviousTxnLgrSeq, soeREQUIRED}
|
{sfPreviousTxnLgrSeq, soeREQUIRED}
|
||||||
},
|
},
|
||||||
commonFields);
|
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
|
// clang-format on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfMetadata, "Metadata", METADATA
|
|||||||
CONSTRUCT_TYPED_SFIELD(sfCloseResolution, "CloseResolution", UINT8, 1);
|
CONSTRUCT_TYPED_SFIELD(sfCloseResolution, "CloseResolution", UINT8, 1);
|
||||||
CONSTRUCT_TYPED_SFIELD(sfMethod, "Method", UINT8, 2);
|
CONSTRUCT_TYPED_SFIELD(sfMethod, "Method", UINT8, 2);
|
||||||
CONSTRUCT_TYPED_SFIELD(sfTransactionResult, "TransactionResult", UINT8, 3);
|
CONSTRUCT_TYPED_SFIELD(sfTransactionResult, "TransactionResult", UINT8, 3);
|
||||||
|
CONSTRUCT_TYPED_SFIELD(sfScale, "Scale", UINT8, 4);
|
||||||
|
|
||||||
// 8-bit integers (uncommon)
|
// 8-bit integers (uncommon)
|
||||||
CONSTRUCT_TYPED_SFIELD(sfTickSize, "TickSize", UINT8, 16);
|
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(sfWalletSize, "WalletSize", UINT32, 12);
|
||||||
CONSTRUCT_TYPED_SFIELD(sfOwnerCount, "OwnerCount", UINT32, 13);
|
CONSTRUCT_TYPED_SFIELD(sfOwnerCount, "OwnerCount", UINT32, 13);
|
||||||
CONSTRUCT_TYPED_SFIELD(sfDestinationTag, "DestinationTag", UINT32, 14);
|
CONSTRUCT_TYPED_SFIELD(sfDestinationTag, "DestinationTag", UINT32, 14);
|
||||||
|
CONSTRUCT_TYPED_SFIELD(sfLastUpdateTime, "LastUpdateTime", UINT32, 15);
|
||||||
|
|
||||||
// 32-bit integers (uncommon)
|
// 32-bit integers (uncommon)
|
||||||
CONSTRUCT_TYPED_SFIELD(sfHighQualityIn, "HighQualityIn", UINT32, 16);
|
CONSTRUCT_TYPED_SFIELD(sfHighQualityIn, "HighQualityIn", UINT32, 16);
|
||||||
@@ -164,6 +166,7 @@ CONSTRUCT_TYPED_SFIELD(sfEmitGeneration, "EmitGeneration", UINT32,
|
|||||||
// 47 is reserved for LockCount(Hooks)
|
// 47 is reserved for LockCount(Hooks)
|
||||||
CONSTRUCT_TYPED_SFIELD(sfVoteWeight, "VoteWeight", UINT32, 48);
|
CONSTRUCT_TYPED_SFIELD(sfVoteWeight, "VoteWeight", UINT32, 48);
|
||||||
CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 50);
|
CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 50);
|
||||||
|
CONSTRUCT_TYPED_SFIELD(sfOracleDocumentID, "OracleDocumentID", UINT32, 51);
|
||||||
|
|
||||||
// 64-bit integers (common)
|
// 64-bit integers (common)
|
||||||
CONSTRUCT_TYPED_SFIELD(sfIndexNext, "IndexNext", UINT64, 1);
|
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(sfXChainClaimID, "XChainClaimID", UINT64, 20);
|
||||||
CONSTRUCT_TYPED_SFIELD(sfXChainAccountCreateCount, "XChainAccountCreateCount", UINT64, 21);
|
CONSTRUCT_TYPED_SFIELD(sfXChainAccountCreateCount, "XChainAccountCreateCount", UINT64, 21);
|
||||||
CONSTRUCT_TYPED_SFIELD(sfXChainAccountClaimCount, "XChainAccountClaimCount", UINT64, 22);
|
CONSTRUCT_TYPED_SFIELD(sfXChainAccountClaimCount, "XChainAccountClaimCount", UINT64, 22);
|
||||||
|
CONSTRUCT_TYPED_SFIELD(sfAssetPrice, "AssetPrice", UINT64, 23);
|
||||||
|
|
||||||
// 128-bit
|
// 128-bit
|
||||||
CONSTRUCT_TYPED_SFIELD(sfEmailHash, "EmailHash", UINT128, 1);
|
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(sfHookParameterValue, "HookParameterValue", VL, 25);
|
||||||
CONSTRUCT_TYPED_SFIELD(sfDIDDocument, "DIDDocument", VL, 26);
|
CONSTRUCT_TYPED_SFIELD(sfDIDDocument, "DIDDocument", VL, 26);
|
||||||
CONSTRUCT_TYPED_SFIELD(sfData, "Data", VL, 27);
|
CONSTRUCT_TYPED_SFIELD(sfData, "Data", VL, 27);
|
||||||
|
CONSTRUCT_TYPED_SFIELD(sfAssetClass, "AssetClass", VL, 28);
|
||||||
|
CONSTRUCT_TYPED_SFIELD(sfProvider, "Provider", VL, 29);
|
||||||
|
|
||||||
// account
|
// account
|
||||||
CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1);
|
CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1);
|
||||||
@@ -331,6 +337,10 @@ CONSTRUCT_TYPED_SFIELD(sfNFTokenOffers, "NFTokenOffers", VECTOR25
|
|||||||
// path set
|
// path set
|
||||||
CONSTRUCT_UNTYPED_SFIELD(sfPaths, "Paths", PATHSET, 1);
|
CONSTRUCT_UNTYPED_SFIELD(sfPaths, "Paths", PATHSET, 1);
|
||||||
|
|
||||||
|
// currency
|
||||||
|
CONSTRUCT_TYPED_SFIELD(sfBaseAsset, "BaseAsset", CURRENCY, 1);
|
||||||
|
CONSTRUCT_TYPED_SFIELD(sfQuoteAsset, "QuoteAsset", CURRENCY, 2);
|
||||||
|
|
||||||
// issue
|
// issue
|
||||||
CONSTRUCT_TYPED_SFIELD(sfLockingChainIssue, "LockingChainIssue", ISSUE, 1);
|
CONSTRUCT_TYPED_SFIELD(sfLockingChainIssue, "LockingChainIssue", ISSUE, 1);
|
||||||
CONSTRUCT_TYPED_SFIELD(sfIssuingChainIssue, "IssuingChainIssue", ISSUE, 2);
|
CONSTRUCT_TYPED_SFIELD(sfIssuingChainIssue, "IssuingChainIssue", ISSUE, 2);
|
||||||
@@ -379,6 +389,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement,
|
|||||||
CONSTRUCT_UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement,
|
CONSTRUCT_UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement,
|
||||||
"XChainCreateAccountAttestationCollectionElement",
|
"XChainCreateAccountAttestationCollectionElement",
|
||||||
OBJECT, 31);
|
OBJECT, 31);
|
||||||
|
CONSTRUCT_UNTYPED_SFIELD(sfPriceData, "PriceData", OBJECT, 32);
|
||||||
|
|
||||||
// array of objects
|
// array of objects
|
||||||
// ARRAY/1 is reserved for end of array
|
// ARRAY/1 is reserved for end of array
|
||||||
@@ -406,7 +417,8 @@ CONSTRUCT_UNTYPED_SFIELD(sfXChainClaimAttestations,
|
|||||||
CONSTRUCT_UNTYPED_SFIELD(sfXChainCreateAccountAttestations,
|
CONSTRUCT_UNTYPED_SFIELD(sfXChainCreateAccountAttestations,
|
||||||
"XChainCreateAccountAttestations",
|
"XChainCreateAccountAttestations",
|
||||||
ARRAY, 22);
|
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);
|
CONSTRUCT_UNTYPED_SFIELD(sfAuthAccounts, "AuthAccounts", ARRAY, 25);
|
||||||
|
|
||||||
// clang-format on
|
// 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/STAccount.h>
|
||||||
#include <ripple/protocol/STArray.h>
|
#include <ripple/protocol/STArray.h>
|
||||||
#include <ripple/protocol/STBlob.h>
|
#include <ripple/protocol/STBlob.h>
|
||||||
|
#include <ripple/protocol/STCurrency.h>
|
||||||
#include <ripple/protocol/STObject.h>
|
#include <ripple/protocol/STObject.h>
|
||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
@@ -642,6 +643,13 @@ STObject::getFieldArray(SField const& field) const
|
|||||||
return getFieldByConstRef<STArray>(field, empty);
|
return getFieldByConstRef<STArray>(field, empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
STCurrency const&
|
||||||
|
STObject::getFieldCurrency(SField const& field) const
|
||||||
|
{
|
||||||
|
static STCurrency const empty{};
|
||||||
|
return getFieldByConstRef<STCurrency>(field, empty);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
STObject::set(std::unique_ptr<STBase> v)
|
STObject::set(std::unique_ptr<STBase> v)
|
||||||
{
|
{
|
||||||
@@ -730,6 +738,12 @@ STObject::setFieldAmount(SField const& field, STAmount const& v)
|
|||||||
setFieldUsingAssignment(field, v);
|
setFieldUsingAssignment(field, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
STObject::setFieldCurrency(SField const& field, STCurrency const& v)
|
||||||
|
{
|
||||||
|
setFieldUsingAssignment(field, v);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
STObject::setFieldIssue(SField const& field, STIssue const& v)
|
STObject::setFieldIssue(SField const& field, STIssue const& v)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -760,6 +760,19 @@ parseLeaf(
|
|||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
error = bad_type(json_name, fieldName);
|
error = bad_type(json_name, fieldName);
|
||||||
return ret;
|
return ret;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#include <ripple/protocol/STBase.h>
|
#include <ripple/protocol/STBase.h>
|
||||||
#include <ripple/protocol/STBitString.h>
|
#include <ripple/protocol/STBitString.h>
|
||||||
#include <ripple/protocol/STBlob.h>
|
#include <ripple/protocol/STBlob.h>
|
||||||
|
#include <ripple/protocol/STCurrency.h>
|
||||||
#include <ripple/protocol/STInteger.h>
|
#include <ripple/protocol/STInteger.h>
|
||||||
#include <ripple/protocol/STIssue.h>
|
#include <ripple/protocol/STIssue.h>
|
||||||
#include <ripple/protocol/STObject.h>
|
#include <ripple/protocol/STObject.h>
|
||||||
@@ -167,6 +168,9 @@ STVar::STVar(SerialIter& sit, SField const& name, int depth)
|
|||||||
case STI_XCHAIN_BRIDGE:
|
case STI_XCHAIN_BRIDGE:
|
||||||
construct<STXChainBridge>(sit, name);
|
construct<STXChainBridge>(sit, name);
|
||||||
return;
|
return;
|
||||||
|
case STI_CURRENCY:
|
||||||
|
construct<STCurrency>(sit, name);
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
Throw<std::runtime_error>("Unknown object type");
|
Throw<std::runtime_error>("Unknown object type");
|
||||||
}
|
}
|
||||||
@@ -228,6 +232,9 @@ STVar::STVar(SerializedTypeID id, SField const& name)
|
|||||||
case STI_XCHAIN_BRIDGE:
|
case STI_XCHAIN_BRIDGE:
|
||||||
construct<STXChainBridge>(name);
|
construct<STXChainBridge>(name);
|
||||||
return;
|
return;
|
||||||
|
case STI_CURRENCY:
|
||||||
|
construct<STCurrency>(name);
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
Throw<std::runtime_error>("Unknown object type");
|
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_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(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(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(tefALREADY, "The exact transaction was already in this ledger."),
|
||||||
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
|
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_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_MIN_ACCOUNT_CREATE_AMOUNT, "Malformed: Bad min account create amount."),
|
||||||
MAKE_ERROR(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT, "Malformed: Bad reward 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(terRETRY, "Retry transaction."),
|
||||||
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),
|
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),
|
||||||
|
|||||||
@@ -483,6 +483,25 @@ TxFormats::TxFormats()
|
|||||||
commonFields);
|
commonFields);
|
||||||
|
|
||||||
add(jss::DIDDelete, ttDID_DELETE, {}, 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&
|
TxFormats const&
|
||||||
|
|||||||
@@ -60,8 +60,11 @@ JSS(Amount); // in: TransactionSign; field.
|
|||||||
JSS(Amount2); // in/out: AMM IOU/XRP pool, deposit, withdraw amount
|
JSS(Amount2); // in/out: AMM IOU/XRP pool, deposit, withdraw amount
|
||||||
JSS(Asset); // in: AMM Asset1
|
JSS(Asset); // in: AMM Asset1
|
||||||
JSS(Asset2); // in: AMM Asset2
|
JSS(Asset2); // in: AMM Asset2
|
||||||
|
JSS(AssetClass); // in: Oracle
|
||||||
|
JSS(AssetPrice); // in: Oracle
|
||||||
JSS(AuthAccount); // in: AMM Auction Slot
|
JSS(AuthAccount); // in: AMM Auction Slot
|
||||||
JSS(AuthAccounts); // in: AMM Auction Slot
|
JSS(AuthAccounts); // in: AMM Auction Slot
|
||||||
|
JSS(BaseAsset); // in: Oracle
|
||||||
JSS(Bridge); // ledger type.
|
JSS(Bridge); // ledger type.
|
||||||
JSS(Check); // ledger type.
|
JSS(Check); // ledger type.
|
||||||
JSS(CheckCancel); // transaction type.
|
JSS(CheckCancel); // transaction type.
|
||||||
@@ -89,6 +92,7 @@ JSS(Flags); // in/out: TransactionSign; field.
|
|||||||
JSS(incomplete_shards); // out: OverlayImpl, PeerImp
|
JSS(incomplete_shards); // out: OverlayImpl, PeerImp
|
||||||
JSS(Invalid); //
|
JSS(Invalid); //
|
||||||
JSS(LastLedgerSequence); // in: TransactionSign; field
|
JSS(LastLedgerSequence); // in: TransactionSign; field
|
||||||
|
JSS(LastUpdateTime); // field.
|
||||||
JSS(LedgerHashes); // ledger type.
|
JSS(LedgerHashes); // ledger type.
|
||||||
JSS(LimitAmount); // field.
|
JSS(LimitAmount); // field.
|
||||||
JSS(BidMax); // in: AMM Bid
|
JSS(BidMax); // in: AMM Bid
|
||||||
@@ -108,16 +112,26 @@ JSS(Offer); // ledger type.
|
|||||||
JSS(OfferCancel); // transaction type.
|
JSS(OfferCancel); // transaction type.
|
||||||
JSS(OfferCreate); // transaction type.
|
JSS(OfferCreate); // transaction type.
|
||||||
JSS(OfferSequence); // field.
|
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(Paths); // in/out: TransactionSign
|
||||||
JSS(PayChannel); // ledger type.
|
JSS(PayChannel); // ledger type.
|
||||||
JSS(Payment); // transaction type.
|
JSS(Payment); // transaction type.
|
||||||
JSS(PaymentChannelClaim); // transaction type.
|
JSS(PaymentChannelClaim); // transaction type.
|
||||||
JSS(PaymentChannelCreate); // transaction type.
|
JSS(PaymentChannelCreate); // transaction type.
|
||||||
JSS(PaymentChannelFund); // transaction type.
|
JSS(PaymentChannelFund); // transaction type.
|
||||||
|
JSS(PriceDataSeries); // field.
|
||||||
|
JSS(PriceData); // field.
|
||||||
|
JSS(Provider); // field.
|
||||||
|
JSS(QuoteAsset); // in: Oracle.
|
||||||
JSS(RippleState); // ledger type.
|
JSS(RippleState); // ledger type.
|
||||||
JSS(SLE_hit_rate); // out: GetCounts.
|
JSS(SLE_hit_rate); // out: GetCounts.
|
||||||
JSS(SetFee); // transaction type.
|
JSS(SetFee); // transaction type.
|
||||||
JSS(UNLModify); // transaction type.
|
JSS(UNLModify); // transaction type.
|
||||||
|
JSS(Scale); // field.
|
||||||
JSS(SettleDelay); // in: TransactionSign
|
JSS(SettleDelay); // in: TransactionSign
|
||||||
JSS(SendMax); // in: TransactionSign
|
JSS(SendMax); // in: TransactionSign
|
||||||
JSS(Sequence); // in/out: TransactionSign; field.
|
JSS(Sequence); // in/out: TransactionSign; field.
|
||||||
@@ -135,6 +149,7 @@ JSS(TradingFee); // in/out: AMM trading fee
|
|||||||
JSS(TransactionType); // in: TransactionSign.
|
JSS(TransactionType); // in: TransactionSign.
|
||||||
JSS(TransferRate); // in: TransferRate.
|
JSS(TransferRate); // in: TransferRate.
|
||||||
JSS(TrustSet); // transaction type.
|
JSS(TrustSet); // transaction type.
|
||||||
|
JSS(URI); // field.
|
||||||
JSS(VoteSlots); // out: AMM Vote
|
JSS(VoteSlots); // out: AMM Vote
|
||||||
JSS(XChainAddAccountCreateAttestation); // transaction type.
|
JSS(XChainAddAccountCreateAttestation); // transaction type.
|
||||||
JSS(XChainAddClaimAttestation); // transaction type.
|
JSS(XChainAddClaimAttestation); // transaction type.
|
||||||
@@ -202,6 +217,7 @@ JSS(avg_bps_sent); // out: Peers
|
|||||||
JSS(balance); // out: AccountLines
|
JSS(balance); // out: AccountLines
|
||||||
JSS(balances); // out: GatewayBalances
|
JSS(balances); // out: GatewayBalances
|
||||||
JSS(base); // out: LogLevel
|
JSS(base); // out: LogLevel
|
||||||
|
JSS(base_asset); // in: get_aggregate_price
|
||||||
JSS(base_fee); // out: NetworkOPs
|
JSS(base_fee); // out: NetworkOPs
|
||||||
JSS(base_fee_xrp); // out: NetworkOPs
|
JSS(base_fee_xrp); // out: NetworkOPs
|
||||||
JSS(bids); // out: Subscribe
|
JSS(bids); // out: Subscribe
|
||||||
@@ -299,6 +315,7 @@ JSS(enabled); // out: AmendmentTable
|
|||||||
JSS(engine_result); // out: NetworkOPs, TransactionSign, Submit
|
JSS(engine_result); // out: NetworkOPs, TransactionSign, Submit
|
||||||
JSS(engine_result_code); // out: NetworkOPs, TransactionSign, Submit
|
JSS(engine_result_code); // out: NetworkOPs, TransactionSign, Submit
|
||||||
JSS(engine_result_message); // out: NetworkOPs, TransactionSign, Submit
|
JSS(engine_result_message); // out: NetworkOPs, TransactionSign, Submit
|
||||||
|
JSS(entire_set); // out: get_aggregate_price
|
||||||
JSS(ephemeral_key); // out: ValidatorInfo
|
JSS(ephemeral_key); // out: ValidatorInfo
|
||||||
// in/out: Manifest
|
// in/out: Manifest
|
||||||
JSS(error); // out: error
|
JSS(error); // out: error
|
||||||
@@ -458,6 +475,8 @@ JSS(max_ledger); // in/out: LedgerCleaner
|
|||||||
JSS(max_queue_size); // out: TxQ
|
JSS(max_queue_size); // out: TxQ
|
||||||
JSS(max_spend_drops); // out: AccountInfo
|
JSS(max_spend_drops); // out: AccountInfo
|
||||||
JSS(max_spend_drops_total); // 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_fee); // out: TxQ
|
||||||
JSS(median_level); // out: TxQ
|
JSS(median_level); // out: TxQ
|
||||||
JSS(message); // error.
|
JSS(message); // error.
|
||||||
@@ -515,6 +534,9 @@ JSS(open); // out: handlers/Ledger
|
|||||||
JSS(open_ledger_cost); // out: SubmitTransaction
|
JSS(open_ledger_cost); // out: SubmitTransaction
|
||||||
JSS(open_ledger_fee); // out: TxQ
|
JSS(open_ledger_fee); // out: TxQ
|
||||||
JSS(open_ledger_level); // 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); // in: LedgerEntry, out: NetworkOPs
|
||||||
JSS(owner_funds); // in/out: Ledger, NetworkOPs, AcceptedLedgerTx
|
JSS(owner_funds); // in/out: Ledger, NetworkOPs, AcceptedLedgerTx
|
||||||
JSS(page_index);
|
JSS(page_index);
|
||||||
@@ -561,6 +583,7 @@ JSS(queue); // in: AccountInfo
|
|||||||
JSS(queue_data); // out: AccountInfo
|
JSS(queue_data); // out: AccountInfo
|
||||||
JSS(queued); // out: SubmitTransaction
|
JSS(queued); // out: SubmitTransaction
|
||||||
JSS(queued_duration_us);
|
JSS(queued_duration_us);
|
||||||
|
JSS(quote_asset); // in: get_aggregate_price
|
||||||
JSS(random); // out: Random
|
JSS(random); // out: Random
|
||||||
JSS(raw_meta); // out: AcceptedLedgerTx
|
JSS(raw_meta); // out: AcceptedLedgerTx
|
||||||
JSS(receive_currencies); // out: AccountCurrencies
|
JSS(receive_currencies); // out: AccountCurrencies
|
||||||
@@ -615,12 +638,14 @@ JSS(signing_keys); // out: ValidatorList
|
|||||||
JSS(signing_time); // out: NetworkOPs
|
JSS(signing_time); // out: NetworkOPs
|
||||||
JSS(signer_list); // in: AccountObjects
|
JSS(signer_list); // in: AccountObjects
|
||||||
JSS(signer_lists); // in/out: AccountInfo
|
JSS(signer_lists); // in/out: AccountInfo
|
||||||
|
JSS(size); // out: get_aggregate_price
|
||||||
JSS(snapshot); // in: Subscribe
|
JSS(snapshot); // in: Subscribe
|
||||||
JSS(source_account); // in: PathRequest, RipplePathFind
|
JSS(source_account); // in: PathRequest, RipplePathFind
|
||||||
JSS(source_amount); // in: PathRequest, RipplePathFind
|
JSS(source_amount); // in: PathRequest, RipplePathFind
|
||||||
JSS(source_currencies); // in: PathRequest, RipplePathFind
|
JSS(source_currencies); // in: PathRequest, RipplePathFind
|
||||||
JSS(source_tag); // out: AccountChannels
|
JSS(source_tag); // out: AccountChannels
|
||||||
JSS(stand_alone); // out: NetworkOPs
|
JSS(stand_alone); // out: NetworkOPs
|
||||||
|
JSS(standard_deviation); // out: get_aggregate_price
|
||||||
JSS(start); // in: TxHistory
|
JSS(start); // in: TxHistory
|
||||||
JSS(started);
|
JSS(started);
|
||||||
JSS(state); // out: Logic.h, ServerState, LedgerData
|
JSS(state); // out: Logic.h, ServerState, LedgerData
|
||||||
@@ -636,6 +661,7 @@ JSS(sub_index); // in: LedgerEntry
|
|||||||
JSS(subcommand); // in: PathFind
|
JSS(subcommand); // in: PathFind
|
||||||
JSS(success); // rpc
|
JSS(success); // rpc
|
||||||
JSS(supported); // out: AmendmentTableImpl
|
JSS(supported); // out: AmendmentTableImpl
|
||||||
|
JSS(sync_mode); // in: Submit
|
||||||
JSS(system_time_offset); // out: NetworkOPs
|
JSS(system_time_offset); // out: NetworkOPs
|
||||||
JSS(tag); // out: Peers
|
JSS(tag); // out: Peers
|
||||||
JSS(taker); // in: Subscribe, BookOffers
|
JSS(taker); // in: Subscribe, BookOffers
|
||||||
@@ -649,9 +675,12 @@ JSS(ticket_count); // out: AccountInfo
|
|||||||
JSS(ticket_seq); // in: LedgerEntry
|
JSS(ticket_seq); // in: LedgerEntry
|
||||||
JSS(time);
|
JSS(time);
|
||||||
JSS(timeouts); // out: InboundLedger
|
JSS(timeouts); // out: InboundLedger
|
||||||
|
JSS(time_threshold); // in/out: Oracle aggregate
|
||||||
JSS(time_interval); // out: AMM Auction Slot
|
JSS(time_interval); // out: AMM Auction Slot
|
||||||
JSS(track); // out: PeerImp
|
JSS(track); // out: PeerImp
|
||||||
JSS(traffic); // out: Overlay
|
JSS(traffic); // out: Overlay
|
||||||
|
JSS(trim); // in: get_aggregate_price
|
||||||
|
JSS(trimmed_set); // out: get_aggregate_price
|
||||||
JSS(total); // out: counters
|
JSS(total); // out: counters
|
||||||
JSS(total_bytes_recv); // out: Peers
|
JSS(total_bytes_recv); // out: Peers
|
||||||
JSS(total_bytes_sent); // 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
|
Json::Value
|
||||||
doGetCounts(RPC::JsonContext&);
|
doGetCounts(RPC::JsonContext&);
|
||||||
Json::Value
|
Json::Value
|
||||||
|
doGetAggregatePrice(RPC::JsonContext&);
|
||||||
|
Json::Value
|
||||||
doLedgerAccept(RPC::JsonContext&);
|
doLedgerAccept(RPC::JsonContext&);
|
||||||
Json::Value
|
Json::Value
|
||||||
doLedgerCleaner(RPC::JsonContext&);
|
doLedgerCleaner(RPC::JsonContext&);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include <ripple/app/main/Application.h>
|
#include <ripple/app/main/Application.h>
|
||||||
#include <ripple/basics/StringUtilities.h>
|
#include <ripple/basics/StringUtilities.h>
|
||||||
#include <ripple/basics/strHex.h>
|
#include <ripple/basics/strHex.h>
|
||||||
|
#include <ripple/beast/core/LexicalCast.h>
|
||||||
#include <ripple/json/json_errors.h>
|
#include <ripple/json/json_errors.h>
|
||||||
#include <ripple/ledger/ReadView.h>
|
#include <ripple/ledger/ReadView.h>
|
||||||
#include <ripple/net/RPCErr.h>
|
#include <ripple/net/RPCErr.h>
|
||||||
@@ -598,6 +599,51 @@ doLedgerEntry(RPC::JsonContext& context)
|
|||||||
else
|
else
|
||||||
uNodeIndex = keylet::did(*account).key;
|
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
|
else
|
||||||
{
|
{
|
||||||
if (context.params.isMember("params") &&
|
if (context.params.isMember("params") &&
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ Handler const handlerArray[]{
|
|||||||
{"gateway_balances", byRef(&doGatewayBalances), Role::USER, NO_CONDITION},
|
{"gateway_balances", byRef(&doGatewayBalances), Role::USER, NO_CONDITION},
|
||||||
#endif
|
#endif
|
||||||
{"get_counts", byRef(&doGetCounts), Role::ADMIN, NO_CONDITION},
|
{"get_counts", byRef(&doGetCounts), Role::ADMIN, NO_CONDITION},
|
||||||
|
{"get_aggregate_price",
|
||||||
|
byRef(&doGetAggregatePrice),
|
||||||
|
Role::USER,
|
||||||
|
NO_CONDITION},
|
||||||
{"feature", byRef(&doFeature), Role::ADMIN, NO_CONDITION},
|
{"feature", byRef(&doFeature), Role::ADMIN, NO_CONDITION},
|
||||||
{"fee", byRef(&doFee), Role::USER, NEEDS_CURRENT_LEDGER},
|
{"fee", byRef(&doFee), Role::USER, NEEDS_CURRENT_LEDGER},
|
||||||
{"fetch_info", byRef(&doFetchInfo), Role::ADMIN, NO_CONDITION},
|
{"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};
|
std::pair<RPC::Status, LedgerEntryType> result{RPC::Status::OK, ltANY};
|
||||||
if (params.isMember(jss::type))
|
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{
|
types{
|
||||||
{{jss::account, ltACCOUNT_ROOT},
|
{{jss::account, ltACCOUNT_ROOT},
|
||||||
{jss::amendments, ltAMENDMENTS},
|
{jss::amendments, ltAMENDMENTS},
|
||||||
@@ -956,7 +956,8 @@ chooseLedgerEntryType(Json::Value const& params)
|
|||||||
{jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID},
|
{jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID},
|
||||||
{jss::xchain_owned_create_account_claim_id,
|
{jss::xchain_owned_create_account_claim_id,
|
||||||
ltXCHAIN_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];
|
auto const& p = params[jss::type];
|
||||||
if (!p.isString())
|
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