#include "data/AmendmentCenter.hpp" #include "data/Types.hpp" #include "rpc/Errors.hpp" #include "rpc/RPCHelpers.hpp" #include "rpc/common/AnyHandler.hpp" #include "rpc/common/Types.hpp" #include "rpc/handlers/BookOffers.hpp" #include "util/HandlerBaseTestFixture.hpp" #include "util/MockAmendmentCenter.hpp" #include "util/NameGenerator.hpp" #include "util/TestObject.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr auto kAccount = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; constexpr auto kAccount2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; constexpr auto kLedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; constexpr auto kIndex1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; constexpr auto kIndex2 = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"; constexpr auto kPayS20UsdGetS10XrpBookDir = "43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000"; constexpr auto kPayS20XrpGetS10UsdBookDir = "7B1767D41DBCE79D9585CF9D0262A5FEC45E5206FF524F8B55071AFD498D0000"; constexpr auto kTransferRateX2 = 2000000000; constexpr auto kDomain = "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134E5"; struct ParameterTestBundle { std::string testName; std::string testJson; std::string expectedError; std::string expectedErrorMessage; }; } // namespace using namespace rpc; using namespace data; namespace json = boost::json; using namespace testing; struct RPCBookOffersHandlerTest : HandlerBaseTest { RPCBookOffersHandlerTest() { backend_->setRange(10, 300); } protected: StrictMockAmendmentCenterSharedPtr mockAmendmentCenterPtr_; }; struct RPCBookOffersParameterTest : RPCBookOffersHandlerTest, WithParamInterface {}; TEST_P(RPCBookOffersParameterTest, CheckError) { auto bundle = GetParam(); auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}}; runSpawn([&](boost::asio::yield_context yield) { auto const output = handler.process(json::parse(bundle.testJson), Context{.yield = yield}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), bundle.expectedError); EXPECT_EQ(err.at("error_message").as_string(), bundle.expectedErrorMessage); }); } static auto generateParameterBookOffersTestBundles() { return std::vector{ ParameterTestBundle{ .testName = "MissingTakerGets", .testJson = R"JSON({ "taker_pays": { "currency": "USD", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" } })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Required field 'taker_gets' missing" }, ParameterTestBundle{ .testName = "MissingTakerPays", .testJson = R"JSON({ "taker_gets": { "currency": "USD", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" } })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Required field 'taker_pays' missing" }, ParameterTestBundle{ .testName = "WrongTypeTakerPays", .testJson = R"JSON({ "taker_pays": "wrong", "taker_gets": { "currency": "XRP" } })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, ParameterTestBundle{ .testName = "WrongTypeTakerGets", .testJson = R"JSON({ "taker_gets": "wrong", "taker_pays": { "currency": "XRP" } })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, ParameterTestBundle{ .testName = "TakerPaysMissingCurrency", .testJson = R"JSON({ "taker_pays": {}, "taker_gets": { "currency": "XRP" } })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Required field 'currency' missing" }, ParameterTestBundle{ .testName = "TakerGetsMissingCurrency", .testJson = R"JSON({ "taker_gets": {}, "taker_pays": { "currency": "XRP" } })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Required field 'currency' missing" }, ParameterTestBundle{ .testName = "TakerGetsWrongCurrency", .testJson = R"JSON({ "taker_gets": { "currency": "CNYY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_pays": { "currency": "XRP" } })JSON", .expectedError = "dstAmtMalformed", .expectedErrorMessage = "Destination amount/currency/issuer is malformed." }, ParameterTestBundle{ .testName = "TakerPaysWrongCurrency", .testJson = R"JSON({ "taker_pays": { "currency": "CNYY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP" } })JSON", .expectedError = "srcCurMalformed", .expectedErrorMessage = "Source currency is malformed." }, ParameterTestBundle{ .testName = "TakerGetsCurrencyNotString", .testJson = R"JSON({ "taker_gets": { "currency": 123, "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_pays": { "currency": "XRP" } })JSON", .expectedError = "dstAmtMalformed", .expectedErrorMessage = "Destination amount/currency/issuer is malformed." }, ParameterTestBundle{ .testName = "TakerPaysCurrencyNotString", .testJson = R"JSON({ "taker_pays": { "currency": 123, "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP" } })JSON", .expectedError = "srcCurMalformed", .expectedErrorMessage = "Source currency is malformed." }, ParameterTestBundle{ .testName = "TakerGetsWrongIssuer", .testJson = R"JSON({ "taker_gets": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs5" }, "taker_pays": { "currency": "XRP" } })JSON", .expectedError = "dstIsrMalformed", .expectedErrorMessage = "Destination issuer is malformed." }, ParameterTestBundle{ .testName = "TakerPaysWrongIssuer", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs5" }, "taker_gets": { "currency": "XRP" } })JSON", .expectedError = "srcIsrMalformed", .expectedErrorMessage = "Source issuer is malformed." }, ParameterTestBundle{ .testName = "InvalidTaker", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP" }, "taker": "123" })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid field 'taker'." }, ParameterTestBundle{ .testName = "TakerNotString", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP" }, "taker": 123 })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid field 'taker'." }, ParameterTestBundle{ .testName = "Domain_InvalidType", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP" }, "domain": 0 })JSON", .expectedError = "domainMalformed", .expectedErrorMessage = "Unable to parse domain." }, ParameterTestBundle{ .testName = "Domain_InvalidInt", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP" }, "domain": "123" })JSON", .expectedError = "domainMalformed", .expectedErrorMessage = "Unable to parse domain." }, ParameterTestBundle{ .testName = "Domain_InvalidObject", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP" }, "domain": {} })JSON", .expectedError = "domainMalformed", .expectedErrorMessage = "Unable to parse domain." }, ParameterTestBundle{ .testName = "LimitNotInt", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP" }, "limit": "123" })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, ParameterTestBundle{ .testName = "LimitNegative", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP" }, "limit": -1 })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, ParameterTestBundle{ .testName = "LimitZero", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP" }, "limit": 0 })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, ParameterTestBundle{ .testName = "LedgerIndexInvalid", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP" }, "ledger_index": "xxx" })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "ledgerIndexMalformed" }, ParameterTestBundle{ .testName = "LedgerHashInvalid", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP" }, "ledger_hash": "xxx" })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "ledger_hashMalformed" }, ParameterTestBundle{ .testName = "LedgerHashNotString", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP" }, "ledger_hash": 123 })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "ledger_hashNotString" }, ParameterTestBundle{ .testName = "GetsPaysXRPWithIssuer", .testJson = R"JSON({ "taker_pays": { "currency": "XRP", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "CNY", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" } })JSON", .expectedError = "srcIsrMalformed", .expectedErrorMessage = "Unneeded field 'taker_pays.issuer' for XRP currency specification." }, ParameterTestBundle{ .testName = "PaysCurrencyWithXRPIssuer", .testJson = R"JSON({ "taker_pays": { "currency": "JPY" }, "taker_gets": { "currency": "CNY", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" } })JSON", .expectedError = "srcIsrMalformed", .expectedErrorMessage = "Invalid field 'taker_pays.issuer', expected non-XRP issuer." }, ParameterTestBundle{ .testName = "GetsCurrencyWithXRPIssuer", .testJson = R"JSON({ "taker_pays": { "currency": "XRP" }, "taker_gets": { "currency": "CNY" } })JSON", .expectedError = "dstIsrMalformed", .expectedErrorMessage = "Invalid field 'taker_gets.issuer', expected non-XRP issuer." }, ParameterTestBundle{ .testName = "GetsXRPWithIssuer", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "XRP", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" } })JSON", .expectedError = "dstIsrMalformed", .expectedErrorMessage = "Unneeded field 'taker_gets.issuer' for XRP currency specification." }, ParameterTestBundle{ .testName = "BadMarket", .testJson = R"JSON({ "taker_pays": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, "taker_gets": { "currency": "CNY", "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" } })JSON", .expectedError = "badMarket", .expectedErrorMessage = "badMarket" } }; } INSTANTIATE_TEST_SUITE_P( RPCBookOffersHandler, RPCBookOffersParameterTest, testing::ValuesIn(generateParameterBookOffersTestBundles()), tests::util::kNameGenerator ); struct BookOffersNormalTestBundle { std::string testName; std::string inputJson; std::map> mockedSuccessors; std::map mockedLedgerObjects; uint32_t ledgerObjectCalls; std::vector mockedOffers; std::string expectedJson; uint32_t amendmentIsEnabledCalls = 0; }; struct RPCBookOffersNormalPathTest : public RPCBookOffersHandlerTest, public WithParamInterface {}; TEST_P(RPCBookOffersNormalPathTest, CheckOutput) { auto const& bundle = GetParam(); auto const seq = 300; EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); // return valid ledgerHeader auto const ledgerHeader = createLedgerHeader(kLedgerHash, seq); ON_CALL(*backend_, fetchLedgerBySequence(seq, _)).WillByDefault(Return(ledgerHeader)); EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(_, Amendments::fixFrozenLPTokenTransfer, _)) .Times(bundle.amendmentIsEnabledCalls); ON_CALL(*mockAmendmentCenterPtr_, isEnabled(_, Amendments::fixFrozenLPTokenTransfer, _)) .WillByDefault(Return(false)); // return valid book dir EXPECT_CALL(*backend_, doFetchSuccessorKey).Times(bundle.mockedSuccessors.size()); for (auto const& [key, value] : bundle.mockedSuccessors) { ON_CALL(*backend_, doFetchSuccessorKey(key, seq, _)).WillByDefault(Return(value)); } EXPECT_CALL(*backend_, doFetchLedgerObject).Times(bundle.ledgerObjectCalls); for (auto const& [key, value] : bundle.mockedLedgerObjects) { ON_CALL(*backend_, doFetchLedgerObject(key, seq, _)).WillByDefault(Return(value)); } std::vector bbs; std::ranges::transform( bundle.mockedOffers, std::back_inserter(bbs), [](auto const& obj) { return obj.getSerializer().peekData(); } ); ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs)); EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1); auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}}; runSpawn([&](boost::asio::yield_context yield) { auto const output = handler.process(json::parse(bundle.inputJson), Context{.yield = yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result.value(), json::parse(bundle.expectedJson)); }); } static auto generateNormalPathBookOffersTestBundles() { auto const account = getAccountIdWithString(kAccount); auto const account2 = getAccountIdWithString(kAccount2); auto const frozenTrustLine = createRippleStateLedgerObject( "USD", kAccount, -8, kAccount2, 1000, kAccount, 2000, kIndex1, 2, ripple::lsfLowFreeze ); auto const gets10USDPays20XRPOffer = createOfferLedgerObject( kAccount2, 10, 20, ripple::to_string(ripple::to_currency("USD")), ripple::to_string(ripple::xrpCurrency()), kAccount, toBase58(ripple::xrpAccount()), kPayS20XrpGetS10UsdBookDir ); auto const gets10USDPays20XRPOwnerOffer = createOfferLedgerObject( kAccount, 10, 20, ripple::to_string(ripple::to_currency("USD")), ripple::to_string(ripple::xrpCurrency()), kAccount, toBase58(ripple::xrpAccount()), kPayS20XrpGetS10UsdBookDir ); auto const gets10XRPPays20USDOffer = createOfferLedgerObject( kAccount2, 10, 20, ripple::to_string(ripple::xrpCurrency()), ripple::to_string(ripple::to_currency("USD")), toBase58(ripple::xrpAccount()), kAccount, kPayS20UsdGetS10XrpBookDir ); auto const gets10XRPPays20USDOfferWithDomain = createOfferLedgerObject( kAccount2, 10, 20, ripple::to_string(ripple::xrpCurrency()), ripple::to_string(ripple::to_currency("USD")), toBase58(ripple::xrpAccount()), kAccount, kPayS20UsdGetS10XrpBookDir, kDomain ); auto const getsXRPPaysUSDBook = getBookBase( rpc::parseBook( ripple::to_currency("USD"), account, ripple::xrpCurrency(), ripple::xrpAccount(), std::nullopt ) .value() ); auto const getsXRPPaysUSDBookWithDomain = getBookBase( rpc::parseBook( ripple::to_currency("USD"), account, ripple::xrpCurrency(), ripple::xrpAccount(), kDomain ) .value() ); auto const getsUSDPaysXRPBook = getBookBase( rpc::parseBook( ripple::xrpCurrency(), ripple::xrpAccount(), ripple::to_currency("USD"), account, std::nullopt ) .value() ); auto const getsXRPPaysUSDInputJson = fmt::format( R"JSON({{ "taker_gets": {{ "currency": "XRP" }}, "taker_pays": {{ "currency": "USD", "issuer": "{}" }} }})JSON", kAccount ); auto const getsXRPPaysUSDInputJsonWithDomain = fmt::format( R"JSON({{ "taker_gets": {{ "currency": "XRP" }}, "taker_pays": {{ "currency": "USD", "issuer": "{}" }}, "domain": "{}" }})JSON", kAccount, kDomain ); auto const paysXRPGetsUSDInputJson = fmt::format( R"JSON({{ "taker_pays": {{ "currency": "XRP" }}, "taker_gets": {{ "currency": "USD", "issuer": "{}" }} }})JSON", kAccount ); auto const feeLedgerObject = createLegacyFeeSettingBlob(1, 2, 3, 4, 0); auto const trustline30Balance = createRippleStateLedgerObject( "USD", kAccount, -30, kAccount2, 1000, kAccount, 2000, kIndex1, 2, 0 ); auto const trustline8Balance = createRippleStateLedgerObject( "USD", kAccount, -8, kAccount2, 1000, kAccount, 2000, kIndex1, 2, 0 ); return std::vector{ BookOffersNormalTestBundle{ .testName = "PaysUSDGetsXRPNoFrozenOwnerFundEnough", .inputJson = getsXRPPaysUSDInputJson, // prepare offer dir index .mockedSuccessors = std::map>{ {getsXRPPaysUSDBook, ripple::uint256{kPayS20UsdGetS10XrpBookDir}}, {ripple::uint256{kPayS20UsdGetS10XrpBookDir}, std::optional{}} }, .mockedLedgerObjects = std::map{ // book dir object {ripple::uint256{kPayS20UsdGetS10XrpBookDir}, createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1) .getSerializer() .peekData()}, // pays issuer account object {ripple::keylet::account(account).key, createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2) .getSerializer() .peekData()}, // owner account object {ripple::keylet::account(account2).key, createAccountRootObject(kAccount2, 0, 2, 200, 2, kIndex1, 2) .getSerializer() .peekData()}, // fee settings: base ->3 inc->2, account2 has 2 objects ,total // reserve ->7 // owner_funds should be 193 {ripple::keylet::fees().key, feeLedgerObject} }, .ledgerObjectCalls = 5, .mockedOffers = std::vector{gets10XRPPays20USDOffer}, .expectedJson = fmt::format( R"JSON({{ "ledger_hash": "{}", "ledger_index": 300, "offers": [ {{ "Account": "{}", "BookDirectory": "43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000", "BookNode": "0", "Flags": 0, "LedgerEntryType": "Offer", "OwnerNode": "0", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "Sequence": 0, "TakerGets": "10", "TakerPays": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "20" }}, "index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", "owner_funds": "{}", "quality": "{}" }} ] }})JSON", kLedgerHash, kAccount2, 193, 2 ) }, BookOffersNormalTestBundle{ .testName = "PaysUSDGetsXRPNoFrozenOwnerFundNotEnough", .inputJson = getsXRPPaysUSDInputJson, // prepare offer dir index .mockedSuccessors = std::map>{ {getsXRPPaysUSDBook, ripple::uint256{kPayS20UsdGetS10XrpBookDir}}, {ripple::uint256{kPayS20UsdGetS10XrpBookDir}, std::optional{}} }, .mockedLedgerObjects = std::map{ // book dir object {ripple::uint256{kPayS20UsdGetS10XrpBookDir}, createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1) .getSerializer() .peekData()}, // pays issuer account object {ripple::keylet::account(account).key, createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2) .getSerializer() .peekData()}, // owner account object, hold {ripple::keylet::account(account2).key, createAccountRootObject(kAccount2, 0, 2, 5 + 7, 2, kIndex1, 2) .getSerializer() .peekData()}, // fee settings: base ->3 inc->2, account2 has 2 objects // ,total // reserve ->7 {ripple::keylet::fees().key, feeLedgerObject} }, .ledgerObjectCalls = 5, .mockedOffers = std::vector{gets10XRPPays20USDOffer}, .expectedJson = fmt::format( R"JSON({{ "ledger_hash": "{}", "ledger_index": 300, "offers": [ {{ "Account": "{}", "BookDirectory": "43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000", "BookNode": "0", "Flags": 0, "LedgerEntryType": "Offer", "OwnerNode": "0", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "Sequence": 0, "TakerGets": "10", "TakerPays": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "20" }}, "index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", "owner_funds": "{}", "quality": "{}", "taker_gets_funded": "5", "taker_pays_funded": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "10" }} }} ] }})JSON", kLedgerHash, kAccount2, 5, 2 ) }, BookOffersNormalTestBundle{ .testName = "PaysUSDGetsXRPFrozen", .inputJson = getsXRPPaysUSDInputJson, // prepare offer dir index .mockedSuccessors = std::map>{ {getsXRPPaysUSDBook, ripple::uint256{kPayS20UsdGetS10XrpBookDir}}, {ripple::uint256{kPayS20UsdGetS10XrpBookDir}, std::optional{}} }, .mockedLedgerObjects = std::map{ // book dir object {ripple::uint256{kPayS20UsdGetS10XrpBookDir}, createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1) .getSerializer() .peekData()}, // pays issuer account object {ripple::keylet::account(account).key, createAccountRootObject( kAccount, ripple::lsfGlobalFreeze, 2, 200, 2, kIndex1, 2 ) .getSerializer() .peekData()} }, .ledgerObjectCalls = 3, .mockedOffers = std::vector{gets10XRPPays20USDOffer}, .expectedJson = fmt::format( R"JSON({{ "ledger_hash": "{}", "ledger_index": 300, "offers": [ {{ "Account": "{}", "BookDirectory": "43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000", "BookNode": "0", "Flags": 0, "LedgerEntryType": "Offer", "OwnerNode": "0", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "Sequence": 0, "TakerGets": "10", "TakerPays": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "20" }}, "index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", "owner_funds": "{}", "quality": "{}", "taker_gets_funded": "0", "taker_pays_funded": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "0" }} }} ] }})JSON", kLedgerHash, kAccount2, 0, 2 ) }, BookOffersNormalTestBundle{ .testName = "PaysUSDGetsXRPFrozenWithDomain", .inputJson = getsXRPPaysUSDInputJsonWithDomain, // prepare offer dir index .mockedSuccessors = std::map>{ {getsXRPPaysUSDBookWithDomain, ripple::uint256{kPayS20UsdGetS10XrpBookDir}}, {ripple::uint256{kPayS20UsdGetS10XrpBookDir}, std::optional{}} }, .mockedLedgerObjects = std::map{ // book dir object {ripple::uint256{kPayS20UsdGetS10XrpBookDir}, createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1) .getSerializer() .peekData()}, // pays issuer account object {ripple::keylet::account(account).key, createAccountRootObject( kAccount, ripple::lsfGlobalFreeze, 2, 200, 2, kIndex1, 2 ) .getSerializer() .peekData()} }, .ledgerObjectCalls = 3, .mockedOffers = std::vector{gets10XRPPays20USDOfferWithDomain}, .expectedJson = fmt::format( R"JSON({{ "ledger_hash": "{}", "ledger_index": 300, "offers": [ {{ "Account": "{}", "BookDirectory": "43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000", "BookNode": "0", "DomainID": "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134E5", "Flags": 0, "LedgerEntryType": "Offer", "OwnerNode": "0", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "Sequence": 0, "TakerGets": "10", "TakerPays": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "20" }}, "index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", "owner_funds": "{}", "quality": "{}", "taker_gets_funded": "0", "taker_pays_funded": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "0" }} }} ] }})JSON", kLedgerHash, kAccount2, 0, 2 ) }, BookOffersNormalTestBundle{ .testName = "GetsUSDPaysXRPFrozen", .inputJson = paysXRPGetsUSDInputJson, // prepare offer dir index .mockedSuccessors = std::map>{ {getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}}, {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional{}} }, .mockedLedgerObjects = std::map{ // book dir object {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1) .getSerializer() .peekData()}, // gets issuer account object {ripple::keylet::account(account).key, createAccountRootObject( kAccount, ripple::lsfGlobalFreeze, 2, 200, 2, kIndex1, 2, kTransferRateX2 ) .getSerializer() .peekData()} }, .ledgerObjectCalls = 3, .mockedOffers = std::vector{gets10USDPays20XRPOffer}, .expectedJson = fmt::format( R"JSON({{ "ledger_hash": "{}", "ledger_index": 300, "offers": [ {{ "Account": "{}", "BookDirectory": "{}", "BookNode": "0", "Flags": 0, "LedgerEntryType": "Offer", "OwnerNode": "0", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "Sequence": 0, "TakerPays": "20", "TakerGets": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "10" }}, "index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", "owner_funds": "{}", "quality": "{}", "taker_pays_funded": "0", "taker_gets_funded": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "0" }} }} ] }})JSON", kLedgerHash, kAccount2, kPayS20XrpGetS10UsdBookDir, 0, 2 ) }, BookOffersNormalTestBundle{ .testName = "PaysXRPGetsUSDWithTransferFee", .inputJson = paysXRPGetsUSDInputJson, // prepare offer dir index .mockedSuccessors = std::map>{ {getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}}, {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional{}} }, .mockedLedgerObjects = std::map{ // book dir object {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1) .getSerializer() .peekData()}, // gets issuer account object, rate is 1/2 {ripple::keylet::account(account).key, createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2, kTransferRateX2) .getSerializer() .peekData()}, // trust line between gets issuer and owner,owner has 8 USD {ripple::keylet::line(account2, account, ripple::to_currency("USD")).key, trustline8Balance.getSerializer().peekData()}, }, .ledgerObjectCalls = 6, .mockedOffers = std::vector{gets10USDPays20XRPOffer}, .expectedJson = fmt::format( R"JSON({{ "ledger_hash": "{}", "ledger_index": 300, "offers": [ {{ "Account": "{}", "BookDirectory": "{}", "BookNode": "0", "Flags": 0, "LedgerEntryType": "Offer", "OwnerNode": "0", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "Sequence": 0, "TakerPays": "20", "TakerGets": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "10" }}, "index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", "owner_funds": "{}", "quality": "{}", "taker_gets_funded": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "4" }}, "taker_pays_funded": "8" }} ] }})JSON", kLedgerHash, kAccount2, kPayS20XrpGetS10UsdBookDir, 8, 2 ), .amendmentIsEnabledCalls = 1, }, BookOffersNormalTestBundle{ .testName = "PaysXRPGetsUSDWithMultipleOffers", .inputJson = paysXRPGetsUSDInputJson, // prepare offer dir index .mockedSuccessors = std::map>{ {getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}}, {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional{}} }, .mockedLedgerObjects = std::map{ // book dir object {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, createOwnerDirLedgerObject( {ripple::uint256{kIndex2}, ripple::uint256{kIndex2}}, kIndex1 ) .getSerializer() .peekData()}, // gets issuer account object {ripple::keylet::account(account).key, createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2, kTransferRateX2) .getSerializer() .peekData()}, // trust line between gets issuer and owner,owner has 30 USD {ripple::keylet::line(account2, account, ripple::to_currency("USD")).key, trustline30Balance.getSerializer().peekData()}, }, .ledgerObjectCalls = 6, .mockedOffers = std::vector{ // After offer1, balance is 30 - 2*10 = 10 gets10USDPays20XRPOffer, // offer2 not fully funded, balance is 10, rate is 2, so only // gets 5 gets10USDPays20XRPOffer }, .expectedJson = fmt::format( R"JSON({{ "ledger_hash": "{}", "ledger_index": 300, "offers": [ {{ "Account": "{}", "BookDirectory": "{}", "BookNode": "0", "Flags": 0, "LedgerEntryType": "Offer", "OwnerNode": "0", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "Sequence": 0, "TakerPays": "20", "TakerGets": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "10" }}, "index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", "owner_funds": "{}", "quality": "{}" }}, {{ "Account": "{}", "BookDirectory": "{}", "BookNode": "0", "Flags": 0, "LedgerEntryType": "Offer", "OwnerNode": "0", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "Sequence": 0, "TakerPays": "20", "TakerGets": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "10" }}, "index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", "taker_gets_funded": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "5" }}, "taker_pays_funded": "10", "quality": "{}" }} ] }})JSON", kLedgerHash, kAccount2, kPayS20XrpGetS10UsdBookDir, 30, 2, kAccount2, kPayS20XrpGetS10UsdBookDir, 2 ), .amendmentIsEnabledCalls = 1, }, BookOffersNormalTestBundle{ .testName = "PaysXRPGetsUSDSellingOwnCurrency", .inputJson = paysXRPGetsUSDInputJson, // prepare offer dir index .mockedSuccessors = std::map>{ {getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}}, {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional{}} }, .mockedLedgerObjects = std::map{ // book dir object {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1) .getSerializer() .peekData()}, // gets issuer account object, rate is 1/2 {ripple::keylet::account(account).key, createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2, kTransferRateX2) .getSerializer() .peekData()}, }, .ledgerObjectCalls = 3, .mockedOffers = std::vector{gets10USDPays20XRPOwnerOffer}, .expectedJson = fmt::format( R"JSON({{ "ledger_hash": "{}", "ledger_index": 300, "offers": [ {{ "Account": "{}", "BookDirectory": "{}", "BookNode": "0", "Flags": 0, "LedgerEntryType": "Offer", "OwnerNode": "0", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "Sequence": 0, "TakerPays": "20", "TakerGets": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "10" }}, "index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", "owner_funds": "{}", "quality": "{}" }} ] }})JSON", kLedgerHash, kAccount, kPayS20XrpGetS10UsdBookDir, 10, 2 ) }, BookOffersNormalTestBundle{ .testName = "PaysXRPGetsUSDTrustLineFrozen", .inputJson = paysXRPGetsUSDInputJson, // prepare offer dir index .mockedSuccessors = std::map>{ {getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}}, {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional{}} }, .mockedLedgerObjects = std::map{ // book dir object {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1) .getSerializer() .peekData()}, // gets issuer account object, rate is 1/2 {ripple::keylet::account(account).key, createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2, kTransferRateX2) .getSerializer() .peekData()}, // trust line between gets issuer and owner,owner has 8 USD {ripple::keylet::line(account2, account, ripple::to_currency("USD")).key, frozenTrustLine.getSerializer().peekData()}, }, .ledgerObjectCalls = 6, .mockedOffers = std::vector{gets10USDPays20XRPOffer}, .expectedJson = fmt::format( R"JSON({{ "ledger_hash": "{}", "ledger_index": 300, "offers": [ {{ "Account": "{}", "BookDirectory": "{}", "BookNode": "0", "Flags": 0, "LedgerEntryType": "Offer", "OwnerNode": "0", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "Sequence": 0, "TakerPays": "20", "TakerGets": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "10" }}, "index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", "owner_funds": "{}", "quality": "{}", "taker_gets_funded": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "0" }}, "taker_pays_funded": "0" }} ] }})JSON", kLedgerHash, kAccount2, kPayS20XrpGetS10UsdBookDir, 0, 2 ), }, BookOffersNormalTestBundle{ .testName = "PaysXRPGetsUSDIsDeepFrozen", .inputJson = paysXRPGetsUSDInputJson, // prepare offer dir index .mockedSuccessors = std::map>{ {getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}}, {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional{}} }, .mockedLedgerObjects = std::map{ // book dir object {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1) .getSerializer() .peekData()}, // gets issuer account object, is deep frozen so unfunded {ripple::keylet::account(account).key, createAccountRootObject( kAccount, ripple::lsfLowDeepFreeze, 2, 200, 2, kIndex1, 2, kTransferRateX2 ) .getSerializer() .peekData()}, }, .ledgerObjectCalls = 4, .mockedOffers = std::vector{gets10USDPays20XRPOffer}, .expectedJson = fmt::format( R"JSON({{ "ledger_hash": "{}", "ledger_index": 300, "offers": [ {{ "Account": "{}", "BookDirectory": "{}", "BookNode": "0", "Flags": 0, "LedgerEntryType": "Offer", "OwnerNode": "0", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "Sequence": 0, "TakerPays": "20", "TakerGets": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "10" }}, "index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", "owner_funds": "{}", "quality": "{}", "taker_gets_funded": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "0" }}, "taker_pays_funded": "0" }} ] }})JSON", kLedgerHash, kAccount2, kPayS20XrpGetS10UsdBookDir, 0, 2 ) }, BookOffersNormalTestBundle{ .testName = "PaysXRPGetsUSDTrustLineFrozenAndIsDeepFrozen", .inputJson = paysXRPGetsUSDInputJson, // prepare offer dir index .mockedSuccessors = std::map>{ {getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}}, {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional{}} }, .mockedLedgerObjects = std::map{ // book dir object {ripple::uint256{kPayS20XrpGetS10UsdBookDir}, createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1) .getSerializer() .peekData()}, // gets issuer account object, is deep frozen so unfunded {ripple::keylet::account(account).key, createAccountRootObject( kAccount, ripple::lsfLowDeepFreeze, 2, 200, 2, kIndex1, 2, kTransferRateX2 ) .getSerializer() .peekData()}, {ripple::keylet::line(account2, account, ripple::to_currency("USD")).key, frozenTrustLine.getSerializer().peekData()}, }, .ledgerObjectCalls = 6, .mockedOffers = std::vector{gets10USDPays20XRPOffer}, .expectedJson = fmt::format( R"JSON({{ "ledger_hash": "{}", "ledger_index": 300, "offers": [ {{ "Account": "{}", "BookDirectory": "{}", "BookNode": "0", "Flags": 0, "LedgerEntryType": "Offer", "OwnerNode": "0", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "Sequence": 0, "TakerPays": "20", "TakerGets": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "10" }}, "index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", "owner_funds": "{}", "quality": "{}", "taker_gets_funded": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "0" }}, "taker_pays_funded": "0" }} ] }})JSON", kLedgerHash, kAccount2, kPayS20XrpGetS10UsdBookDir, 0, 2 ) } }; } INSTANTIATE_TEST_SUITE_P( RPCBookOffersHandler, RPCBookOffersNormalPathTest, testing::ValuesIn(generateNormalPathBookOffersTestBundles()), tests::util::kNameGenerator ); // ledger not exist TEST_F(RPCBookOffersHandlerTest, LedgerNonExistViaIntSequence) { EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); // return empty ledgerHeader ON_CALL(*backend_, fetchLedgerBySequence(30, _)) .WillByDefault(Return(std::optional{})); static auto const kInput = json::parse( fmt::format( R"JSON({{ "ledger_index": 30, "taker_gets": {{ "currency": "XRP" }}, "taker_pays": {{ "currency": "USD", "issuer": "{}" }} }})JSON", kAccount ) ); auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}}; runSpawn([&](boost::asio::yield_context yield) { auto const output = handler.process(kInput, Context{.yield = yield}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); }); } TEST_F(RPCBookOffersHandlerTest, LedgerNonExistViaSequence) { EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); // return empty ledgerHeader ON_CALL(*backend_, fetchLedgerBySequence(30, _)) .WillByDefault(Return(std::optional{})); static auto const kInput = json::parse( fmt::format( R"JSON({{ "ledger_index": "30", "taker_gets": {{ "currency": "XRP" }}, "taker_pays": {{ "currency": "USD", "issuer": "{}" }} }})JSON", kAccount ) ); auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}}; runSpawn([&](boost::asio::yield_context yield) { auto const output = handler.process(kInput, Context{.yield = yield}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); }); } TEST_F(RPCBookOffersHandlerTest, LedgerNonExistViaHash) { EXPECT_CALL(*backend_, fetchLedgerByHash).Times(1); // return empty ledgerHeader ON_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLedgerHash}, _)) .WillByDefault(Return(std::optional{})); static auto const kInput = json::parse( fmt::format( R"JSON({{ "ledger_hash": "{}", "taker_gets": {{ "currency": "XRP" }}, "taker_pays": {{ "currency": "USD", "issuer": "{}" }} }})JSON", kLedgerHash, kAccount ) ); auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}}; runSpawn([&](boost::asio::yield_context yield) { auto const output = handler.process(kInput, Context{.yield = yield}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); }); } TEST_F(RPCBookOffersHandlerTest, Limit) { auto const seq = 300; EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); // return valid ledgerHeader auto const ledgerHeader = createLedgerHeader(kLedgerHash, seq); ON_CALL(*backend_, fetchLedgerBySequence(seq, _)).WillByDefault(Return(ledgerHeader)); auto const issuer = getAccountIdWithString(kAccount); // return valid book dir EXPECT_CALL(*backend_, doFetchSuccessorKey).Times(1); auto const getsXRPPaysUSDBook = getBookBase( rpc::parseBook( ripple::to_currency("USD"), issuer, ripple::xrpCurrency(), ripple::xrpAccount(), std::nullopt ) .value() ); ON_CALL(*backend_, doFetchSuccessorKey(getsXRPPaysUSDBook, seq, _)) .WillByDefault(Return(ripple::uint256{kPayS20UsdGetS10XrpBookDir})); EXPECT_CALL(*backend_, doFetchLedgerObject).Times(5); auto const indexes = std::vector(10, ripple::uint256{kIndex2}); ON_CALL(*backend_, doFetchLedgerObject(ripple::uint256{kPayS20UsdGetS10XrpBookDir}, seq, _)) .WillByDefault( Return(createOwnerDirLedgerObject(indexes, kIndex1).getSerializer().peekData()) ); ON_CALL( *backend_, doFetchLedgerObject(ripple::keylet::account(getAccountIdWithString(kAccount2)).key, seq, _) ) .WillByDefault(Return( createAccountRootObject(kAccount2, 0, 2, 200, 2, kIndex1, 2).getSerializer().peekData() )); ON_CALL(*backend_, doFetchLedgerObject(ripple::keylet::fees().key, seq, _)) .WillByDefault(Return(createLegacyFeeSettingBlob(1, 2, 3, 4, 0))); ON_CALL(*backend_, doFetchLedgerObject(ripple::keylet::account(issuer).key, seq, _)) .WillByDefault( Return(createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2, kTransferRateX2) .getSerializer() .peekData()) ); auto const gets10XRPPays20USDOffer = createOfferLedgerObject( kAccount2, 10, 20, ripple::to_string(ripple::xrpCurrency()), ripple::to_string(ripple::to_currency("USD")), toBase58(ripple::xrpAccount()), kAccount, kPayS20UsdGetS10XrpBookDir ); std::vector const bbs(10, gets10XRPPays20USDOffer.getSerializer().peekData()); ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs)); EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1); static auto const kInput = json::parse( fmt::format( R"JSON({{ "taker_gets": {{ "currency": "XRP" }}, "taker_pays": {{ "currency": "USD", "issuer": "{}" }}, "limit": 5 }})JSON", kAccount ) ); auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}}; runSpawn([&](boost::asio::yield_context yield) { auto const output = handler.process(kInput, Context{.yield = yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result.value().as_object().at("offers").as_array().size(), 5); }); } TEST_F(RPCBookOffersHandlerTest, LimitMoreThanMax) { auto const seq = 300; EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); // return valid ledgerHeader auto const ledgerHeader = createLedgerHeader(kLedgerHash, seq); ON_CALL(*backend_, fetchLedgerBySequence(seq, _)).WillByDefault(Return(ledgerHeader)); auto const issuer = getAccountIdWithString(kAccount); // return valid book dir EXPECT_CALL(*backend_, doFetchSuccessorKey).Times(1); auto const getsXRPPaysUSDBook = getBookBase( rpc::parseBook( ripple::to_currency("USD"), issuer, ripple::xrpCurrency(), ripple::xrpAccount(), std::nullopt ) .value() ); ON_CALL(*backend_, doFetchSuccessorKey(getsXRPPaysUSDBook, seq, _)) .WillByDefault(Return(ripple::uint256{kPayS20UsdGetS10XrpBookDir})); EXPECT_CALL(*backend_, doFetchLedgerObject).Times(5); auto const indexes = std::vector(BookOffersHandler::kLimitMax + 1, ripple::uint256{kIndex2}); ON_CALL(*backend_, doFetchLedgerObject(ripple::uint256{kPayS20UsdGetS10XrpBookDir}, seq, _)) .WillByDefault( Return(createOwnerDirLedgerObject(indexes, kIndex1).getSerializer().peekData()) ); ON_CALL( *backend_, doFetchLedgerObject(ripple::keylet::account(getAccountIdWithString(kAccount2)).key, seq, _) ) .WillByDefault(Return( createAccountRootObject(kAccount2, 0, 2, 200, 2, kIndex1, 2).getSerializer().peekData() )); ON_CALL(*backend_, doFetchLedgerObject(ripple::keylet::fees().key, seq, _)) .WillByDefault(Return(createLegacyFeeSettingBlob(1, 2, 3, 4, 0))); ON_CALL(*backend_, doFetchLedgerObject(ripple::keylet::account(issuer).key, seq, _)) .WillByDefault( Return(createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2, kTransferRateX2) .getSerializer() .peekData()) ); auto const gets10XRPPays20USDOffer = createOfferLedgerObject( kAccount2, 10, 20, ripple::to_string(ripple::xrpCurrency()), ripple::to_string(ripple::to_currency("USD")), toBase58(ripple::xrpAccount()), kAccount, kPayS20UsdGetS10XrpBookDir ); std::vector const bbs( BookOffersHandler::kLimitMax + 1, gets10XRPPays20USDOffer.getSerializer().peekData() ); ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs)); EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1); static auto const kInput = json::parse( fmt::format( R"JSON({{ "taker_gets": {{ "currency": "XRP" }}, "taker_pays": {{ "currency": "USD", "issuer": "{}" }}, "limit": {} }})JSON", kAccount, BookOffersHandler::kLimitMax + 1 ) ); auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}}; runSpawn([&](boost::asio::yield_context yield) { auto const output = handler.process(kInput, Context{.yield = yield}); ASSERT_TRUE(output); EXPECT_EQ( output.result.value().as_object().at("offers").as_array().size(), BookOffersHandler::kLimitMax ); }); }