diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 8d2ff6cbaef..0c922da4760 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -98,6 +98,7 @@ target_sources (xrpl_core PRIVATE src/ripple/protocol/impl/STArray.cpp src/ripple/protocol/impl/STBase.cpp src/ripple/protocol/impl/STBlob.cpp + src/ripple/protocol/impl/STCurrency.cpp src/ripple/protocol/impl/STInteger.cpp src/ripple/protocol/impl/STLedgerEntry.cpp src/ripple/protocol/impl/STObject.cpp @@ -518,6 +519,7 @@ target_sources (rippled PRIVATE src/ripple/app/tx/impl/CreateOffer.cpp src/ripple/app/tx/impl/CreateTicket.cpp src/ripple/app/tx/impl/DeleteAccount.cpp + src/ripple/app/tx/impl/DeleteOracle.cpp src/ripple/app/tx/impl/DepositPreauth.cpp src/ripple/app/tx/impl/Escrow.cpp src/ripple/app/tx/impl/InvariantCheck.cpp @@ -530,6 +532,7 @@ target_sources (rippled PRIVATE src/ripple/app/tx/impl/PayChan.cpp src/ripple/app/tx/impl/Payment.cpp src/ripple/app/tx/impl/SetAccount.cpp + src/ripple/app/tx/impl/SetOracle.cpp src/ripple/app/tx/impl/SetRegularKey.cpp src/ripple/app/tx/impl/SetSignerList.cpp src/ripple/app/tx/impl/SetTrust.cpp @@ -686,6 +689,7 @@ target_sources (rippled PRIVATE src/ripple/rpc/handlers/FetchInfo.cpp src/ripple/rpc/handlers/GatewayBalances.cpp src/ripple/rpc/handlers/GetCounts.cpp + src/ripple/rpc/handlers/GetAggregatePrice.cpp src/ripple/rpc/handlers/LedgerAccept.cpp src/ripple/rpc/handlers/LedgerCleanerHandler.cpp src/ripple/rpc/handlers/LedgerClosed.cpp @@ -804,6 +808,7 @@ if (tests) src/test/app/NFTokenDir_test.cpp src/test/app/OfferStream_test.cpp src/test/app/Offer_test.cpp + src/test/app/Oracle_test.cpp src/test/app/OversizeMeta_test.cpp src/test/app/Path_test.cpp src/test/app/PayChan_test.cpp @@ -927,6 +932,7 @@ if (tests) src/test/jtx/impl/AMMTest.cpp src/test/jtx/impl/Env.cpp src/test/jtx/impl/JSONRPCClient.cpp + src/test/jtx/impl/Oracle.cpp src/test/jtx/impl/TestHelpers.cpp src/test/jtx/impl/WSClient.cpp src/test/jtx/impl/acctdelete.cpp @@ -1051,6 +1057,7 @@ if (tests) src/test/rpc/DeliveredAmount_test.cpp src/test/rpc/Feature_test.cpp src/test/rpc/GatewayBalances_test.cpp + src/test/rpc/GetAggregatePrice_test.cpp src/test/rpc/GetCounts_test.cpp src/test/rpc/JSONRPC_test.cpp src/test/rpc/KeyGeneration_test.cpp diff --git a/src/ripple/app/tx/impl/DeleteOracle.cpp b/src/ripple/app/tx/impl/DeleteOracle.cpp new file mode 100644 index 00000000000..3f8f5fdd279 --- /dev/null +++ b/src/ripple/app/tx/impl/DeleteOracle.cpp @@ -0,0 +1,84 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include +#include + +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 (auto const sle = ctx.view.read(keylet::oracle(ctx.tx[sfOracleID])); + !sle) + { + JLOG(ctx.j.debug()) << "Oracle Delete: Oracle does not exist."; + return tecNO_ENTRY; + } + else if (ctx.tx.getAccountID(sfAccount) != sle->getAccountID(sfOwner)) + { + JLOG(ctx.j.debug()) << "Oracle Delete: invalid account."; + return tecNO_PERMISSION; + } + return tesSUCCESS; +} + +TER +DeleteOracle::doApply() +{ + // This is the ledger view that we work against. Transactions are applied + // as we go on processing transactions. + Sandbox sb(&ctx_.view()); + + if (auto sle = sb.peek(keylet::oracle(ctx_.tx[sfOracleID])); !sle) + return tecINTERNAL; + else + { + sb.erase(sle); + sb.apply(ctx_.rawView()); + } + + adjustOwnerCount(sb, sb.peek(keylet::account(account_)), -1, j_); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/DeleteOracle.h b/src/ripple/app/tx/impl/DeleteOracle.h new file mode 100644 index 00000000000..1e755912612 --- /dev/null +++ b/src/ripple/app/tx/impl/DeleteOracle.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { + +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; +}; + +} // namespace ripple + +#endif // RIPPLE_TX_DELETEORACLE_H_INCLUDED diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index 7b1ac7d08df..0bec2179583 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -391,6 +391,7 @@ LedgerEntryTypesMatch::visitEntry( case ltBRIDGE: case ltXCHAIN_OWNED_CLAIM_ID: case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: + case ltORACLE: break; default: invalidTypeAdded_ = true; diff --git a/src/ripple/app/tx/impl/SetOracle.cpp b/src/ripple/app/tx/impl/SetOracle.cpp new file mode 100644 index 00000000000..916aff54902 --- /dev/null +++ b/src/ripple/app/tx/impl/SetOracle.cpp @@ -0,0 +1,245 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +SetOracle::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featurePriceOracle)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const flags = ctx.tx.getFlags(); + if (flags & tfOracleMask) + { + JLOG(ctx.j.debug()) << "Oracle Set: invalid flags."; + return temINVALID_FLAG; + } + + if (ctx.tx.getFieldArray(sfPriceDataSeries).size() > 10) + { + JLOG(ctx.j.debug()) << "Oracle Set: price data series too large"; + return temARRAY_SIZE; + } + + if (ctx.tx.getFieldVL(sfProvider).size() > 256) + { + JLOG(ctx.j.debug()) << "Oracle Set: provider too large"; + return temMALFORMED; + } + + if (ctx.tx.getFieldVL(sfURI).size() > 256) + { + JLOG(ctx.j.debug()) << "Oracle Set: URI too large"; + return temMALFORMED; + } + + if (ctx.tx.getFieldVL(sfSymbolClass).size() > 12) + { + JLOG(ctx.j.debug()) << "Oracle Set: symbol class too large"; + return temMALFORMED; + } + + return preflight2(ctx); +} + +TER +SetOracle::preclaim(PreclaimContext const& ctx) +{ + hash_set pairs; + for (auto const& entry : ctx.tx.getFieldArray(sfPriceDataSeries)) + { + auto const hash = sha512Half( + entry.getFieldCurrency(sfSymbol).currency(), + entry.getFieldCurrency(sfPriceUnit).currency()); + if (pairs.contains(hash)) + return tecDUPLICATE; + pairs.emplace(hash); + } + + if (auto const sle = ctx.view.read(keylet::oracle(ctx.tx[sfOracleID]))) + { + if (ctx.tx[sfAccount] != sle->getAccountID(sfOwner)) + return tecNO_PERMISSION; + + if (ctx.tx.isFieldPresent(sfProvider) || + ctx.tx.isFieldPresent(sfSymbolClass)) + return temMALFORMED; + + for (auto const& entry : sle->getFieldArray(sfPriceDataSeries)) + { + auto const hash = sha512Half( + entry.getFieldCurrency(sfSymbol).currency(), + entry.getFieldCurrency(sfPriceUnit).currency()); + if (!pairs.contains(hash)) + pairs.emplace(hash); + } + } + else + { + if (!ctx.tx.isFieldPresent(sfProvider) || + !ctx.tx.isFieldPresent(sfSymbolClass)) + return temMALFORMED; + } + + if (pairs.size() == 0 || pairs.size() > 10) + return temARRAY_SIZE; + + auto const sleSetter = + ctx.view.read(keylet::account(ctx.tx.getAccountID(sfAccount))); + if (!sleSetter) + return {tefINTERNAL}; + + auto const reserve = ctx.view.fees().accountReserve( + sleSetter->getFieldU32(sfOwnerCount) + pairs.size() <= 5 ? 1 : 2); + auto const balance = sleSetter->getFieldAmount(sfBalance); + + if (balance < reserve) + { + JLOG(ctx.j.debug()) << "Oracle Set: insufficient reserve"; + return tecINSUFFICIENT_RESERVE; + } + + return tesSUCCESS; +} + +static std::pair +applySet( + ApplyContext& ctx_, + Sandbox& sb, + AccountID const& account_, + beast::Journal j_) +{ + auto const oracleID = keylet::oracle(ctx_.tx[sfOracleID]); + + if (auto sle = sb.peek(oracleID)) + { + // update Oracle + + hash_map pairs; + // collect current pairs + for (auto const& entry : sle->getFieldArray(sfPriceDataSeries)) + { + STObject priceData{sfPriceData}; + priceData.setFieldCurrency( + sfSymbol, entry.getFieldCurrency(sfSymbol)); + priceData.setFieldCurrency( + sfPriceUnit, entry.getFieldCurrency(sfPriceUnit)); + priceData.setFieldU64(sfSymbolPrice, 0); + priceData.setFieldU8(sfScale, 0); + pairs.emplace( + sha512Half( + entry.getFieldCurrency(sfSymbol).currency(), + entry.getFieldCurrency(sfPriceUnit).currency()), + std::move(priceData)); + } + // update/add pairs + for (auto const& entry : ctx_.tx.getFieldArray(sfPriceDataSeries)) + { + auto const hash = sha512Half( + entry.getFieldCurrency(sfSymbol).currency(), + entry.getFieldCurrency(sfPriceUnit).currency()); + if (auto iter = pairs.find(hash); iter != pairs.end()) + { + iter->second.setFieldU64( + sfSymbolPrice, entry.getFieldU64(sfSymbolPrice)); + iter->second.setFieldU8(sfScale, entry.getFieldU8(sfScale)); + } + else + { + STObject priceData{sfPriceData}; + priceData.setFieldCurrency( + sfSymbol, entry.getFieldCurrency(sfSymbol)); + priceData.setFieldCurrency( + sfPriceUnit, entry.getFieldCurrency(sfPriceUnit)); + priceData.setFieldU64( + sfSymbolPrice, entry.getFieldU64(sfSymbolPrice)); + priceData.setFieldU8(sfScale, entry.getFieldU8(sfScale)); + pairs.emplace( + sha512Half( + entry.getFieldCurrency(sfSymbol).currency(), + entry.getFieldCurrency(sfPriceUnit).currency()), + std::move(priceData)); + } + } + STArray updatedSeries; + for (auto 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]); + + sb.update(sle); + } + else + { + // create new Oracle + + sle = std::make_shared(keylet::oracle(ctx_.tx[sfOracleID])); + sle->setAccountID(sfOwner, ctx_.tx.getAccountID(sfAccount)); + sle->setFieldVL(sfProvider, ctx_.tx[sfProvider]); + if (ctx_.tx.isFieldPresent(sfURI)) + sle->setFieldVL(sfURI, ctx_.tx[sfURI]); + sle->setFieldArray( + sfPriceDataSeries, ctx_.tx.getFieldArray(sfPriceDataSeries)); + sle->setFieldVL(sfSymbolClass, ctx_.tx[sfSymbolClass]); + sle->setFieldU32(sfLastUpdateTime, ctx_.tx[sfLastUpdateTime]); + + auto page = sb.dirInsert( + keylet::ownerDir(account_), sle->key(), describeOwnerDir(account_)); + if (!page) + return {tecDIR_FULL, false}; + + (*sle)[sfOwnerNode] = *page; + + adjustOwnerCount(sb, sb.peek(keylet::account(account_)), 1, j_); + + sb.insert(sle); + } + + return {tesSUCCESS, true}; +} + +TER +SetOracle::doApply() +{ + // This is the ledger view that we work against. Transactions are applied + // as we go on processing transactions. + Sandbox sb(&ctx_.view()); + + auto const result = applySet(ctx_, sb, account_, j_); + if (result.second) + sb.apply(ctx_.rawView()); + + return result.first; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/SetOracle.h b/src/ripple/app/tx/impl/SetOracle.h new file mode 100644 index 00000000000..270bffda53c --- /dev/null +++ b/src/ripple/app/tx/impl/SetOracle.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { + +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 diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index 4c882f3fb8a..e02d33d0c45 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -154,6 +156,10 @@ with_txn_type(TxType txnType, F&& f) return f.template operator()(); case ttXCHAIN_ACCOUNT_CREATE_COMMIT: return f.template operator()(); + case ttORACLE_SET: + return f.template operator()(); + case ttORACLE_DELETE: + return f.template operator()(); default: throw UnknownTxnType(txnType); } diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp index 1242fdc2a68..0b17c652b62 100644 --- a/src/ripple/net/impl/RPCCall.cpp +++ b/src/ripple/net/impl/RPCCall.cpp @@ -1261,6 +1261,7 @@ class RPCParser {"fetch_info", &RPCParser::parseFetchInfo, 0, 1}, {"gateway_balances", &RPCParser::parseGatewayBalances, 1, -1}, {"get_counts", &RPCParser::parseGetCounts, 0, 1}, + {"get_aggregate_price", &RPCParser::parseAsIs, 2, 3}, {"json", &RPCParser::parseJson, 2, 2}, {"json2", &RPCParser::parseJson2, 1, 1}, {"ledger", &RPCParser::parseLedger, 0, 2}, diff --git a/src/ripple/protocol/ErrorCodes.h b/src/ripple/protocol/ErrorCodes.h index 8319b69c8c2..ad849f2b4ef 100644 --- a/src/ripple/protocol/ErrorCodes.h +++ b/src/ripple/protocol/ErrorCodes.h @@ -145,7 +145,11 @@ enum error_code_i { // AMM rpcISSUE_MALFORMED = 93, - rpcLAST = rpcISSUE_MALFORMED // rpcLAST should always equal the last code.= + // Oracle + rpcORACLE_MALFORMED = 94, + + rpcLAST = + rpcORACLE_MALFORMED // rpcLAST should always equal the last code.= }; /** Codes returned in the `warnings` array of certain RPC commands. diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 17aca813f71..58a881cc0ac 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 63; +static constexpr std::size_t numFeatures = 64; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -350,6 +350,7 @@ extern uint256 const fixReducedOffersV1; extern uint256 const featureClawback; extern uint256 const featureXChainBridge; extern uint256 const fixDisallowIncomingV1; +extern uint256 const featurePriceOracle; } // namespace ripple diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 0c83f7765a4..8640a29f5c9 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -280,6 +280,9 @@ xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq); Keylet xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq); +Keylet +oracle(uint256 const& id) noexcept; + } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index e907e299f52..117e398c537 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -186,6 +186,11 @@ enum LedgerEntryType : std::uint16_t */ ltAMM = 0x0079, + /** A ledger object which tracks Oracle + \sa keylet::oracle + */ + ltORACLE = 0x0080, + //--------------------------------------------------------------------------- /** A special type, matching any ledger entry type. diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 7c802a64e1d..8b3e21e690a 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -51,6 +51,7 @@ template class STInteger; class STXChainBridge; class STVector256; +class STCurrency; enum SerializedTypeID { // special types @@ -81,6 +82,7 @@ enum SerializedTypeID { STI_UINT512 = 23, STI_ISSUE = 24, STI_XCHAIN_BRIDGE = 25, + STI_CURRENCY = 26, // high level types // cannot be serialized inside other types @@ -318,6 +320,7 @@ using SF_UINT512 = TypedField>; using SF_ACCOUNT = TypedField; using SF_AMOUNT = TypedField; using SF_ISSUE = TypedField; +using SF_CURRENCY = TypedField; using SF_VL = TypedField; using SF_VECTOR256 = TypedField; using SF_XCHAIN_BRIDGE = TypedField; @@ -336,6 +339,7 @@ extern SF_UINT8 const sfCloseResolution; extern SF_UINT8 const sfMethod; extern SF_UINT8 const sfTransactionResult; extern SF_UINT8 const sfWasLockingChainSend; +extern SF_UINT8 const sfScale; // 8-bit integers (uncommon) extern SF_UINT8 const sfTickSize; @@ -372,6 +376,7 @@ extern SF_UINT32 const sfTransferRate; extern SF_UINT32 const sfWalletSize; extern SF_UINT32 const sfOwnerCount; extern SF_UINT32 const sfDestinationTag; +extern SF_UINT32 const sfLastUpdateTime; // 32-bit integers (uncommon) extern SF_UINT32 const sfHighQualityIn; @@ -407,6 +412,7 @@ extern SF_UINT32 const sfHookStateCount; extern SF_UINT32 const sfEmitGeneration; extern SF_UINT32 const sfVoteWeight; extern SF_UINT32 const sfFirstNFTokenSequence; +extern SF_UINT32 const sfOracleSequence; // 64-bit integers (common) extern SF_UINT64 const sfIndexNext; @@ -431,6 +437,7 @@ extern SF_UINT64 const sfReferenceCount; extern SF_UINT64 const sfXChainClaimID; extern SF_UINT64 const sfXChainAccountCreateCount; extern SF_UINT64 const sfXChainAccountClaimCount; +extern SF_UINT64 const sfSymbolPrice; // 128-bit extern SF_UINT128 const sfEmailHash; @@ -456,6 +463,7 @@ extern SF_UINT256 const sfEmitParentTxnID; extern SF_UINT256 const sfEmitNonce; extern SF_UINT256 const sfEmitHookHash; extern SF_UINT256 const sfAMMID; +extern SF_UINT256 const sfOracleID; // 256-bit (uncommon) extern SF_UINT256 const sfBookDirectory; @@ -524,6 +532,8 @@ extern SF_VL const sfCreateCode; extern SF_VL const sfMemoType; extern SF_VL const sfMemoData; extern SF_VL const sfMemoFormat; +extern SF_VL const sfSymbolClass; +extern SF_VL const sfProvider; // variable length (uncommon) extern SF_VL const sfFulfillment; @@ -560,6 +570,10 @@ extern SF_ACCOUNT const sfIssuingChainDoor; // path set extern SField const sfPaths; +// currency +extern SF_CURRENCY const sfSymbol; +extern SF_CURRENCY const sfPriceUnit; + // issue extern SF_ISSUE const sfAsset; extern SF_ISSUE const sfAsset2; @@ -593,6 +607,7 @@ extern SField const sfHook; extern SField const sfVoteEntry; extern SField const sfAuctionSlot; extern SField const sfAuthAccount; +extern SField const sfPriceData; extern SField const sfSigner; extern SField const sfMajority; @@ -621,6 +636,7 @@ extern SField const sfNFTokens; extern SField const sfHooks; extern SField const sfVoteSlots; extern SField const sfAuthAccounts; +extern SField const sfPriceDataSeries; // array of objects (uncommon) extern SField const sfMajorities; diff --git a/src/ripple/protocol/STCurrency.h b/src/ripple/protocol/STCurrency.h new file mode 100644 index 00000000000..f855c24832e --- /dev/null +++ b/src/ripple/protocol/STCurrency.h @@ -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 +#include +#include +#include +#include + +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 + 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 diff --git a/src/ripple/protocol/STObject.h b/src/ripple/protocol/STObject.h index 19d4b264734..602cf3ab5b4 100644 --- a/src/ripple/protocol/STObject.h +++ b/src/ripple/protocol/STObject.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -236,6 +237,8 @@ class STObject : public STBase, public CountedObject getFieldV256(SField const& field) const; const STArray& getFieldArray(SField const& field) const; + const STCurrency& + getFieldCurrency(SField const& field) const; /** Get the value of a field. @param A TypedField built from an SField value representing the desired @@ -365,6 +368,8 @@ class STObject : public STBase, public CountedObject void setFieldIssue(SField const& field, STIssue const&); void + setFieldCurrency(SField const& field, STCurrency const&); + void setFieldPathSet(SField const& field, STPathSet const&); void setFieldV256(SField const& field, STVector256 const& v); diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 4cabac1dd13..05deda6d69b 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -132,6 +132,7 @@ enum TEMcodes : TERUnderlyingType { temXCHAIN_BRIDGE_NONDOOR_OWNER, temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT, temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT, + temARRAY_SIZE, }; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/TxFlags.h b/src/ripple/protocol/TxFlags.h index ba2b97562db..1f3700dce11 100644 --- a/src/ripple/protocol/TxFlags.h +++ b/src/ripple/protocol/TxFlags.h @@ -185,6 +185,12 @@ constexpr std::uint32_t tfDepositMask = ~(tfUniversal | tfDepositSubTx); constexpr std::uint32_t tfClearAccountCreateAmount = 0x00010000; constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreateAmount); +// Oracle Flags: +constexpr std::uint32_t tfSimpleAverage = 0x00010000; +constexpr std::uint32_t tfMedian = 0x00020000; +constexpr std::uint32_t tfTrimmedMean = 0x00040000; +constexpr std::uint32_t tfOracleMask = ~tfUniversal; + // clang-format on } // namespace ripple diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index d8785f3ea1d..6eea1ffc82c 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -185,6 +185,12 @@ enum TxType : std::uint16_t ttXCHAIN_CREATE_BRIDGE = 48, + /** This transaction type creates an Oracle instance */ + ttORACLE_SET = 49, + + /** This transaction type deletes an Oracle instance */ + ttORACLE_DELETE = 50, + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/src/ripple/protocol/impl/ErrorCodes.cpp b/src/ripple/protocol/impl/ErrorCodes.cpp index 319bd8e28c2..3af48891c78 100644 --- a/src/ripple/protocol/impl/ErrorCodes.cpp +++ b/src/ripple/protocol/impl/ErrorCodes.cpp @@ -109,7 +109,8 @@ constexpr static ErrorInfo unorderedErrorInfos[]{ {rpcSTREAM_MALFORMED, "malformedStream", "Stream malformed.", 400}, {rpcTOO_BUSY, "tooBusy", "The server is too busy to help you now.", 503}, {rpcTXN_NOT_FOUND, "txnNotFound", "Transaction not found.", 404}, - {rpcUNKNOWN_COMMAND, "unknownCmd", "Unknown method.", 405}}; + {rpcUNKNOWN_COMMAND, "unknownCmd", "Unknown method.", 405}, + {rpcORACLE_MALFORMED, "oracleMalformed", "Oracle request is malformed.", 400}}; // clang-format on // Sort and validate unorderedErrorInfos at compile time. Should be diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 6a3430f4f50..4d0c9cfb65c 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -457,6 +457,7 @@ REGISTER_FEATURE(Clawback, Supported::yes, VoteBehavior::De REGISTER_FEATURE(AMM, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(XChainBridge, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX(fixDisallowIncomingV1, Supported::yes, VoteBehavior::DefaultNo); +REGISTER_FEATURE(PriceOracle, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 3fef856b365..cde7d2bfe17 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -71,6 +71,7 @@ enum class LedgerNameSpace : std::uint16_t { BRIDGE = 'H', XCHAIN_CLAIM_ID = 'Q', XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K', + ORACLE = 'R', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -437,6 +438,12 @@ xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq) seq)}; } +Keylet +oracle(uint256 const& id) noexcept +{ + return {ltORACLE, indexHash(LedgerNameSpace::ORACLE, id)}; +} + } // namespace keylet } // namespace ripple diff --git a/src/ripple/protocol/impl/InnerObjectFormats.cpp b/src/ripple/protocol/impl/InnerObjectFormats.cpp index 58f4392f536..1e25dde729a 100644 --- a/src/ripple/protocol/impl/InnerObjectFormats.cpp +++ b/src/ripple/protocol/impl/InnerObjectFormats.cpp @@ -135,6 +135,15 @@ InnerObjectFormats::InnerObjectFormats() { {sfAccount, soeREQUIRED}, }); + + add(sfPriceData.jsonName.c_str(), + sfPriceData.getCode(), + { + {sfSymbol, soeREQUIRED}, + {sfPriceUnit, soeREQUIRED}, + {sfSymbolPrice, soeREQUIRED}, + {sfScale, soeREQUIRED}, + }); } InnerObjectFormats const& diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index e5313a8c1f9..4d49f7f3dff 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -326,6 +326,23 @@ LedgerFormats::LedgerFormats() {sfPreviousTxnLgrSeq, soeREQUIRED} }, commonFields); + + add(jss::Oracle, + ltORACLE, + { + {sfOwner, soeREQUIRED}, + {sfSymbol, soeREQUIRED}, + {sfProvider, soeREQUIRED}, + {sfPriceDataSeries, soeREQUIRED}, + {sfSymbolClass, soeREQUIRED}, + {sfLastUpdateTime, soeREQUIRED}, + {sfURI, soeOPTIONAL}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED} + }, + commonFields); + // clang-format on } diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 517971dbf07..dbeba94a3fd 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -91,6 +91,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfMetadata, "Metadata", METADATA CONSTRUCT_TYPED_SFIELD(sfCloseResolution, "CloseResolution", UINT8, 1); CONSTRUCT_TYPED_SFIELD(sfMethod, "Method", UINT8, 2); CONSTRUCT_TYPED_SFIELD(sfTransactionResult, "TransactionResult", UINT8, 3); +CONSTRUCT_TYPED_SFIELD(sfScale, "Scale", UINT8, 4); // 8-bit integers (uncommon) CONSTRUCT_TYPED_SFIELD(sfTickSize, "TickSize", UINT8, 16); @@ -128,6 +129,7 @@ CONSTRUCT_TYPED_SFIELD(sfTransferRate, "TransferRate", UINT32, CONSTRUCT_TYPED_SFIELD(sfWalletSize, "WalletSize", UINT32, 12); CONSTRUCT_TYPED_SFIELD(sfOwnerCount, "OwnerCount", UINT32, 13); CONSTRUCT_TYPED_SFIELD(sfDestinationTag, "DestinationTag", UINT32, 14); +CONSTRUCT_TYPED_SFIELD(sfLastUpdateTime, "LastUpdateTime", UINT32, 15); // 32-bit integers (uncommon) CONSTRUCT_TYPED_SFIELD(sfHighQualityIn, "HighQualityIn", UINT32, 16); @@ -164,6 +166,7 @@ CONSTRUCT_TYPED_SFIELD(sfEmitGeneration, "EmitGeneration", UINT32, // 47 is reserved for LockCount(Hooks) CONSTRUCT_TYPED_SFIELD(sfVoteWeight, "VoteWeight", UINT32, 48); CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 50); +CONSTRUCT_TYPED_SFIELD(sfOracleSequence, "OracleSequence", UINT32, 51); // 64-bit integers (common) CONSTRUCT_TYPED_SFIELD(sfIndexNext, "IndexNext", UINT64, 1); @@ -188,6 +191,7 @@ CONSTRUCT_TYPED_SFIELD(sfReferenceCount, "ReferenceCount", U CONSTRUCT_TYPED_SFIELD(sfXChainClaimID, "XChainClaimID", UINT64, 20); CONSTRUCT_TYPED_SFIELD(sfXChainAccountCreateCount, "XChainAccountCreateCount", UINT64, 21); CONSTRUCT_TYPED_SFIELD(sfXChainAccountClaimCount, "XChainAccountClaimCount", UINT64, 22); +CONSTRUCT_TYPED_SFIELD(sfSymbolPrice, "SymbolPrice", UINT64, 23); // 128-bit CONSTRUCT_TYPED_SFIELD(sfEmailHash, "EmailHash", UINT128, 1); @@ -233,6 +237,7 @@ CONSTRUCT_TYPED_SFIELD(sfHookStateKey, "HookStateKey", UINT256, CONSTRUCT_TYPED_SFIELD(sfHookHash, "HookHash", UINT256, 31); CONSTRUCT_TYPED_SFIELD(sfHookNamespace, "HookNamespace", UINT256, 32); CONSTRUCT_TYPED_SFIELD(sfHookSetTxnID, "HookSetTxnID", UINT256, 33); +CONSTRUCT_TYPED_SFIELD(sfOracleID, "OracleID", UINT256, 34); // currency amount (common) CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", AMOUNT, 1); @@ -298,6 +303,8 @@ CONSTRUCT_TYPED_SFIELD(sfHookStateData, "HookStateData", VL, CONSTRUCT_TYPED_SFIELD(sfHookReturnString, "HookReturnString", VL, 23); CONSTRUCT_TYPED_SFIELD(sfHookParameterName, "HookParameterName", VL, 24); CONSTRUCT_TYPED_SFIELD(sfHookParameterValue, "HookParameterValue", VL, 25); +CONSTRUCT_TYPED_SFIELD(sfSymbolClass, "SymbolClass", VL, 26); +CONSTRUCT_TYPED_SFIELD(sfProvider, "Provider", VL, 27); // account CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1); @@ -329,6 +336,10 @@ CONSTRUCT_TYPED_SFIELD(sfNFTokenOffers, "NFTokenOffers", VECTOR25 // path set CONSTRUCT_UNTYPED_SFIELD(sfPaths, "Paths", PATHSET, 1); +// currency +CONSTRUCT_TYPED_SFIELD(sfSymbol, "Symbol", CURRENCY, 1); +CONSTRUCT_TYPED_SFIELD(sfPriceUnit, "PriceUnit", CURRENCY, 2); + // issue CONSTRUCT_TYPED_SFIELD(sfLockingChainIssue, "LockingChainIssue", ISSUE, 1); CONSTRUCT_TYPED_SFIELD(sfIssuingChainIssue, "IssuingChainIssue", ISSUE, 2); @@ -377,6 +388,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, CONSTRUCT_UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, "XChainCreateAccountAttestationCollectionElement", OBJECT, 31); +CONSTRUCT_UNTYPED_SFIELD(sfPriceData, "PriceData", OBJECT, 32); // array of objects // ARRAY/1 is reserved for end of array @@ -404,7 +416,8 @@ CONSTRUCT_UNTYPED_SFIELD(sfXChainClaimAttestations, CONSTRUCT_UNTYPED_SFIELD(sfXChainCreateAccountAttestations, "XChainCreateAccountAttestations", ARRAY, 22); -// 23 and 24 are unused and available for use +// 23 is unused and available for use +CONSTRUCT_UNTYPED_SFIELD(sfPriceDataSeries, "PriceDataSeries", ARRAY, 24); CONSTRUCT_UNTYPED_SFIELD(sfAuthAccounts, "AuthAccounts", ARRAY, 25); // clang-format on diff --git a/src/ripple/protocol/impl/STCurrency.cpp b/src/ripple/protocol/impl/STCurrency.cpp new file mode 100644 index 00000000000..2cd2e150227 --- /dev/null +++ b/src/ripple/protocol/impl/STCurrency.cpp @@ -0,0 +1,117 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include + +#include + +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 +{ + Json::Value jv; + jv[jss::currency] = to_string(currency_); + return jv; +} + +void +STCurrency::add(Serializer& s) const +{ + s.addBitString(currency_); +} + +bool +STCurrency::isEquivalent(const STBase& t) const +{ + const STCurrency* v = dynamic_cast(&t); + return v && (*v == *this); +} + +bool +STCurrency::isDefault() const +{ + return isXRP(currency_); +} + +std::unique_ptr +STCurrency::construct(SerialIter& sit, SField const& name) +{ + return std::make_unique(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( + "currencyFromJson currency must be a string Json value"); + } + + auto const currency = to_currency(v.asString()); + if (currency == badCurrency() || currency == noCurrency()) + { + Throw( + "currencyFromJson currency must be a valid currency"); + } + + return STCurrency{name, currency}; +} + +} // namespace ripple diff --git a/src/ripple/protocol/impl/STObject.cpp b/src/ripple/protocol/impl/STObject.cpp index 5bafbcfce54..35e249d0313 100644 --- a/src/ripple/protocol/impl/STObject.cpp +++ b/src/ripple/protocol/impl/STObject.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include namespace ripple { @@ -627,6 +628,13 @@ STObject::getFieldArray(SField const& field) const return getFieldByConstRef(field, empty); } +STCurrency const& +STObject::getFieldCurrency(SField const& field) const +{ + static STCurrency const empty{}; + return getFieldByConstRef(field, empty); +} + void STObject::set(std::unique_ptr v) { @@ -709,6 +717,12 @@ STObject::setFieldAmount(SField const& field, STAmount const& v) setFieldUsingAssignment(field, v); } +void +STObject::setFieldCurrency(SField const& field, STCurrency const& v) +{ + setFieldUsingAssignment(field, v); +} + void STObject::setFieldIssue(SField const& field, STIssue const& v) { diff --git a/src/ripple/protocol/impl/STParsedJSON.cpp b/src/ripple/protocol/impl/STParsedJSON.cpp index fb960e6f11e..6727fe7388c 100644 --- a/src/ripple/protocol/impl/STParsedJSON.cpp +++ b/src/ripple/protocol/impl/STParsedJSON.cpp @@ -760,6 +760,19 @@ parseLeaf( } break; + case STI_CURRENCY: + try + { + ret = detail::make_stvar( + currencyFromJson(field, value)); + } + catch (std::exception const&) + { + error = invalid_data(json_name, fieldName); + return ret; + } + break; + default: error = bad_type(json_name, fieldName); return ret; diff --git a/src/ripple/protocol/impl/STVar.cpp b/src/ripple/protocol/impl/STVar.cpp index 2ec55ccaf03..adda165901f 100644 --- a/src/ripple/protocol/impl/STVar.cpp +++ b/src/ripple/protocol/impl/STVar.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -167,6 +168,9 @@ STVar::STVar(SerialIter& sit, SField const& name, int depth) case STI_XCHAIN_BRIDGE: construct(sit, name); return; + case STI_CURRENCY: + construct(sit, name); + return; default: Throw("Unknown object type"); } @@ -228,6 +232,9 @@ STVar::STVar(SerializedTypeID id, SField const& name) case STI_XCHAIN_BRIDGE: construct(name); return; + case STI_CURRENCY: + construct(name); + return; default: Throw("Unknown object type"); } diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index 87dae362598..2605da00227 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -198,6 +198,7 @@ transResults() MAKE_ERROR(temXCHAIN_BRIDGE_NONDOOR_OWNER, "Malformed: Bridge owner must be one of the door accounts."), MAKE_ERROR(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT, "Malformed: Bad min account create amount."), MAKE_ERROR(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT, "Malformed: Bad reward amount."), + MAKE_ERROR(temARRAY_SIZE, "Malformed: Invalid array size."), MAKE_ERROR(terRETRY, "Retry transaction."), MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."), diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 755401bda92..3ce6368f6ec 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -472,6 +472,25 @@ TxFormats::TxFormats() {sfSignatureReward, soeREQUIRED}, }, commonFields); + + add(jss::OracleSet, + ttORACLE_SET, + { + {sfOracleID, soeREQUIRED}, + {sfProvider, soeOPTIONAL}, + {sfURI, soeOPTIONAL}, + {sfSymbolClass, soeOPTIONAL}, + {sfLastUpdateTime, soeREQUIRED}, + {sfPriceDataSeries, soeREQUIRED}, + }, + commonFields); + + add(jss::OracleDelete, + ttORACLE_DELETE, + { + {sfOracleID, soeREQUIRED}, + }, + commonFields); } TxFormats const& diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index e31a1cb3bf8..7d1fef821e5 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -85,6 +85,7 @@ JSS(Flags); // in/out: TransactionSign; field. JSS(incomplete_shards); // out: OverlayImpl, PeerImp JSS(Invalid); // JSS(LastLedgerSequence); // in: TransactionSign; field +JSS(LastUpdateTime); // field. JSS(LedgerHashes); // ledger type. JSS(LimitAmount); // field. JSS(BidMax); // in: AMM Bid @@ -104,16 +105,26 @@ JSS(Offer); // ledger type. JSS(OfferCancel); // transaction type. JSS(OfferCreate); // transaction type. JSS(OfferSequence); // field. +JSS(Oracle); // ledger type. +JSS(OracleDelete); // transaction type. +JSS(OracleID); // field +JSS(OracleSet); // transaction type. +JSS(Owner); // field JSS(Paths); // in/out: TransactionSign JSS(PayChannel); // ledger type. JSS(Payment); // transaction type. JSS(PaymentChannelClaim); // transaction type. JSS(PaymentChannelCreate); // transaction type. JSS(PaymentChannelFund); // transaction type. +JSS(PriceDataSeries); // field. +JSS(PriceData); // field. +JSS(PriceUnit); // field. +JSS(Provider); // field. JSS(RippleState); // ledger type. JSS(SLE_hit_rate); // out: GetCounts. JSS(SetFee); // transaction type. JSS(UNLModify); // transaction type. +JSS(Scale); // field. JSS(SettleDelay); // in: TransactionSign JSS(SendMax); // in: TransactionSign JSS(Sequence); // in/out: TransactionSign; field. @@ -122,6 +133,9 @@ JSS(SetRegularKey); // transaction type. JSS(SignerList); // ledger type. JSS(SignerListSet); // transaction type. JSS(SigningPubKey); // field. +JSS(Symbol); // field. +JSS(SymbolClass); // field. +JSS(SymbolPrice); // field. JSS(TakerGets); // field. JSS(TakerPays); // field. JSS(Ticket); // ledger type. @@ -131,6 +145,7 @@ JSS(TradingFee); // in/out: AMM trading fee JSS(TransactionType); // in: TransactionSign. JSS(TransferRate); // in: TransferRate. JSS(TrustSet); // transaction type. +JSS(URI); // field. JSS(VoteSlots); // out: AMM Vote JSS(XChainAddAccountCreateAttestation); // transaction type. JSS(XChainAddClaimAttestation); // transaction type. @@ -441,6 +456,7 @@ JSS(max_ledger); // in/out: LedgerCleaner JSS(max_queue_size); // out: TxQ JSS(max_spend_drops); // out: AccountInfo JSS(max_spend_drops_total); // out: AccountInfo +JSS(median); // out: get_aggregate_price JSS(median_fee); // out: TxQ JSS(median_level); // out: TxQ JSS(message); // error. @@ -496,6 +512,10 @@ JSS(open); // out: handlers/Ledger JSS(open_ledger_cost); // out: SubmitTransaction JSS(open_ledger_fee); // out: TxQ JSS(open_ledger_level); // out: TxQ +JSS(oracle); // in: LedgerEntry +JSS(oracles); // in: get_aggregate_price +JSS(oracle_id); // in: get_aggregate_price +JSS(oracle_sequence); // in: get_aggregate_price JSS(owner); // in: LedgerEntry, out: NetworkOPs JSS(owner_funds); // in/out: Ledger, NetworkOPs, AcceptedLedgerTx JSS(page_index); @@ -521,6 +541,7 @@ JSS(ports); // out: NetworkOPs JSS(previous); // out: Reservations JSS(previous_ledger); // out: LedgerPropose JSS(price); // out: amm_info, AuctionSlot +JSS(price_unit); // in: get_aggregate_price JSS(proof); // in: BookOffers JSS(propose_seq); // out: LedgerPropose JSS(proposers); // out: NetworkOPs, LedgerConsensus @@ -596,6 +617,7 @@ JSS(signing_keys); // out: ValidatorList JSS(signing_time); // out: NetworkOPs JSS(signer_list); // in: AccountObjects JSS(signer_lists); // in/out: AccountInfo +JSS(simple_average); // out: get_aggregate_price JSS(snapshot); // in: Subscribe JSS(source_account); // in: PathRequest, RipplePathFind JSS(source_amount); // in: PathRequest, RipplePathFind @@ -617,6 +639,7 @@ JSS(sub_index); // in: LedgerEntry JSS(subcommand); // in: PathFind JSS(success); // rpc JSS(supported); // out: AmendmentTableImpl +JSS(symbol); // in: get_aggregate_price JSS(sync_mode); // in: Submit JSS(system_time_offset); // out: NetworkOPs JSS(tag); // out: Peers @@ -634,6 +657,7 @@ JSS(timeouts); // out: InboundLedger JSS(time_interval); // out: AMM Auction Slot JSS(track); // out: PeerImp JSS(traffic); // out: Overlay +JSS(trim); // in: get_aggregate_price JSS(total); // out: counters JSS(total_bytes_recv); // out: Peers JSS(total_bytes_sent); // out: Peers @@ -649,6 +673,7 @@ JSS(transfer_rate); // out: nft_info (clio) JSS(transitions); // out: NetworkOPs JSS(treenode_cache_size); // out: GetCounts JSS(treenode_track_size); // out: GetCounts +JSS(trimmed_mean); // out: get_aggregate_price JSS(trusted); // out: UnlList JSS(trusted_validator_keys); // out: ValidatorList JSS(tx); // out: STTx, AccountTx* diff --git a/src/ripple/rpc/handlers/GetAggregatePrice.cpp b/src/ripple/rpc/handlers/GetAggregatePrice.cpp new file mode 100644 index 00000000000..7c3bf7b6158 --- /dev/null +++ b/src/ripple/rpc/handlers/GetAggregatePrice.cpp @@ -0,0 +1,197 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include +#include +#include + +namespace ripple { + +/** + * oracles: array of OracleID + * symbol: is the symbol to be priced + * priceUnit: is the denomination in which the prices are expressed + * trim : percentage of outliers to trim [optional] + * flags : specify aggregation type. at least one flag must be included + * tfSimpleAverage : 0x01 + * tfMedian : 0x02 + * tfTrimmedMedian : 0x04 + */ +Json::Value +doGetAggregatePrice(RPC::JsonContext& context) +{ + auto const& params(context.params); + std::shared_ptr ledger; + auto result = RPC::lookupLedger(ledger, context); + if (!ledger) + return result; + + if (!params.isMember(jss::oracles)) + return RPC::missing_field_error(jss::oracles); + if (!params[jss::oracles].isArray()) + { + RPC::inject_error(rpcORACLE_MALFORMED, result); + return result; + } + + if (!params.isMember(jss::symbol)) + return RPC::missing_field_error(jss::symbol); + + if (!params.isMember(jss::price_unit)) + return RPC::missing_field_error(jss::price_unit); + + if (!params.isMember(jss::flags)) + return RPC::missing_field_error(jss::flags); + + auto const flags = params[jss::flags].asUInt(); + auto constexpr fSimpleAverage = 0x01; + auto constexpr fMedian = 0x02; + auto constexpr fTrimmedMean = 0x04; + if (!(flags & (fSimpleAverage | fMedian | fTrimmedMean))) + { + RPC::inject_error(rpcINVALID_PARAMS, result); + return result; + } + std::optional trim = params.isMember(jss::trim) + ? std::optional(params[jss::trim].asUInt()) + : std::nullopt; + if (((flags & fTrimmedMean) && !trim) || + (!(flags & fTrimmedMean) && trim) || trim == 0 || trim > 25) + { + RPC::inject_error(rpcINVALID_PARAMS, result); + return result; + } + + auto const symbol = params[jss::symbol]; + auto const priceUnit = params[jss::price_unit]; + + // prices sorted low to high. use STAmount since Number is int64 only + std::vector prices; + Issue const someIssue = {to_currency("SOM"), AccountID{1}}; + STAmount avg{someIssue, 0, 0}; + for (auto const& oracle : params[jss::oracles]) + { + if (!oracle.isMember(jss::oracle_id)) + { + RPC::inject_error(rpcORACLE_MALFORMED, result); + return result; + } + uint256 hash; + if (!hash.parseHex(oracle[jss::oracle_id].asString())) + { + RPC::inject_error(rpcINVALID_PARAMS, result); + return result; + } + if (auto const sle = ledger->read(keylet::oracle(hash))) + { + auto const series = sle->getFieldArray(sfPriceDataSeries); + if (auto iter = std::find_if( + series.begin(), + series.end(), + [&](STObject const& o) -> bool { + return o.getFieldCurrency(sfSymbol).getText() == + symbol && + o.getFieldCurrency(sfPriceUnit).getText() == + priceUnit; + }); + iter == series.end()) + { + RPC::inject_error(rpcOBJECT_NOT_FOUND, result); + return result; + } + else + { + auto const price = iter->getFieldU64(sfSymbolPrice); + auto const scale = -static_cast(iter->getFieldU8(sfScale)); + prices.push_back(STAmount{someIssue, price, scale}); + } + avg += prices.back(); + } + else + { + RPC::inject_error(rpcOBJECT_NOT_FOUND, result); + return result; + } + } + if (prices.empty()) + { + RPC::inject_error(rpcORACLE_MALFORMED, result); + return result; + } + if (flags & fSimpleAverage) + { + avg = divide( + avg, + STAmount{someIssue, static_cast(prices.size()), 0}, + someIssue); + result[jss::simple_average] = avg.getText(); + } + if (flags & (fMedian | fTrimmedMean)) + { + std::stable_sort(prices.begin(), prices.end()); + if (flags & fMedian) + { + auto const median = [&]() { + if (prices.size() % 2 == 0) + { + // Even number of elements + size_t middle = prices.size() / 2; + return divide( + prices[middle - 1] + prices[middle], + STAmount{someIssue, 2, 0}, + someIssue); + } + else + { + // Odd number of elements + return divide( + prices[prices.size()], + STAmount{someIssue, 2, 0}, + someIssue); + } + }(); + result[jss::median] = median.getText(); + } + if (flags & fTrimmedMean) + { + auto const trimCount = prices.size() * *trim / 100; + size_t start = trimCount; + size_t end = prices.size() - trimCount; + + avg = std::accumulate( + prices.begin() + trimCount, + prices.begin() + end, + STAmount{someIssue, 0, 0}); + + avg = divide( + avg, + STAmount{someIssue, static_cast(end - start), 0}, + someIssue); + result[jss::trimmed_mean] = avg.getText(); + } + } + + return result; +} + +} // namespace ripple diff --git a/src/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h index 367e715ce1f..7e80abed79a 100644 --- a/src/ripple/rpc/handlers/Handlers.h +++ b/src/ripple/rpc/handlers/Handlers.h @@ -73,6 +73,8 @@ doGatewayBalances(RPC::JsonContext&); Json::Value doGetCounts(RPC::JsonContext&); Json::Value +doGetAggregatePrice(RPC::JsonContext&); +Json::Value doLedgerAccept(RPC::JsonContext&); Json::Value doLedgerCleaner(RPC::JsonContext&); diff --git a/src/ripple/rpc/handlers/LedgerEntry.cpp b/src/ripple/rpc/handlers/LedgerEntry.cpp index 7f40d3ee3be..3c4ef20d1c7 100644 --- a/src/ripple/rpc/handlers/LedgerEntry.cpp +++ b/src/ripple/rpc/handlers/LedgerEntry.cpp @@ -588,6 +588,42 @@ doLedgerEntry(RPC::JsonContext& context) } } } + 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::OracleID)) + { + jvResult[jss::error] = "malformedRequest"; + } + else + { + try + { + uint256 oracleID; + if (!oracleID.parseHex( + context.params[jss::oracle][jss::OracleID] + .asCString())) + { + uNodeIndex = beast::zero; + jvResult[jss::error] = "malformedRequest"; + } + uNodeIndex = keylet::oracle(oracleID).key; + } + catch (std::runtime_error const&) + { + jvResult[jss::error] = "malformedRequest"; + } + } + } else { if (context.params.isMember("params") && diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index dd898ee8722..13ae1f0db73 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -91,6 +91,10 @@ Handler const handlerArray[]{ {"gateway_balances", byRef(&doGatewayBalances), Role::USER, NO_CONDITION}, #endif {"get_counts", byRef(&doGetCounts), Role::ADMIN, NO_CONDITION}, + {"get_aggregate_price", + byRef(&doGetAggregatePrice), + Role::ADMIN, + NO_CONDITION}, {"feature", byRef(&doFeature), Role::ADMIN, NO_CONDITION}, {"fee", byRef(&doFee), Role::USER, NEEDS_CURRENT_LEDGER}, {"fetch_info", byRef(&doFetchInfo), Role::ADMIN, NO_CONDITION}, diff --git a/src/test/app/Oracle_test.cpp b/src/test/app/Oracle_test.cpp new file mode 100644 index 00000000000..34ce0c65712 --- /dev/null +++ b/src/test/app/Oracle_test.cpp @@ -0,0 +1,583 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include + +namespace ripple { +namespace test { + +struct Oracle_test : public beast::unit_test::suite +{ +private: + static auto + ledgerEntryOracle(jtx::Env& env, uint256 const& id) + { + Json::Value jvParams; + jvParams[jss::oracle][jss::OracleID] = to_string(id); + return env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + } + + void + testInvalidSet() + { + testcase("Invalid Create"); + + using namespace jtx; + Account const owner("owner"); + + // Insufficient reserve + { + Env env(*this); + env.fund(env.current()->fees().accountReserve(0), owner); + Oracle oracle( + env, + owner, + {{"XRP", "USD", 740, 1}}, + ter(tecINSUFFICIENT_RESERVE)); + } + + { + Env env(*this); + env.fund(XRP(1'000), owner); + Oracle oracle(env); + + // Invalid flag + oracle.create( + owner, + {{"XRP", "USD", 740, 1}}, + "currency", + "provider", + "URI", + // last update time + std::nullopt, + tfSellNFToken, + // msig + std::nullopt, + 0, + ter(temINVALID_FLAG)); + + // Duplicate token pair + oracle.create( + owner, + {{"XRP", "USD", 740, 1}, {"XRP", "USD", 750, 1}}, + "currency", + "provider", + "URI", + std::nullopt, + 0, + std::nullopt, + 0, + ter(tecDUPLICATE)); + + // Array of token pair is 0 or exceeds 10 + oracle.create( + owner, + {{"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}}, + "currency", + "provider", + "URI", + std::nullopt, + 0, + std::nullopt, + 0, + ter(temARRAY_SIZE)); + oracle.create( + owner, + {}, + "currency", + "provider", + "URI", + std::nullopt, + 0, + std::nullopt, + 0, + ter(temARRAY_SIZE)); + } + + // Array of token pair exceeds 10 after update + { + Env env{*this}; + env.fund(XRP(1'000), owner); + + Oracle oracle( + env, owner, {{"XRP", "USD", 740, 1}}, ter(tesSUCCESS)); + oracle.update( + owner, + { + {"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}, + }, + "URI", + std::nullopt, + 0, + std::nullopt, + std::nullopt, + 0, + ter(temARRAY_SIZE)); + } + + { + Env env(*this); + env.fund(XRP(1'000), owner); + Oracle oracle(env); + + // Symbol class or provider not included on create + oracle.create( + owner, + {{"XRP", "USD", 740, 1}}, + std::nullopt, + "provider", + "URI", + std::nullopt, + 0, + std::nullopt, + 0, + ter(temMALFORMED)); + oracle.create( + owner, + {{"XRP", "USD", 740, 1}}, + "currency", + std::nullopt, + "URI", + std::nullopt, + 0, + std::nullopt, + 0, + ter(temMALFORMED)); + + // Symbol class or provider are included on update + oracle.set( + owner, + {{"XRP", "USD", 740, 1}}, + std::nullopt, + "provider", + "URI", + std::nullopt, + 0, + std::nullopt, + std::nullopt, + 0, + ter(temMALFORMED)); + oracle.set( + owner, + {{"XRP", "USD", 740, 1}}, + "currency", + std::nullopt, + "URI", + std::nullopt, + 0, + std::nullopt, + std::nullopt, + 0, + ter(temMALFORMED)); + + // Fields too long + // Symbol class + std::string symbolClass(13, '0'); + oracle.set( + owner, + {{"XRP", "USD", 740, 1}}, + symbolClass, + "provider", + "URI", + std::nullopt, + 0, + std::nullopt, + std::nullopt, + 0, + ter(temMALFORMED)); + // provider + std::string const large(257, '0'); + oracle.set( + owner, + {{"XRP", "USD", 740, 1}}, + "currency", + large, + "URI", + std::nullopt, + 0, + std::nullopt, + std::nullopt, + 0, + ter(temMALFORMED)); + // URI + oracle.set( + owner, + {{"XRP", "USD", 740, 1}}, + "currency", + "provider", + large, + std::nullopt, + 0, + std::nullopt, + std::nullopt, + 0, + ter(temMALFORMED)); + } + + { + // invalid owner + Env env(*this); + Account const some("some"); + env.fund(XRP(1'000), owner); + env.fund(XRP(1'000), some); + Oracle oracle( + env, owner, {{"XRP", "USD", 740, 1}}, ter(tesSUCCESS)); + oracle.update( + some, + {{"XRP", "USD", 740, 1}}, + "URI", + std::nullopt, + 0, + std::nullopt, + std::nullopt, + 0, + ter(tecNO_PERMISSION)); + } + } + + void + testCreate() + { + testcase("Create"); + using namespace jtx; + + Env env(*this); + Account const owner("owner"); + env.fund(XRP(1'000), owner); + Oracle oracle(env, owner, {{"XRP", "USD", 740, 1}}, ter(tesSUCCESS)); + BEAST_EXPECT(oracle.exists()); + } + + 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, {{"USD", "XRP", 740, 1}}, ter(tesSUCCESS)); + BEAST_EXPECT(oracle.exists()); + + // Invalid OracleID + oracle.remove( + owner, std::nullopt, oracle.randOracleID(), 0, ter(tecNO_ENTRY)); + + // Invalid owner + Account const invalid("invalid"); + env.fund(XRP(1'000), invalid); + oracle.remove( + invalid, std::nullopt, std::nullopt, 0, ter(tecNO_PERMISSION)); + } + + void + testDelete() + { + testcase("Delete"); + using namespace jtx; + + Env env(*this); + Account const owner("owner"); + env.fund(XRP(1'000), owner); + Oracle oracle(env, owner, {{"XRP", "USD", 740, 1}}, ter(tesSUCCESS)); + BEAST_EXPECT(oracle.exists()); + oracle.remove(owner); + BEAST_EXPECT(!oracle.exists()); + } + + void + testUpdate() + { + testcase("Update"); + using namespace jtx; + Account const owner("owner"); + + Env env(*this); + env.fund(XRP(1'000), owner); + Oracle oracle(env, owner, {{"XRP", "USD", 740, 1}}, ter(tesSUCCESS)); + BEAST_EXPECT(oracle.exists()); + + // update existing pair + oracle.update(owner, {{"XRP", "USD", 740, 2}}); + BEAST_EXPECT(oracle.expectPrice({{"XRP", "USD", 740, 2}})); + + // add new pairs, the not-included pair is reset + oracle.update(owner, {{"XRP", "EUR", 700, 2}}); + BEAST_EXPECT( + oracle.expectPrice({{"XRP", "USD", 0, 0}, {"XRP", "EUR", 700, 2}})); + } + + void + testMultisig(FeatureBitset features) + { + testcase("Multisig"); + using namespace jtx; + + 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(1'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 + Oracle oracle(env); + oracle.create( + alice, + {{"XRP", "USD", 740, 1}}, + "currency", + "provider", + "URI", + std::nullopt, + 0, + msig(becky), + 0, + ter(tefBAD_QUORUM)); + oracle.create( + alice, + {{"XRP", "USD", 740, 1}}, + "currency", + "provider", + "URI", + std::nullopt, + 0, + msig(zelda), + 0, + ter(tefBAD_SIGNATURE)); + oracle.create( + alice, + {{"XRP", "USD", 740, 1}}, + "currency", + "provider", + "URI", + std::nullopt, + 0, + msig(becky, bogie)); + BEAST_EXPECT(oracle.exists()); + + // Update + oracle.update( + alice, + {{"XRP", "USD", 740, 1}}, + "URI", + std::nullopt, + 0, + msig(becky), + std::nullopt, + 0, + ter(tefBAD_QUORUM)); + oracle.update( + alice, + {{"XRP", "USD", 740, 1}}, + "URI", + std::nullopt, + 0, + msig(zelda), + std::nullopt, + 0, + ter(tefBAD_SIGNATURE)); + oracle.update( + alice, + {{"XRP", "USD", 740, 1}}, + "URI", + std::nullopt, + 0, + msig(becky, bogie)); + BEAST_EXPECT(oracle.expectPrice({{"XRP", "USD", 740, 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.update( + alice, + {{"XRP", "USD", 740, 1}}, + "URI", + std::nullopt, + 0, + msig(becky, bogie), + std::nullopt, + 0, + ter(tefBAD_SIGNATURE)); + // updated list succeeds + oracle.update( + alice, + {{"XRP", "USD", 7412, 2}}, + "URI", + std::nullopt, + 0, + msig(zelda, bob)); + BEAST_EXPECT(oracle.expectPrice({{"XRP", "USD", 7412, 2}})); + oracle.update( + alice, + {{"XRP", "USD", 74245, 3}}, + "URI", + std::nullopt, + 0, + msig(ed)); + BEAST_EXPECT(oracle.expectPrice({{"XRP", "USD", 74245, 3}})); + + // Remove + oracle.remove( + alice, msig(bob), std::nullopt, 100'000, ter(tefBAD_QUORUM)); + oracle.remove( + alice, msig(becky), std::nullopt, 100'000, ter(tefBAD_SIGNATURE)); + oracle.remove(alice, msig(ed), std::nullopt, 100'000); + 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, {{"XRP", "USD", 740, 1}}, ter(temDISABLED)); + } + { + Oracle oracle(env); + oracle.update( + owner, + {{"XRP", "USD", 740, 1}}, + std::nullopt, + std::nullopt, + 0, + std::nullopt, + std::nullopt, + 0, + ter(temDISABLED)); + } + { + Oracle oracle(env); + oracle.remove( + owner, std::nullopt, std::nullopt, 0, ter(temDISABLED)); + } + } + + void + testLedgerEntry() + { + testcase("Ledger Entry"); + using namespace jtx; + + Env env(*this); + std::vector accounts; + std::vector 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, {{"XRP", "USD", 740, 1}}, ter(tesSUCCESS)); + accounts.push_back(owner.id()); + oracles.push_back(oracle.oracleID()); + // same account can have different asset pair + Oracle oracle1( + env, owner, {{"XRP", "EUR", 740, 1}}, ter(tesSUCCESS)); + accounts.push_back(owner.id()); + oracles.push_back(oracle1.oracleID()); + } + for (int i = 0; i < accounts.size(); ++i) + { + auto const jv = ledgerEntryOracle(env, 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 test + +} // namespace ripple diff --git a/src/test/jtx/Oracle.h b/src/test/jtx/Oracle.h new file mode 100644 index 00000000000..f7e010b8f70 --- /dev/null +++ b/src/test/jtx/Oracle.h @@ -0,0 +1,154 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { +namespace test { +namespace jtx { + +class Oracle +{ + // symbol, price unit, price, scale + using DataSeries = std::vector< + std::tuple>; + +private: + static inline std::uint32_t id_ = 0; + Env& env_; + uint256 oracleID_; + std::optional const msig_; + std::uint32_t fee_; + // Same as LedgerNameSpace + std::uint16_t static constexpr oracleNameSpace_ = 'R'; + +private: + void + submit( + Json::Value const& jv, + std::optional const& msig, + std::optional const& seq, + std::optional const& ter); + +public: + using ustring = std::basic_string; + Oracle( + Env& env, + std::optional const& msig = std::nullopt, + std::uint32_t fee = 0); + + Oracle( + Env& env, + Account const& owner, + DataSeries const& series, + std::string const& symbolClass = "currency", + std::string const& provider = "provider", + std::optional const& URI = "URI", + std::optional const& lastUpdateTime = std::nullopt, + std::optional const& msig = std::nullopt, + std::uint32_t fee = 0, + std::optional const& ter = std::nullopt); + + Oracle( + Env& env, + Account const& owner, + DataSeries const& series, + std::optional const& ter); + + void + create( + AccountID const& owner, + DataSeries const& series, + std::optional const& symbolClass = std::nullopt, + std::optional const& provider = std::nullopt, + std::optional const& URI = std::nullopt, + std::optional const& lastUpdateTime = std::nullopt, + std::uint32_t flags = 0, + std::optional const& msig = std::nullopt, + std::uint32_t fee = 0, + std::optional const& ter = std::nullopt); + + void + remove( + AccountID const& owner, + std::optional const& msig = std::nullopt, + std::optional const& oracleID = std::nullopt, + std::uint32_t fee = 0, + std::optional const& ter = std::nullopt); + + void + update( + AccountID const& owner, + DataSeries const& series, + std::optional const& URI = std::nullopt, + std::optional const& lastUpdateTime = std::nullopt, + std::uint32_t flags = 0, + std::optional const& msig = std::nullopt, + std::optional const& oracleID = std::nullopt, + std::uint32_t fee = 0, + std::optional const& ter = std::nullopt); + + static Json::Value + aggregatePrice( + Env& env, + std::optional const& symbol, + std::optional const& priceUnit, + std::optional> const& oracles, + std::optional const& trim, + std::uint32_t flags); + + uint256 + oracleID() const + { + return oracleID_; + } + + bool + exists() const; + + uint256 + randOracleID() const; + + bool + expectPrice(DataSeries const& pricess) const; + + void + set(AccountID const& owner, + DataSeries const& series, + std::optional const& symbolClass = std::nullopt, + std::optional const& provider = std::nullopt, + std::optional const& URI = std::nullopt, + std::optional const& lastUpdateTime = std::nullopt, + std::uint32_t flags = 0, + std::optional const& msig = std::nullopt, + std::optional const& oracleID = std::nullopt, + std::uint32_t fee = 0, + std::optional const& ter = std::nullopt); + +private: +}; + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif // RIPPLE_TEST_JTX_ORACLE_H_INCLUDED diff --git a/src/test/jtx/impl/Oracle.cpp b/src/test/jtx/impl/Oracle.cpp new file mode 100644 index 00000000000..7621acbdd6b --- /dev/null +++ b/src/test/jtx/impl/Oracle.cpp @@ -0,0 +1,328 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include + +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +Oracle::Oracle( + Env& env, + std::optional const& msig, + std::uint32_t fee) + : env_(env), oracleID_(randOracleID()), msig_(msig), fee_(fee) +{ +} + +Oracle::Oracle( + Env& env, + Account const& owner, + DataSeries const& series, + std::string const& symbolClass, + std::string const& provider, + std::optional const& URI, + std::optional const& lastUpdateTime, + std::optional const& msig, + std::uint32_t fee, + std::optional const& ter) + : env_(env) + , oracleID_(sha512Half(owner, Oracle::id_++)) + , msig_(msig) + , fee_(fee) +{ + create( + owner, + series, + symbolClass, + provider, + URI, + lastUpdateTime, + 0, + msig, + fee, + ter); +} + +Oracle::Oracle( + Env& env, + Account const& owner, + DataSeries const& series, + std::optional const& ter) + : Oracle( + env, + owner, + series, + "currency", + "provider", + "URI", + std::nullopt, + std::nullopt, + 0, + ter) +{ +} + +void +Oracle::create( + AccountID const& owner, + DataSeries const& series, + std::optional const& symbolClass, + std::optional const& provider, + std::optional const& URI, + std::optional const& lastUpdateTime, + std::uint32_t flags, + std::optional const& msig, + std::uint32_t fee, + std::optional const& ter) +{ + set(owner, + series, + symbolClass, + provider, + URI, + lastUpdateTime, + flags, + msig, + std::nullopt, + fee, + ter); +} + +void +Oracle::remove( + AccountID const& owner, + std::optional const& msig, + std::optional const& oracleID, + std::uint32_t fee, + std::optional const& ter) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::OracleDelete; + jv[jss::Account] = to_string(owner); + jv[jss::OracleID] = to_string(oracleID.value_or(oracleID_)); + if (auto const f = fee != 0 ? fee : fee_) + jv[jss::Fee] = std::to_string(f); + else + jv[jss::Fee] = std::to_string(env_.current()->fees().increment.drops()); + submit(jv, msig, std::nullopt, ter); +} + +void +Oracle::update( + AccountID const& owner, + DataSeries const& series, + std::optional const& URI, + std::optional const& lastUpdateTime, + std::uint32_t flags, + std::optional const& msig, + std::optional const& oracleID, + std::uint32_t fee, + std::optional const& ter) +{ + set(owner, + series, + std::nullopt, + std::nullopt, + URI, + lastUpdateTime, + flags, + msig, + oracleID, + fee, + ter); +} + +void +Oracle::submit( + Json::Value const& jv, + std::optional const& msig, + std::optional const& seq, + std::optional const& ter) +{ + if (auto const& ms = msig ? msig : msig_) + { + if (seq && ter) + env_(jv, *ms, *seq, *ter); + else if (seq) + env_(jv, *ms, *seq); + else if (ter) + env_(jv, *ms, *ter); + else + env_(jv, *ms); + } + else if (seq && ter) + env_(jv, *seq, *ter); + else if (seq) + env_(jv, *seq); + else if (ter) + env_(jv, *ter); + else + env_(jv); + env_.close(); +} + +bool +Oracle::exists() const +{ + return env_.le(keylet::oracle(oracleID_)) != nullptr; +} + +uint256 +Oracle::randOracleID() const +{ + auto keys = randomKeyPair(KeyType::secp256k1); + return sha512Half(keys.first); +} + +bool +Oracle::expectPrice(DataSeries const& series) const +{ + if (auto const sle = env_.le(keylet::oracle(oracleID_))) + { + auto const leSeries = sle->getFieldArray(sfPriceDataSeries); + if (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& symbol = o.getFieldCurrency(sfSymbol); + auto const& priceUnit = o.getFieldCurrency(sfPriceUnit); + auto const& price = o.getFieldU64(sfSymbolPrice); + auto const& scale = o.getFieldU8(sfScale); + return symbol.getText() == std::get<0>(data) && + priceUnit.getText() == std::get<1>(data) && + price == std::get<2>(data) && + scale == std::get<3>(data); + }) == leSeries.end()) + return false; + } + return true; + } + return false; +} + +Json::Value +Oracle::aggregatePrice( + Env& env, + std::optional const& symbol, + std::optional const& priceUnit, + std::optional> const& oracles, + std::optional const& trim, + std::uint32_t flags) +{ + Json::Value jv; + Json::Value jvOracles(Json::arrayValue); + if (oracles) + { + for (auto const& id : *oracles) + { + Json::Value oracle; + oracle[jss::oracle_id] = to_string(id); + jvOracles.append(oracle); + } + jv[jss::oracles] = jvOracles; + } + if (flags != 0) + jv[jss::flags] = flags; + if (trim) + jv[jss::trim] = *trim; + if (symbol) + jv[jss::symbol] = *symbol; + if (priceUnit) + jv[jss::price_unit] = *priceUnit; + + 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( + AccountID const& owner, + DataSeries const& series, + std::optional const& symbolClass, + std::optional const& provider, + std::optional const& URI, + std::optional const& lastUpdateTime, + std::uint32_t flags, + std::optional const& msig, + std::optional const& oracleID, + std::uint32_t fee, + std::optional const& ter) +{ + using namespace std::chrono; + Json::Value jv; + jv[jss::TransactionType] = jss::OracleSet; + if (oracleID) + jv[jss::OracleID] = to_string(*oracleID); + else + jv[jss::OracleID] = to_string(oracleID_); + jv[jss::Account] = to_string(owner); + if (symbolClass) + jv[jss::SymbolClass] = strHex(*symbolClass); + if (provider) + jv[jss::Provider] = strHex(*provider); + if (URI) + jv[jss::URI] = strHex(*URI); + if (flags != 0) + jv[jss::Flags] = flags; + if (auto const f = fee != 0 ? fee : fee_) + jv[jss::Fee] = std::to_string(f); + else + jv[jss::Fee] = std::to_string(env_.current()->fees().increment.drops()); + if (lastUpdateTime) + jv[jss::LastUpdateTime] = *lastUpdateTime; + else + jv[jss::LastUpdateTime] = to_string( + duration_cast(env_.timeKeeper().now().time_since_epoch()) + .count()); + Json::Value dataSeries(Json::arrayValue); + for (auto const& data : series) + { + Json::Value priceData; + Json::Value price; + price[jss::Symbol] = std::get<0>(data); + price[jss::PriceUnit] = std::get<1>(data); + // std::stringstream str; + // str << std::hex << std::get<3>(data); + price[jss::SymbolPrice] = std::get<2>(data); + price[jss::Scale] = std::get<3>(data); + priceData[jss::PriceData] = price; + dataSeries.append(priceData); + } + jv[jss::PriceDataSeries] = dataSeries; + submit(jv, msig, std::nullopt, ter); +} + +} // namespace jtx +} // namespace test +} // namespace ripple \ No newline at end of file diff --git a/src/test/rpc/GetAggregatePrice_test.cpp b/src/test/rpc/GetAggregatePrice_test.cpp new file mode 100644 index 00000000000..66bc1e78e73 --- /dev/null +++ b/src/test/rpc/GetAggregatePrice_test.cpp @@ -0,0 +1,150 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include + +namespace ripple { +namespace test { + +class GetAggregatePrice_test : public beast::unit_test::suite +{ + static auto + ledgerEntryOracle(jtx::Env& env, uint256 const& id) + { + Json::Value jvParams; + jvParams[jss::oracle][jss::OracleID] = to_string(id); + return env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + } + +public: + void + testErrors() + { + testcase("Errors"); + using namespace jtx; + Env env(*this); + + // missing symbol + auto ret = Oracle::aggregatePrice( + env, std::nullopt, "USD", {{uint256{1}}}, std::nullopt, 0x01); + BEAST_EXPECT( + ret[jss::error_message].asString() == "Missing field 'symbol'."); + + // missing price_unit + ret = Oracle::aggregatePrice( + env, "XRP", std::nullopt, {{uint256{1}}}, std::nullopt, 0x01); + BEAST_EXPECT( + ret[jss::error_message].asString() == + "Missing field 'price_unit'."); + + // missing oracles array + ret = Oracle::aggregatePrice( + env, "XRP", "USD", std::nullopt, std::nullopt, 0x01); + BEAST_EXPECT( + ret[jss::error_message].asString() == "Missing field 'oracles'."); + + // empty oracles array + ret = + Oracle::aggregatePrice(env, "XRP", "USD", {{}}, std::nullopt, 0x01); + BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed"); + + // invalid flags + ret = Oracle::aggregatePrice( + env, "XRP", "USD", {{uint256{1}}}, std::nullopt, 0); + BEAST_EXPECT( + ret[jss::error_message].asString() == "Missing field 'flags'."); + + // trim set but not the flag + ret = + Oracle::aggregatePrice(env, "XRP", "USD", {{uint256{1}}}, 5, 0x01); + BEAST_EXPECT(ret[jss::error].asString() == "invalidParams"); + + // flag set but not the trim + ret = Oracle::aggregatePrice( + env, "XRP", "USD", {{uint256{1}}}, std::nullopt, 0x04); + BEAST_EXPECT(ret[jss::error].asString() == "invalidParams"); + + // invalid oracle id + ret = Oracle::aggregatePrice( + env, "XRP", "USD", {{uint256{1}}}, std::nullopt, 0x01); + BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound"); + + // oracles have wrong asset pair + Account const owner("owner"); + env.fund(XRP(1'000), owner); + Oracle oracle(env, owner, {{"XRP", "EUR", 740, 1}}, ter(tesSUCCESS)); + Oracle oracle1(env, owner, {{"XRP", "USD", 740, 1}}, ter(tesSUCCESS)); + ret = Oracle::aggregatePrice( + env, + "XRP", + "USD", + {{oracle.oracleID(), oracle1.oracleID()}}, + std::nullopt, + 0x01); + BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound"); + } + + void + testRpc() + { + testcase("RPC"); + using namespace jtx; + + { + Env env(*this); + std::vector oracles; + for (int i = 0; i < 10; ++i) + { + Account const owner{std::to_string(i)}; + env.fund(XRP(1'000), owner); + Oracle oracle( + env, owner, {{"XRP", "USD", 740 + i, 1}}, ter(tesSUCCESS)); + oracles.push_back(oracle.oracleID()); + } + // simple average + auto ret = Oracle::aggregatePrice( + env, "XRP", "USD", oracles, std::nullopt, 0x01); + BEAST_EXPECT(ret[jss::simple_average] == "74.45"); + // median + ret = Oracle::aggregatePrice( + env, "XRP", "USD", oracles, std::nullopt, 0x02); + BEAST_EXPECT(ret[jss::median] == "74.45"); + // trimmed mean + ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles, 20, 0x04); + BEAST_EXPECT(ret[jss::trimmed_mean] == "74.45"); + } + BEAST_EXPECT(true); + } + + void + run() override + { + testErrors(); + testRpc(); + } +}; + +BEAST_DEFINE_TESTSUITE(GetAggregatePrice, app, ripple); + +} // namespace test +} // namespace ripple