From 584d2bb5f29194fe0c8c715da40e78b6c05137b1 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Tue, 17 Feb 2026 22:54:24 +0000 Subject: [PATCH] feat: Support Lending Protocol (#2945) --- conan.lock | 21 +- conanfile.py | 4 +- src/data/AmendmentCenter.hpp | 1 + src/rpc/handlers/AccountInfo.cpp | 16 + src/rpc/handlers/LedgerEntry.cpp | 17 +- src/rpc/handlers/LedgerEntry.hpp | 36 ++ tests/common/util/TestObject.cpp | 64 ++++ tests/common/util/TestObject.hpp | 23 ++ .../rpc/handlers/GetAggregatePriceTests.cpp | 14 +- tests/unit/rpc/handlers/LedgerEntryTests.cpp | 308 +++++++++++++++++- 10 files changed, 479 insertions(+), 25 deletions(-) diff --git a/conan.lock b/conan.lock index 3bbf1eee4..4c2c81221 100644 --- a/conan.lock +++ b/conan.lock @@ -3,15 +3,15 @@ "requires": [ "zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1765850150.075", "xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987", - "xrpl/3.0.0#534d3f65a336109eee929b88962bae4e%1765375071.547", + "xrpl/3.1.0#3d408ab8c8020014fa7dd52bc7cc7ea8%1769706825.165", "sqlite3/3.49.1#8631739a4c9b93bd3d6b753bac548a63%1765850149.926", - "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1767636069.964", + "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1768312128.781", "soci/4.0.3#a9f8d773cd33e356b5879a4b0564f287%1765850149.46", "re2/20230301#ca3b241baec15bd31ea9187150e0b333%1765850148.103", "rapidjson/cci.20220822#1b9d8c2256876a154172dc5cfbe447c6%1754325007.656", "protobuf/3.21.12#44ee56c0a6eea0c19aeeaca680370b88%1764175361.456", "openssl/1.1.1w#a8f0792d7c5121b954578a7149d23e03%1756223730.729", - "nudb/2.0.9#fb8dfd1a5557f5e0528114c2da17721e%1765850143.957", + "nudb/2.0.9#0432758a24204da08fee953ec9ea03cb%1769436073.32", "minizip/1.2.13#9e87d57804bd372d6d1e32b1871517a3%1754325004.374", "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914", "libuv/1.46.0#dc28c1f653fa197f00db5b577a6f6011%1754325003.592", @@ -19,7 +19,7 @@ "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03", "libarchive/3.8.1#ffee18995c706e02bf96e7a2f7042e0d%1765850144.736", "http_parser/2.9.4#98d91690d6fd021e9e624218a85d9d97%1754325001.385", - "gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1755784855.585", + "gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1768312129.152", "grpc/1.50.1#02291451d1e17200293a409410d1c4e1%1756234248.958", "fmt/12.1.0#50abab23274d56bb8f42c94b3b9a40c7%1763984116.926", "doctest/2.4.11#a4211dfc329a16ba9f280f9574025659%1756234220.819", @@ -40,17 +40,20 @@ ], "python_requires": [], "overrides": { - "boost/1.83.0": [ + "protobuf/3.21.12#44ee56c0a6eea0c19aeeaca680370b88": [ + null, + "protobuf/3.21.12" + ], + "boost/1.83.0#91d8b1572534d2c334d6790e3c34d0c1": [ null, "boost/1.83.0#91d8b1572534d2c334d6790e3c34d0c1" ], - "protobuf/3.21.12": [ - null, - "protobuf/3.21.12#44ee56c0a6eea0c19aeeaca680370b88" - ], "lz4/1.9.4": [ "lz4/1.10.0" ], + "boost/1.90.0": [ + "boost/1.83.0" + ], "sqlite3/3.44.2": [ "sqlite3/3.49.1" ] diff --git a/conanfile.py b/conanfile.py index f06e5d018..bbfce6e81 100644 --- a/conanfile.py +++ b/conanfile.py @@ -12,7 +12,6 @@ class ClioConan(ConanFile): options = {} requires = [ - "boost/1.83.0", "cassandra-cpp-driver/2.17.0", "fmt/12.1.0", "grpc/1.50.1", @@ -20,7 +19,7 @@ class ClioConan(ConanFile): "openssl/1.1.1w", "protobuf/3.21.12", "spdlog/1.17.0", - "xrpl/3.0.0", + "xrpl/3.1.0", "zlib/1.3.1", ] @@ -43,6 +42,7 @@ class ClioConan(ConanFile): exports_sources = ("CMakeLists.txt", "cmake/*", "src/*") def requirements(self): + self.requires("boost/1.83.0", force=True) self.requires("gtest/1.17.0") self.requires("benchmark/1.9.4") diff --git a/src/data/AmendmentCenter.hpp b/src/data/AmendmentCenter.hpp index 7b990d877..1da72373c 100644 --- a/src/data/AmendmentCenter.hpp +++ b/src/data/AmendmentCenter.hpp @@ -177,6 +177,7 @@ struct Amendments { REGISTER(fix1512); REGISTER(fix1523); REGISTER(fix1528); + REGISTER(fixBatchInnerSigs); // NOLINTEND(readability-identifier-naming) /** @endcond */ }; diff --git a/src/rpc/handlers/AccountInfo.cpp b/src/rpc/handlers/AccountInfo.cpp index 8631b5632..460f20ab6 100644 --- a/src/rpc/handlers/AccountInfo.cpp +++ b/src/rpc/handlers/AccountInfo.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -173,6 +174,21 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, AccountInfoHandl jv.as_object()[JS(account_flags)] = std::move(acctFlags); + auto const pseudoFields = ripple::getPseudoAccountFields(); + for (auto const& pseudoField : pseudoFields) { + if (output.accountData.isFieldPresent(*pseudoField)) { + std::string name = pseudoField->fieldName; + if (name.ends_with("ID")) { + // Remove the ID suffix from the field name. + name = name.substr(0, name.size() - 2); + ASSERT(!name.empty(), "Field name is empty after stripping 'ID'"); + } + // ValidPseudoAccounts invariant guarantees that only one field can be set + jv.as_object()[JS(pseudo_account)].as_object()[JS(type)] = name; + break; + } + } + if (output.signerLists) { auto signers = boost::json::array(); std::transform( diff --git a/src/rpc/handlers/LedgerEntry.cpp b/src/rpc/handlers/LedgerEntry.cpp index fa173fc5c..41c912fb2 100644 --- a/src/rpc/handlers/LedgerEntry.cpp +++ b/src/rpc/handlers/LedgerEntry.cpp @@ -192,6 +192,15 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input const& input, Context cons ripple::parseBase58(boost::json::value_to(input.vault->at(JS(owner)))); auto const seq = util::integralValueAs(input.vault->at(JS(seq))); key = ripple::keylet::vault(*account, seq).key; + } else if (input.loanBroker) { + auto const account = + ripple::parseBase58(boost::json::value_to(input.loanBroker->at(JS(owner)))); + auto const seq = util::integralValueAs(input.loanBroker->at(JS(seq))); + key = ripple::keylet::loanbroker(*account, seq).key; + } else if (input.loan) { + auto const id = ripple::uint256{boost::json::value_to(input.loan->at(JS(loan_broker_id))).data()}; + auto const seq = util::integralValueAs(input.loan->at(JS(loan_seq))); + key = ripple::keylet::loan(id, seq).key; } else if (input.delegate) { auto const account = ripple::parseBase58(boost::json::value_to(input.delegate->at(JS(account)))); @@ -333,13 +342,15 @@ tag_invoke(boost::json::value_to_tag, boost::json::va {JS(mptoken), ripple::ltMPTOKEN}, {JS(permissioned_domain), ripple::ltPERMISSIONED_DOMAIN}, {JS(vault), ripple::ltVAULT}, + {JS(loan_broker), ripple::ltLOAN_BROKER}, + {JS(loan), ripple::ltLOAN}, {JS(delegate), ripple::ltDELEGATE}, {JS(amendments), ripple::ltAMENDMENTS}, {JS(fee), ripple::ltFEE_SETTINGS}, {JS(hashes), ripple::ltLEDGER_HASHES}, {JS(nft_offer), ripple::ltNFTOKEN_OFFER}, {JS(nunl), ripple::ltNEGATIVE_UNL}, - {JS(signer_list), ripple::ltSIGNER_LIST} + {JS(signer_list), ripple::ltSIGNER_LIST}, }; auto const parseBridgeFromJson = [](boost::json::value const& bridgeJson) { @@ -430,6 +441,10 @@ tag_invoke(boost::json::value_to_tag, boost::json::va input.permissionedDomain = jv.at(JS(permissioned_domain)).as_object(); } else if (jsonObject.contains(JS(vault))) { input.vault = jv.at(JS(vault)).as_object(); + } else if (jsonObject.contains(JS(loan_broker))) { + input.loanBroker = jv.at(JS(loan_broker)).as_object(); + } else if (jsonObject.contains(JS(loan))) { + input.loan = jv.at(JS(loan)).as_object(); } else if (jsonObject.contains(JS(delegate))) { input.delegate = jv.at(JS(delegate)).as_object(); } diff --git a/src/rpc/handlers/LedgerEntry.hpp b/src/rpc/handlers/LedgerEntry.hpp index 4280d569c..97070bf07 100644 --- a/src/rpc/handlers/LedgerEntry.hpp +++ b/src/rpc/handlers/LedgerEntry.hpp @@ -105,6 +105,8 @@ public: std::optional mptoken; std::optional permissionedDomain; std::optional vault; + std::optional loanBroker; + std::optional loan; std::optional bridge; std::optional bridgeAccount; std::optional chainClaimId; @@ -411,6 +413,40 @@ public: }, }, }}}, + {JS(loan_broker), + meta::WithCustomError{ + validation::Type{}, Status(ClioError::RpcMalformedRequest) + }, + meta::IfType{kMALFORMED_REQUEST_HEX_STRING_VALIDATOR}, + meta::IfType{meta::Section{ + {JS(seq), + meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)}, + meta::WithCustomError{validation::Type{}, Status(ClioError::RpcMalformedRequest)}}, + { + JS(owner), + meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)}, + meta::WithCustomError{ + validation::CustomValidators::accountBase58Validator, Status(ClioError::RpcMalformedOwner) + }, + }, + }}}, + {JS(loan), + meta::WithCustomError{ + validation::Type{}, Status(ClioError::RpcMalformedRequest) + }, + meta::IfType{kMALFORMED_REQUEST_HEX_STRING_VALIDATOR}, + meta::IfType{meta::Section{ + {JS(loan_seq), + meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)}, + meta::WithCustomError{validation::Type{}, Status(ClioError::RpcMalformedRequest)}}, + { + JS(loan_broker_id), + meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)}, + meta::WithCustomError{ + validation::CustomValidators::uint256HexStringValidator, Status(ClioError::RpcMalformedRequest) + }, + }, + }}}, {JS(delegate), meta::WithCustomError{ validation::Type{}, Status(ClioError::RpcMalformedRequest) diff --git a/tests/common/util/TestObject.cpp b/tests/common/util/TestObject.cpp index 2db24dffd..59761d56f 100644 --- a/tests/common/util/TestObject.cpp +++ b/tests/common/util/TestObject.cpp @@ -1800,3 +1800,67 @@ createVault( return vault; } + +ripple::STObject +createLoanBroker( + std::string_view owner, + std::string_view account, + ripple::LedgerIndex seq, + ripple::uint256 vaultID, + uint32_t loanSequence, + ripple::uint256 previousTxId, + uint32_t previousTxSeq +) +{ + auto loanBroker = ripple::STObject(ripple::sfLedgerEntry); + loanBroker.setAccountID(ripple::sfOwner, getAccountIdWithString(owner)); + loanBroker.setAccountID(ripple::sfAccount, getAccountIdWithString(account)); + loanBroker.setFieldU32(ripple::sfSequence, seq); + loanBroker.setFieldU64(ripple::sfOwnerNode, 0); + loanBroker.setFieldU64(ripple::sfVaultNode, 0); + loanBroker.setFieldH256(ripple::sfVaultID, vaultID); + loanBroker.setFieldH256(ripple::sfPreviousTxnID, previousTxId); + loanBroker.setFieldU32(ripple::sfPreviousTxnLgrSeq, previousTxSeq); + loanBroker.setFieldU32(ripple::sfLoanSequence, loanSequence); + + // Optional/default fields - not setting them as they will use default values + + loanBroker.setFieldU32(ripple::sfFlags, 0); + loanBroker.setFieldU16(ripple::sfLedgerEntryType, ripple::ltLOAN_BROKER); + + return loanBroker; +} + +ripple::STObject +createLoan( + std::string_view borrower, + ripple::uint256 loanBrokerID, + uint32_t loanSequence, + uint32_t startDate, + uint32_t paymentInterval, + int64_t periodicPaymentValue, + ripple::uint256 previousTxId, + uint32_t previousTxSeq +) +{ + auto loan = ripple::STObject(ripple::sfLedgerEntry); + loan.setAccountID(ripple::sfBorrower, getAccountIdWithString(borrower)); + loan.setFieldH256(ripple::sfLoanBrokerID, loanBrokerID); + loan.setFieldU32(ripple::sfLoanSequence, loanSequence); + loan.setFieldU64(ripple::sfOwnerNode, 0); + loan.setFieldU64(ripple::sfLoanBrokerNode, 0); + loan.setFieldH256(ripple::sfPreviousTxnID, previousTxId); + loan.setFieldU32(ripple::sfPreviousTxnLgrSeq, previousTxSeq); + + loan.setFieldU32(ripple::sfStartDate, startDate); + loan.setFieldU32(ripple::sfPaymentInterval, paymentInterval); + + loan.setFieldNumber(ripple::sfPeriodicPayment, ripple::STNumber{ripple::sfPeriodicPayment, periodicPaymentValue}); + + // Optional/default fields - not setting them as they will use default values + + loan.setFieldU32(ripple::sfFlags, 0); + loan.setFieldU16(ripple::sfLedgerEntryType, ripple::ltLOAN); + + return loan; +} diff --git a/tests/common/util/TestObject.hpp b/tests/common/util/TestObject.hpp index e06fa62b2..3a208baba 100644 --- a/tests/common/util/TestObject.hpp +++ b/tests/common/util/TestObject.hpp @@ -581,3 +581,26 @@ createVault( ripple::uint256 previousTxId, uint32_t previousTxSeq ); + +[[nodiscard]] ripple::STObject +createLoanBroker( + std::string_view owner, + std::string_view account, + ripple::LedgerIndex seq, + ripple::uint256 vaultID, + uint32_t loanSequence, + ripple::uint256 previousTxId, + uint32_t previousTxSeq +); + +[[nodiscard]] ripple::STObject +createLoan( + std::string_view borrower, + ripple::uint256 loanBrokerID, + uint32_t loanSequence, + uint32_t startDate, + uint32_t paymentInterval, + int64_t periodicPaymentValue, + ripple::uint256 previousTxId, + uint32_t previousTxSeq +); diff --git a/tests/unit/rpc/handlers/GetAggregatePriceTests.cpp b/tests/unit/rpc/handlers/GetAggregatePriceTests.cpp index b974f82ea..07850565c 100644 --- a/tests/unit/rpc/handlers/GetAggregatePriceTests.cpp +++ b/tests/unit/rpc/handlers/GetAggregatePriceTests.cpp @@ -741,7 +741,7 @@ TEST_F(RPCGetAggregatePriceHandlerTest, OracleLedgerEntryMultipleOraclesOdd) "entire_set": {{ "mean": "110", "size": 3, - "standard_deviation": "164.6207763315433" + "standard_deviation": "164.6207763315432795" }}, "median": "20", "time": 4321, @@ -817,7 +817,7 @@ TEST_F(RPCGetAggregatePriceHandlerTest, OracleLedgerEntryMultipleOraclesEven) "entire_set": {{ "mean": "92.5", "size": 4, - "standard_deviation": "138.8944443333378" + "standard_deviation": "138.8944443333377776" }}, "median": "30", "time": 4321, @@ -895,12 +895,12 @@ TEST_F(RPCGetAggregatePriceHandlerTest, OracleLedgerEntryTrim) "entire_set": {{ "mean": "92.5", "size": 4, - "standard_deviation": "138.8944443333378" + "standard_deviation": "138.8944443333377776" }}, "trimmed_set": {{ "mean": "30", "size": 2, - "standard_deviation": "14.14213562373095" + "standard_deviation": "14.14213562373095049" }}, "median": "30", "time": 4321, @@ -1134,7 +1134,7 @@ TEST_F(RPCGetAggregatePriceHandlerTest, ValidTimeThreshold) "entire_set": {{ "mean": "15", "size": 2, - "standard_deviation": "7.071067811865475" + "standard_deviation": "7.071067811865475245" }}, "median": "15", "time": {}, @@ -1216,7 +1216,7 @@ TEST_F(RPCGetAggregatePriceHandlerTest, TimeThresholdTooLong) "entire_set": {{ "mean": "92.5", "size": 4, - "standard_deviation": "138.8944443333378" + "standard_deviation": "138.8944443333377776" }}, "median": "30", "time": 1711461384, @@ -1297,7 +1297,7 @@ TEST_F(RPCGetAggregatePriceHandlerTest, TimeThresholdIncludeOldest) "entire_set": {{ "mean": "92.5", "size": 4, - "standard_deviation": "138.8944443333378" + "standard_deviation": "138.8944443333377776" }}, "median": "30", "time": 1711461384, diff --git a/tests/unit/rpc/handlers/LedgerEntryTests.cpp b/tests/unit/rpc/handlers/LedgerEntryTests.cpp index 6c691b0a3..6b6d31a9e 100644 --- a/tests/unit/rpc/handlers/LedgerEntryTests.cpp +++ b/tests/unit/rpc/handlers/LedgerEntryTests.cpp @@ -2174,6 +2174,154 @@ generateTestValuesForParametersTest() .expectedError = "malformedRequest", .expectedErrorMessage = "Malformed request.", }, + // LoanBroker tests + ParamTestCaseBundle{ + .testName = "LoanBroker_InvalidType", + .testJson = R"JSON({"loan_broker": 0})JSON", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "LoanBroker_NotHex", + .testJson = + R"JSON({ + "loan_broker": "invalid_hex" + })JSON", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "LoanBroker_MissingOwner", + .testJson = + R"JSON({ + "loan_broker": { "seq": 1 } + })JSON", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "LoanBroker_MissingSeq", + .testJson = + R"JSON({ + "loan_broker": { "owner": "abcd" } + })JSON", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "LoanBroker_SeqNotInteger", + .testJson = fmt::format( + R"JSON({{ + "loan_broker": {{ + "owner": "{}", + "seq": "notAnInteger" + }} + }})JSON", + kACCOUNT + ), + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "LoanBroker_InvalidOwnerFormat", + .testJson = + R"JSON({ + "loan_broker": { + "owner": "abcd", + "seq": 10 + } + })JSON", + .expectedError = "malformedOwner", + .expectedErrorMessage = "Malformed owner.", + }, + ParamTestCaseBundle{ + .testName = "LoanBroker_NegativeSeq", + .testJson = + R"JSON({ + "loan_broker": { + "owner": "abcd", + "seq": -200 + } + })JSON", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + // Loan tests + ParamTestCaseBundle{ + .testName = "Loan_InvalidType", + .testJson = R"JSON({"loan": 0})JSON", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "Loan_NotHex", + .testJson = + R"JSON({ + "loan": "invalid_hex" + })JSON", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "Loan_MissingLoanBrokerId", + .testJson = + R"JSON({ + "loan": { "loan_seq": 1 } + })JSON", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "Loan_MissingLoanSeq", + .testJson = fmt::format( + R"JSON({{ + "loan": {{ "loan_broker_id": "{}" }} + }})JSON", + kINDEX1 + ), + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "Loan_LoanSeqNotInteger", + .testJson = fmt::format( + R"JSON({{ + "loan": {{ + "loan_broker_id": "{}", + "loan_seq": "notAnInteger" + }} + }})JSON", + kINDEX1 + ), + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "Loan_InvalidLoanBrokerIdFormat", + .testJson = + R"JSON({ + "loan": { + "loan_broker_id": "invalid_hex", + "loan_seq": 10 + } + })JSON", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "Loan_NegativeLoanSeq", + .testJson = fmt::format( + R"JSON({{ + "loan": {{ + "loan_broker_id": "{}", + "loan_seq": -200 + }} + }})JSON", + kINDEX1 + ), + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, ParamTestCaseBundle{ .testName = "Delegate_InvalidType", .testJson = R"JSON({"delegate": 123})JSON", @@ -2377,7 +2525,6 @@ TEST_P(IndexTest, InvalidIndexNotString) TEST_F(RPCLedgerEntryTest, LedgerEntryNotFound) { - // return valid ledgerHeader auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kRANGE_MAX); EXPECT_CALL(*backend_, fetchLedgerBySequence(kRANGE_MAX, _)).WillRepeatedly(Return(ledgerHeader)); @@ -3106,6 +3253,64 @@ generateTestValuesForNormalPathTest() 0 ) }, + NormalPathTestBundle{ + .testName = "CreateLoanBrokerObjectByHexString", + .testJson = fmt::format( + R"JSON({{ + "binary": true, + "loan_broker": "{}" + }})JSON", + kINDEX1 + ), + .expectedIndex = ripple::uint256(kINDEX1), + .mockedEntity = + createLoanBroker(kACCOUNT, kACCOUNT, kRANGE_MAX, ripple::uint256{kINDEX1}, 1, ripple::uint256{0}, 0) + }, + NormalPathTestBundle{ + .testName = "CreateLoanBrokerObjectByOwnerAndSeq", + .testJson = fmt::format( + R"JSON({{ + "binary": true, + "loan_broker": {{ + "owner": "{}", + "seq": {} + }} + }})JSON", + kACCOUNT, + kRANGE_MAX + ), + .expectedIndex = + ripple::keylet::loanbroker(ripple::parseBase58(kACCOUNT).value(), kRANGE_MAX).key, + .mockedEntity = + createLoanBroker(kACCOUNT, kACCOUNT, kRANGE_MAX, ripple::uint256{kINDEX1}, 1, ripple::uint256{0}, 0) + }, + NormalPathTestBundle{ + .testName = "CreateLoanObjectByHexString", + .testJson = fmt::format( + R"JSON({{ + "binary": true, + "loan": "{}" + }})JSON", + kINDEX1 + ), + .expectedIndex = ripple::uint256(kINDEX1), + .mockedEntity = createLoan(kACCOUNT, ripple::uint256{kINDEX1}, 1, 1000, 86400, 100, ripple::uint256{0}, 0) + }, + NormalPathTestBundle{ + .testName = "CreateLoanObjectByLoanBrokerIdAndSeq", + .testJson = fmt::format( + R"JSON({{ + "binary": true, + "loan": {{ + "loan_broker_id": "{}", + "loan_seq": 1 + }} + }})JSON", + kINDEX1 + ), + .expectedIndex = ripple::keylet::loan(ripple::uint256{kINDEX1}, 1).key, + .mockedEntity = createLoan(kACCOUNT, ripple::uint256{kINDEX1}, 1, 1000, 86400, 100, ripple::uint256{0}, 0) + }, NormalPathTestBundle{ .testName = "DelegateViaStringIndex", .testJson = fmt::format( @@ -3151,7 +3356,6 @@ TEST_P(RPCLedgerEntryNormalPathTest, NormalPath) { auto const testBundle = GetParam(); - // return valid ledgerHeader auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kRANGE_MAX); EXPECT_CALL(*backend_, fetchLedgerBySequence(kRANGE_MAX, _)).WillRepeatedly(Return(ledgerHeader)); @@ -3199,7 +3403,6 @@ TEST_F(RPCLedgerEntryTest, BinaryFalse) } })JSON"; - // return valid ledgerHeader auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kRANGE_MAX); EXPECT_CALL(*backend_, fetchLedgerBySequence(kRANGE_MAX, _)).WillRepeatedly(Return(ledgerHeader)); @@ -3226,7 +3429,6 @@ TEST_F(RPCLedgerEntryTest, BinaryFalse) TEST_F(RPCLedgerEntryTest, Vault_BinaryFalse) { - // return valid ledgerHeader auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kRANGE_MAX); EXPECT_CALL(*backend_, fetchLedgerBySequence(kRANGE_MAX, _)).WillRepeatedly(Return(ledgerHeader)); @@ -3277,9 +3479,104 @@ TEST_F(RPCLedgerEntryTest, Vault_BinaryFalse) }); } +TEST_F(RPCLedgerEntryTest, LoanBroker_BinaryFalse) +{ + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kRANGE_MAX); + EXPECT_CALL(*backend_, fetchLedgerBySequence(kRANGE_MAX, _)).WillRepeatedly(Return(ledgerHeader)); + + boost::json::object const entry; + + auto const loanBroker = + createLoanBroker(kACCOUNT, kACCOUNT, kRANGE_MAX, ripple::uint256{kINDEX1}, 1, ripple::uint256{1}, 0); + + auto const loanBrokerKey = + ripple::keylet::loanbroker(ripple::parseBase58(kACCOUNT).value(), kRANGE_MAX).key; + + ripple::STLedgerEntry const sle{ + ripple::SerialIter{loanBroker.getSerializer().peekData().data(), loanBroker.getSerializer().peekData().size()}, + loanBrokerKey + }; + + EXPECT_CALL(*backend_, doFetchLedgerObject(loanBrokerKey, testing::_, testing::_)) + .WillOnce(Return(loanBroker.getSerializer().peekData())); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{LedgerEntryHandler{backend_}}; + auto const req = json::parse( + fmt::format( + R"JSON({{ + "binary": false, + "loan_broker": {{ + "owner": "{}", + "seq": {} + }} + }})JSON", + kACCOUNT, + kRANGE_MAX + ) + ); + auto const output = handler.process(req, Context{yield}); + ASSERT_TRUE(output); + + EXPECT_EQ(output.result->at("node").at("Owner").as_string(), kACCOUNT); + EXPECT_EQ(output.result->at("node").at("Sequence").as_int64(), kRANGE_MAX); + EXPECT_EQ(output.result->at("node").at("LoanSequence").as_int64(), 1); + }); +} + +TEST_F(RPCLedgerEntryTest, Loan_BinaryFalse) +{ + static constexpr auto kLOAN_SEQ = 1; + static constexpr auto kSTART_DATE = 1000; + static constexpr auto kPAYMENT_INTERVAL = 86400; + static constexpr auto kINTEREST_RATE = 100; + + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kRANGE_MAX); + EXPECT_CALL(*backend_, fetchLedgerBySequence(kRANGE_MAX, _)).WillRepeatedly(Return(ledgerHeader)); + + boost::json::object const entry; + + auto const loan = createLoan( + kACCOUNT, + ripple::uint256{kINDEX1}, + kLOAN_SEQ, + kSTART_DATE, + kPAYMENT_INTERVAL, + kINTEREST_RATE, + ripple::uint256{1}, + 0 + ); + + EXPECT_CALL(*backend_, doFetchLedgerObject(testing::_, testing::_, testing::_)) + .WillOnce(Return(loan.getSerializer().peekData())); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{LedgerEntryHandler{backend_}}; + auto const req = json::parse( + fmt::format( + R"JSON({{ + "binary": false, + "loan": {{ + "loan_broker_id": "{}", + "loan_seq": {} + }} + }})JSON", + kINDEX1, + kLOAN_SEQ + ) + ); + auto const output = handler.process(req, Context{yield}); + ASSERT_TRUE(output); + + EXPECT_EQ(output.result->at("node").at("Borrower").as_string(), kACCOUNT); + EXPECT_EQ(output.result->at("node").at("LoanSequence").as_int64(), kLOAN_SEQ); + EXPECT_EQ(output.result->at("node").at("StartDate").as_int64(), kSTART_DATE); + EXPECT_EQ(output.result->at("node").at("PaymentInterval").as_int64(), kPAYMENT_INTERVAL); + }); +} + TEST_F(RPCLedgerEntryTest, UnexpectedLedgerType) { - // return valid ledgerHeader auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kRANGE_MAX); EXPECT_CALL(*backend_, fetchLedgerBySequence(kRANGE_MAX, _)).WillRepeatedly(Return(ledgerHeader)); @@ -3768,7 +4065,6 @@ TEST_F(RPCLedgerEntryTest, SyntheticMPTIssuanceID) auto const mptId = ripple::makeMptID(2, getAccountIdWithString(kACCOUNT)); - // return valid ledgerHeader auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kRANGE_MAX); EXPECT_CALL(*backend_, fetchLedgerBySequence(kRANGE_MAX, _)).WillRepeatedly(Return(ledgerHeader));