From 27a422369de484df64084f38a64d536cacf18bdd Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Fri, 22 Mar 2024 16:51:06 +0000 Subject: [PATCH] Add support for Price Oracle in `ledger_entry` (#1287) Fixes #1277 --- src/rpc/Errors.cpp | 1 + src/rpc/Errors.hpp | 1 + src/rpc/handlers/LedgerEntry.cpp | 13 + src/rpc/handlers/LedgerEntry.hpp | 20 ++ src/web/impl/ErrorHandling.hpp | 1 + unittests/rpc/handlers/LedgerEntryTests.cpp | 309 ++++++++++++++++++-- unittests/util/TestObject.cpp | 59 ++++ unittests/util/TestObject.hpp | 24 ++ 8 files changed, 400 insertions(+), 28 deletions(-) diff --git a/src/rpc/Errors.cpp b/src/rpc/Errors.cpp index dc042a2d..65830031 100644 --- a/src/rpc/Errors.cpp +++ b/src/rpc/Errors.cpp @@ -91,6 +91,7 @@ getErrorInfo(ClioError code) {ClioError::rpcINVALID_HOT_WALLET, "invalidHotWallet", "Invalid hot wallet."}, {ClioError::rpcUNKNOWN_OPTION, "unknownOption", "Unknown option."}, {ClioError::rpcFIELD_NOT_FOUND_TRANSACTION, "fieldNotFoundTransaction", "Missing field."}, + {ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID, "malformedDocumentID", "Malformed oracle_document_id."}, // special system errors {ClioError::rpcINVALID_API_VERSION, JS(invalid_API_version), "Invalid API version."}, {ClioError::rpcCOMMAND_IS_MISSING, JS(missingCommand), "Method is not specified or is not a string."}, diff --git a/src/rpc/Errors.hpp b/src/rpc/Errors.hpp index b36a441f..3d4549c3 100644 --- a/src/rpc/Errors.hpp +++ b/src/rpc/Errors.hpp @@ -42,6 +42,7 @@ enum class ClioError { rpcINVALID_HOT_WALLET = 5004, rpcUNKNOWN_OPTION = 5005, rpcFIELD_NOT_FOUND_TRANSACTION = 5006, + rpcMALFORMED_ORACLE_DOCUMENT_ID = 5007, // special system errors start with 6000 rpcINVALID_API_VERSION = 6000, diff --git a/src/rpc/handlers/LedgerEntry.cpp b/src/rpc/handlers/LedgerEntry.cpp index 575e20ca..e1f0de43 100644 --- a/src/rpc/handlers/LedgerEntry.cpp +++ b/src/rpc/handlers/LedgerEntry.cpp @@ -139,6 +139,8 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx) key = ripple::keylet::xChainCreateAccountClaimID(input.bridge->value(), input.createAccountClaimId.value()) .key; } + } else if (input.oracleNode) { + key = input.oracleNode.value(); } else { // Must specify 1 of the following fields to indicate what type if (ctx.apiVersion == 1) @@ -257,6 +259,7 @@ tag_invoke(boost::json::value_to_tag, boost::json::va {JS(amm), ripple::ltAMM}, {JS(xchain_owned_create_account_claim_id), ripple::ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, {JS(xchain_owned_claim_id), ripple::ltXCHAIN_OWNED_CLAIM_ID}, + {JS(oracle), ripple::ltORACLE}, }; auto const parseBridgeFromJson = [](boost::json::value const& bridgeJson) { @@ -274,6 +277,14 @@ tag_invoke(boost::json::value_to_tag, boost::json::va return ripple::STXChainBridge{lockingDoor, lockingIssue, issuingDoor, issuingIssue}; }; + auto const parseOracleFromJson = [](boost::json::value const& json) { + auto const account = + ripple::parseBase58(boost::json::value_to(json.at(JS(account)))); + auto const documentId = boost::json::value_to(json.at(JS(oracle_document_id))); + + return ripple::keylet::oracle(*account, documentId).key; + }; + auto const indexFieldType = std::find_if(indexFieldTypeMap.begin(), indexFieldTypeMap.end(), [&jsonObject](auto const& pair) { auto const& [field, _] = pair; @@ -318,6 +329,8 @@ tag_invoke(boost::json::value_to_tag, boost::json::va input.createAccountClaimId = boost::json::value_to( jv.at(JS(xchain_owned_create_account_claim_id)).at(JS(xchain_owned_create_account_claim_id)) ); + } else if (jsonObject.contains(JS(oracle))) { + input.oracleNode = parseOracleFromJson(jv.at(JS(oracle))); } return input; diff --git a/src/rpc/handlers/LedgerEntry.hpp b/src/rpc/handlers/LedgerEntry.hpp index d603b9be..f8753582 100644 --- a/src/rpc/handlers/LedgerEntry.hpp +++ b/src/rpc/handlers/LedgerEntry.hpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -98,6 +99,7 @@ public: std::optional bridgeAccount; std::optional chainClaimId; std::optional createAccountClaimId; + std::optional oracleNode; }; using Result = HandlerReturnType; @@ -278,6 +280,24 @@ public: }}, Status(ClioError::rpcMALFORMED_REQUEST) }}, + {JS(oracle), + meta::WithCustomError{ + validation::Type{}, Status(ClioError::rpcMALFORMED_REQUEST) + }, + meta::IfType{ + meta::WithCustomError{malformedRequestHexStringValidator, Status(ClioError::rpcMALFORMED_ADDRESS)} + }, + meta::IfType{meta::Section{ + {JS(account), + meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)}, + meta::WithCustomError{validation::AccountBase58Validator, Status(ClioError::rpcMALFORMED_ADDRESS)}}, + // note: Unlike `rippled`, Clio only supports UInt as input, no string, no `null`, etc.: + {JS(oracle_document_id), + meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)}, + meta::WithCustomError{ + validation::Type{}, Status(ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID) + }}, + }}} }; return rpcSpec; diff --git a/src/web/impl/ErrorHandling.hpp b/src/web/impl/ErrorHandling.hpp index 7f5e9156..1729507f 100644 --- a/src/web/impl/ErrorHandling.hpp +++ b/src/web/impl/ErrorHandling.hpp @@ -89,6 +89,7 @@ public: case rpc::ClioError::rpcMALFORMED_ADDRESS: case rpc::ClioError::rpcINVALID_HOT_WALLET: case rpc::ClioError::rpcFIELD_NOT_FOUND_TRANSACTION: + case rpc::ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID: ASSERT( false, "Unknown rpc error code {}", static_cast(*clioCode) ); // this should never happen diff --git a/unittests/rpc/handlers/LedgerEntryTests.cpp b/unittests/rpc/handlers/LedgerEntryTests.cpp index aa97474f..eeaa6b5f 100644 --- a/unittests/rpc/handlers/LedgerEntryTests.cpp +++ b/unittests/rpc/handlers/LedgerEntryTests.cpp @@ -1269,7 +1269,7 @@ generateTestValuesForParametersTest() R"({{ "bridge_account": "{}", "bridge": "invalid" - }})", + }})", ACCOUNT ), "malformedRequest", @@ -1278,8 +1278,8 @@ generateTestValuesForParametersTest() ParamTestCaseBundle{ "OwnedClaimIdInvalidType", R"({ - "xchain_owned_claim_id": 123 - })", + "xchain_owned_claim_id": 123 + })", "malformedRequest", "Malformed request." }, @@ -1547,6 +1547,219 @@ generateTestValuesForParametersTest() "malformedRequest", "Malformed request." }, + ParamTestCaseBundle{ + "OracleObjectDocumentIdMissing", + fmt::format( + R"({{ + "oracle": {{ + "account": "{}" + }} + }})", + ACCOUNT + ), + "malformedRequest", + "Malformed request." + }, + ParamTestCaseBundle{ + "OracleObjectDocumentIdInvalidNegative", + fmt::format( + R"({{ + "oracle": {{ + "account": "{}", + "oracle_document_id": -1 + }} + }})", + ACCOUNT + ), + "malformedDocumentID", + "Malformed oracle_document_id." + }, + ParamTestCaseBundle{ + "OracleObjectDocumentIdInvalidTypeString", + fmt::format( + R"({{ + "oracle": {{ + "account": "{}", + "oracle_document_id": "invalid" + }} + }})", + ACCOUNT + ), + "malformedDocumentID", + "Malformed oracle_document_id." + }, + ParamTestCaseBundle{ + "OracleObjectDocumentIdInvalidTypeDouble", + fmt::format( + R"({{ + "oracle": {{ + "account": "{}", + "oracle_document_id": 3.21 + }} + }})", + ACCOUNT + ), + "malformedDocumentID", + "Malformed oracle_document_id." + }, + ParamTestCaseBundle{ + "OracleObjectDocumentIdInvalidTypeObject", + fmt::format( + R"({{ + "oracle": {{ + "account": "{}", + "oracle_document_id": {{}} + }} + }})", + ACCOUNT + ), + "malformedDocumentID", + "Malformed oracle_document_id." + }, + ParamTestCaseBundle{ + "OracleObjectDocumentIdInvalidTypeArray", + fmt::format( + R"({{ + "oracle": {{ + "account": "{}", + "oracle_document_id": [] + }} + }})", + ACCOUNT + ), + "malformedDocumentID", + "Malformed oracle_document_id." + }, + ParamTestCaseBundle{ + "OracleObjectDocumentIdInvalidTypeNull", + fmt::format( + R"({{ + "oracle": {{ + "account": "{}", + "oracle_document_id": null + }} + }})", + ACCOUNT + ), + "malformedDocumentID", + "Malformed oracle_document_id." + }, + ParamTestCaseBundle{ + "OracleObjectAccountMissing", + R"({ + "oracle": { + "oracle_document_id": 1 + } + })", + "malformedRequest", + "Malformed request." + }, + ParamTestCaseBundle{ + "OracleObjectAccountInvalidTypeInteger", + R"({ + "oracle": { + "account": 123, + "oracle_document_id": 1 + } + })", + "malformedAddress", + "Malformed address." + }, + ParamTestCaseBundle{ + "OracleObjectAccountInvalidTypeDouble", + R"({ + "oracle": { + "account": 123.45, + "oracle_document_id": 1 + } + })", + "malformedAddress", + "Malformed address." + }, + ParamTestCaseBundle{ + "OracleObjectAccountInvalidTypeNull", + R"({ + "oracle": { + "account": null, + "oracle_document_id": 1 + } + })", + "malformedAddress", + "Malformed address." + }, + ParamTestCaseBundle{ + "OracleObjectAccountInvalidTypeObject", + R"({ + "oracle": { + "account": {"test": "test"}, + "oracle_document_id": 1 + } + })", + "malformedAddress", + "Malformed address." + }, + ParamTestCaseBundle{ + "OracleObjectAccountInvalidTypeArray", + R"({ + "oracle": { + "account": [{"test": "test"}], + "oracle_document_id": 1 + } + })", + "malformedAddress", + "Malformed address." + }, + ParamTestCaseBundle{ + "OracleObjectAccountInvalidFormat", + R"({ + "oracle": { + "account": "NotHex", + "oracle_document_id": 1 + } + })", + "malformedAddress", + "Malformed address." + }, + ParamTestCaseBundle{ + "OracleStringInvalidFormat", + R"({ + "oracle": "NotHex" + })", + "malformedAddress", + "Malformed address." + }, + ParamTestCaseBundle{ + "OracleStringInvalidTypeInteger", + R"({ + "oracle": 123 + })", + "malformedRequest", + "Malformed request." + }, + ParamTestCaseBundle{ + "OracleStringInvalidTypeDouble", + R"({ + "oracle": 123.45 + })", + "malformedRequest", + "Malformed request." + }, + ParamTestCaseBundle{ + "OracleStringInvalidTypeArray", + R"({ + "oracle": [{"test": "test"}] + })", + "malformedRequest", + "Malformed request." + }, + ParamTestCaseBundle{ + "OracleStringInvalidTypeNull", + R"({ + "oracle": null + })", + "malformedRequest", + "Malformed request." + }, }; } @@ -1637,13 +1850,11 @@ TEST_F(RPCLedgerEntryTest, LedgerEntryNotFound) backend->setRange(RANGEMIN, RANGEMAX); // return valid ledgerinfo auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, RANGEMAX); - EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1); - ON_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(ledgerinfo)); // return null for ledger entry auto const key = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; - EXPECT_CALL(*backend, doFetchLedgerObject).Times(1); - ON_CALL(*backend, doFetchLedgerObject(key, RANGEMAX, _)).WillByDefault(Return(std::optional{})); + EXPECT_CALL(*backend, doFetchLedgerObject(key, RANGEMAX, _)).WillRepeatedly(Return(std::optional{})); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{LedgerEntryHandler{backend}}; @@ -2119,6 +2330,57 @@ generateTestValuesForNormalPathTest() .key, CreateChainOwnedClaimIDObject(ACCOUNT, ACCOUNT, ACCOUNT2, "JPY", ACCOUNT3, ACCOUNT) }, + NormalPathTestBundle{ + "OracleEntryFoundViaObject", + fmt::format( + R"({{ + "binary": true, + "oracle": {{ + "account": "{}", + "oracle_document_id": 1 + }} + }})", + ACCOUNT + ), + ripple::keylet::oracle(GetAccountIDWithString(ACCOUNT), 1).key, + CreateOracleObject( + ACCOUNT, + "70726F7669646572", + 32u, + 1234u, + ripple::Blob(8, 's'), + ripple::Blob(8, 's'), + RANGEMAX - 2, + ripple::uint256{"E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"}, + CreatePriceDataSeries( + {CreateOraclePriceData(2e4, ripple::to_currency("XRP"), ripple::to_currency("USD"), 3)} + ) + ) + }, + NormalPathTestBundle{ + "OracleEntryFoundViaString", + fmt::format( + R"({{ + "binary": true, + "oracle": "{}" + }})", + ripple::to_string(ripple::keylet::oracle(GetAccountIDWithString(ACCOUNT), 1).key) + ), + ripple::keylet::oracle(GetAccountIDWithString(ACCOUNT), 1).key, + CreateOracleObject( + ACCOUNT, + "70726F7669646572", + 64u, + 4321u, + ripple::Blob(8, 'a'), + ripple::Blob(8, 'a'), + RANGEMAX - 4, + ripple::uint256{"E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"}, + CreatePriceDataSeries( + {CreateOraclePriceData(1e3, ripple::to_currency("USD"), ripple::to_currency("XRP"), 2)} + ) + ) + }, }; } @@ -2138,12 +2400,10 @@ TEST_P(RPCLedgerEntryNormalPathTest, NormalPath) backend->setRange(RANGEMIN, RANGEMAX); // return valid ledgerinfo auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, RANGEMAX); - EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1); - ON_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(ledgerinfo)); - EXPECT_CALL(*backend, doFetchLedgerObject).Times(1); - ON_CALL(*backend, doFetchLedgerObject(testBundle.expectedIndex, RANGEMAX, _)) - .WillByDefault(Return(testBundle.mockedEntity.getSerializer().peekData())); + EXPECT_CALL(*backend, doFetchLedgerObject(testBundle.expectedIndex, RANGEMAX, _)) + .WillRepeatedly(Return(testBundle.mockedEntity.getSerializer().peekData())); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{LedgerEntryHandler{backend}}; @@ -2190,14 +2450,12 @@ TEST_F(RPCLedgerEntryTest, BinaryFalse) backend->setRange(RANGEMIN, RANGEMAX); // return valid ledgerinfo auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, RANGEMAX); - EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1); - ON_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(ledgerinfo)); // return valid ledger entry which can be deserialized auto const ledgerEntry = CreatePaymentChannelLedgerObject(ACCOUNT, ACCOUNT2, 100, 200, 300, INDEX1, 400); - EXPECT_CALL(*backend, doFetchLedgerObject).Times(1); - ON_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _)) - .WillByDefault(Return(ledgerEntry.getSerializer().peekData())); + EXPECT_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _)) + .WillRepeatedly(Return(ledgerEntry.getSerializer().peekData())); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{LedgerEntryHandler{backend}}; @@ -2218,14 +2476,12 @@ TEST_F(RPCLedgerEntryTest, UnexpectedLedgerType) backend->setRange(RANGEMIN, RANGEMAX); // return valid ledgerinfo auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, RANGEMAX); - EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1); - ON_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(ledgerinfo)); // return valid ledger entry which can be deserialized auto const ledgerEntry = CreatePaymentChannelLedgerObject(ACCOUNT, ACCOUNT2, 100, 200, 300, INDEX1, 400); - EXPECT_CALL(*backend, doFetchLedgerObject).Times(1); - ON_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _)) - .WillByDefault(Return(ledgerEntry.getSerializer().peekData())); + EXPECT_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _)) + .WillRepeatedly(Return(ledgerEntry.getSerializer().peekData())); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{LedgerEntryHandler{backend}}; @@ -2246,8 +2502,7 @@ TEST_F(RPCLedgerEntryTest, LedgerNotExistViaIntSequence) { backend->setRange(RANGEMIN, RANGEMAX); - EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1); - ON_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillByDefault(Return(std::nullopt)); + EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(std::nullopt)); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{LedgerEntryHandler{backend}}; @@ -2271,8 +2526,7 @@ TEST_F(RPCLedgerEntryTest, LedgerNotExistViaStringSequence) { backend->setRange(RANGEMIN, RANGEMAX); - EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1); - ON_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillByDefault(Return(std::nullopt)); + EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(std::nullopt)); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{LedgerEntryHandler{backend}}; @@ -2296,8 +2550,7 @@ TEST_F(RPCLedgerEntryTest, LedgerNotExistViaHash) { backend->setRange(RANGEMIN, RANGEMAX); - EXPECT_CALL(*backend, fetchLedgerByHash).Times(1); - ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(std::nullopt)); + EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillRepeatedly(Return(std::nullopt)); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{LedgerEntryHandler{backend}}; diff --git a/unittests/util/TestObject.cpp b/unittests/util/TestObject.cpp index 171f7e95..cd71b61c 100644 --- a/unittests/util/TestObject.cpp +++ b/unittests/util/TestObject.cpp @@ -1025,3 +1025,62 @@ CreateLPTCurrency(std::string_view assetCurrency, std::string_view asset2Currenc ripple::to_currency(std::string(assetCurrency)), ripple::to_currency(std::string(asset2Currency)) ); } + +ripple::STObject +CreateOraclePriceData( + uint64_t assetPrice, + ripple::Currency baseAssetCurrency, + ripple::Currency quoteAssetCurrency, + uint8_t scale +) +{ + auto priceData = ripple::STObject(ripple::sfPriceData); + priceData.setFieldU64(ripple::sfAssetPrice, assetPrice); + priceData.setFieldCurrency(ripple::sfBaseAsset, ripple::STCurrency{ripple::sfBaseAsset, baseAssetCurrency}); + priceData.setFieldCurrency(ripple::sfQuoteAsset, ripple::STCurrency{ripple::sfQuoteAsset, quoteAssetCurrency}); + priceData.setFieldU8(ripple::sfScale, scale); + + return priceData; +} + +ripple::STArray +CreatePriceDataSeries(std::vector const& series) +{ + auto priceDataSeries = ripple::STArray{series.size()}; + + for (auto& data : series) { + auto serializer = data.getSerializer(); + priceDataSeries.add(serializer); + } + + return priceDataSeries; +} + +ripple::STObject +CreateOracleObject( + std::string_view accountId, + std::string_view provider, + uint64_t ownerNode, + uint32_t lastUpdateTime, + ripple::Blob uri, + ripple::Blob assetClass, + uint32_t previousTxSeq, + ripple::uint256 previousTxId, + ripple::STArray priceDataSeries +) +{ + auto ledgerObject = ripple::STObject(ripple::sfLedgerEntry); + ledgerObject.setFieldU16(ripple::sfLedgerEntryType, ripple::ltORACLE); + ledgerObject.setFieldU32(ripple::sfFlags, 0); + ledgerObject.setAccountID(ripple::sfOwner, GetAccountIDWithString(accountId)); + ledgerObject.setFieldVL(ripple::sfProvider, ripple::Blob{provider.begin(), provider.end()}); + ledgerObject.setFieldU64(ripple::sfOwnerNode, ownerNode); + ledgerObject.setFieldU32(ripple::sfLastUpdateTime, lastUpdateTime); + ledgerObject.setFieldVL(ripple::sfURI, uri); + ledgerObject.setFieldVL(ripple::sfAssetClass, assetClass); + ledgerObject.setFieldU32(ripple::sfPreviousTxnLgrSeq, previousTxSeq); + ledgerObject.setFieldH256(ripple::sfPreviousTxnID, previousTxId); + ledgerObject.setFieldArray(ripple::sfPriceDataSeries, priceDataSeries); + + return ledgerObject; +} diff --git a/unittests/util/TestObject.hpp b/unittests/util/TestObject.hpp index 2e1d27b4..0c984e0c 100644 --- a/unittests/util/TestObject.hpp +++ b/unittests/util/TestObject.hpp @@ -383,3 +383,27 @@ CreateDidObject(std::string_view accountId, std::string_view didDoc, std::string [[nodiscard]] ripple::Currency CreateLPTCurrency(std::string_view assetCurrency, std::string_view asset2Currency); + +[[nodiscard]] ripple::STObject +CreateOraclePriceData( + uint64_t assetPrice, + ripple::Currency baseAssetCurrency, + ripple::Currency quoteAssetCurrency, + uint8_t scale +); + +[[nodiscard]] ripple::STArray +CreatePriceDataSeries(std::vector const& series); + +[[nodiscard]] ripple::STObject +CreateOracleObject( + std::string_view accountId, + std::string_view provider, + uint64_t ownerNode, + uint32_t lastUpdateTime, + ripple::Blob uri, + ripple::Blob assetClass, + uint32_t previousTxSeq, + ripple::uint256 previousTxId, + ripple::STArray priceDataSeries +);