#include "data/Types.hpp" #include "rpc/Errors.hpp" #include "rpc/common/AnyHandler.hpp" #include "rpc/common/Types.hpp" #include "rpc/handlers/AccountObjects.hpp" #include "util/HandlerBaseTestFixture.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 using namespace rpc; using namespace data; namespace json = boost::json; using namespace testing; namespace { constexpr auto kAccount = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; constexpr auto kIssuer = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"; constexpr auto kAccount2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; constexpr auto kLedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; constexpr auto kIndex1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; constexpr auto kTxnId = "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"; constexpr auto kTokenId = "000827103B94ECBB7BF0A0A6ED62B3607801A27B65F4679F4AD1D4850000C0EA"; constexpr auto kMaxSeq = 30; constexpr auto kMinSeq = 10; } // namespace struct RPCAccountObjectsHandlerTest : HandlerBaseTest { RPCAccountObjectsHandlerTest() { backend_->setRange(kMinSeq, kMaxSeq); } }; struct AccountObjectsParamTestCaseBundle { std::string testName; std::string testJson; std::string expectedError; std::string expectedErrorMessage; }; // parameterized test cases for parameters check struct AccountObjectsParameterTest : public RPCAccountObjectsHandlerTest, public WithParamInterface { }; static auto generateTestValuesForParametersTest() { return std::vector{ AccountObjectsParamTestCaseBundle{ .testName = "MissingAccount", .testJson = R"JSON({})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Required field 'account' missing" }, AccountObjectsParamTestCaseBundle{ .testName = "AccountNotString", .testJson = R"JSON({"account": 1})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "accountNotString" }, AccountObjectsParamTestCaseBundle{ .testName = "AccountInvalid", .testJson = R"JSON({"account": "xxx"})JSON", .expectedError = "actMalformed", .expectedErrorMessage = "accountMalformed" }, AccountObjectsParamTestCaseBundle{ .testName = "TypeNotString", .testJson = R"JSON({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "type": 1})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid field 'type', not string." }, AccountObjectsParamTestCaseBundle{ .testName = "TypeInvalid", .testJson = R"JSON({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "type": "wrong"})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid field 'type'." }, AccountObjectsParamTestCaseBundle{ .testName = "TypeNotAccountOwned", .testJson = R"JSON({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "type": "amendments"})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid field 'type'." }, AccountObjectsParamTestCaseBundle{ .testName = "LedgerHashInvalid", .testJson = R"JSON({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "ledger_hash": "1"})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "ledger_hashMalformed" }, AccountObjectsParamTestCaseBundle{ .testName = "LedgerHashNotString", .testJson = R"JSON({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "ledger_hash": 1})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "ledger_hashNotString" }, AccountObjectsParamTestCaseBundle{ .testName = "LedgerIndexInvalid", .testJson = R"JSON({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "ledger_index": "a"})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "ledgerIndexMalformed" }, AccountObjectsParamTestCaseBundle{ .testName = "LimitNotInt", .testJson = R"JSON({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "limit": "1"})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, AccountObjectsParamTestCaseBundle{ .testName = "LimitNegative", .testJson = R"JSON({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "limit":-1})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, AccountObjectsParamTestCaseBundle{ .testName = "LimitZero", .testJson = R"JSON({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "limit": 0})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, AccountObjectsParamTestCaseBundle{ .testName = "MarkerNotString", .testJson = R"JSON({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "marker": 9})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "markerNotString" }, AccountObjectsParamTestCaseBundle{ .testName = "MarkerInvalid", .testJson = R"JSON({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "marker": "xxxx"})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Malformed cursor." }, AccountObjectsParamTestCaseBundle{ .testName = "NFTMarkerInvalid", .testJson = fmt::format( R"JSON({{"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "marker": "wronghex256,{}"}})JSON", std::numeric_limits::max() ), .expectedError = "invalidParams", .expectedErrorMessage = "Malformed cursor." }, AccountObjectsParamTestCaseBundle{ .testName = "DeletionBlockersOnlyInvalidString", .testJson = R"JSON({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "deletion_blockers_only": "wrong"})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, AccountObjectsParamTestCaseBundle{ .testName = "DeletionBlockersOnlyInvalidNull", .testJson = R"JSON({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "deletion_blockers_only": null})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, }; } INSTANTIATE_TEST_CASE_P( RPCAccountObjectsGroup1, AccountObjectsParameterTest, ValuesIn(generateTestValuesForParametersTest()), tests::util::kNameGenerator ); TEST_P(AccountObjectsParameterTest, InvalidParams) { auto const testBundle = GetParam(); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; auto const req = json::parse(testBundle.testJson); auto const output = handler.process(req, Context{yield}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), testBundle.expectedError); EXPECT_EQ(err.at("error_message").as_string(), testBundle.expectedErrorMessage); }); } TEST_F(RPCAccountObjectsHandlerTest, LedgerNonExistViaIntSequence) { // return empty ledgerHeader EXPECT_CALL(*backend_, fetchLedgerBySequence(kMaxSeq, _)) .WillOnce(Return(std::optional{})); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "ledger_index": 30 }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{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(RPCAccountObjectsHandlerTest, LedgerNonExistViaStringSequence) { // return empty ledgerHeader EXPECT_CALL(*backend_, fetchLedgerBySequence(kMaxSeq, _)).WillOnce(Return(std::nullopt)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "ledger_index": "30" }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{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(RPCAccountObjectsHandlerTest, LedgerNonExistViaHash) { // return empty ledgerHeader EXPECT_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLedgerHash}, _)) .WillOnce(Return(std::optional{})); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "ledger_hash": "{}" }})JSON", kAccount, kLedgerHash ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{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(RPCAccountObjectsHandlerTest, AccountNotExist) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(std::optional{})); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}" }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), "actNotFound"); EXPECT_EQ(err.at("error_message").as_string(), "Account not found."); }); } TEST_F(RPCAccountObjectsHandlerTest, DefaultParameterNoNFTFound) { static constexpr auto kExpectedOut = R"JSON({ "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index": 30, "validated": true, "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "limit": 200, "account_objects": [ { "Balance": { "currency": "USD", "issuer": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", "value": "100" }, "Flags": 0, "HighLimit": { "currency": "USD", "issuer": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "value": "20" }, "LedgerEntryType": "RippleState", "LowLimit": { "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "10" }, "PreviousTxnID": "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879", "PreviousTxnLgrSeq": 123, "index": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC" } ] })JSON"; auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); std::vector bbs; auto const line1 = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); bbs.push_back(line1.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}" }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(*output.result, json::parse(kExpectedOut)); }); } TEST_F(RPCAccountObjectsHandlerTest, Limit) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); static constexpr auto kLimit = 10; auto count = kLimit * 2; // put 20 items in owner dir, but only return 10 auto const ownerDir = createOwnerDirLedgerObject(std::vector(count, ripple::uint256{kIndex1}), kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); std::vector bbs; while (count-- != 0) { auto const line1 = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); bbs.push_back(line1.getSerializer().peekData()); } EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "limit": {} }})JSON", kAccount, kLimit ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->as_object().at("account_objects").as_array().size(), kLimit); EXPECT_EQ( output.result->as_object().at("marker").as_string(), fmt::format("{},{}", kIndex1, 0) ); }); } TEST_F(RPCAccountObjectsHandlerTest, Marker) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const accountKk = ripple::keylet::account(getAccountIdWithString(kAccount)).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); static constexpr auto kLimit = 20; static constexpr auto kPage = 2; auto count = kLimit; auto const ownerDir = createOwnerDirLedgerObject(std::vector(count, ripple::uint256{kIndex1}), kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(getAccountIdWithString(kAccount)).key; auto const hintIndex = ripple::keylet::page(ownerDirKk, kPage).key; EXPECT_CALL(*backend_, doFetchLedgerObject(hintIndex, 30, _)) .Times(2) .WillRepeatedly(Return(ownerDir.getSerializer().peekData())); std::vector bbs; while (count-- != 0) { auto const line1 = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); bbs.push_back(line1.getSerializer().peekData()); } EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "marker": "{},{}" }})JSON", kAccount, kIndex1, kPage ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->as_object().at("account_objects").as_array().size(), kLimit - 1); EXPECT_FALSE(output.result->as_object().contains("marker")); }); } TEST_F(RPCAccountObjectsHandlerTest, MultipleDirNoNFT) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); static constexpr auto kCount = 10; static constexpr auto kNextpage = 1; auto cc = kCount; auto ownerDir = createOwnerDirLedgerObject(std::vector(cc, ripple::uint256{kIndex1}), kIndex1); // set next page ownerDir.setFieldU64(ripple::sfIndexNext, kNextpage); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; auto const page1 = ripple::keylet::page(ownerDirKk, kNextpage).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); EXPECT_CALL(*backend_, doFetchLedgerObject(page1, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); std::vector bbs; // 10 items per page, 2 pages cc = kCount * 2; while (cc-- != 0) { auto const line1 = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); bbs.push_back(line1.getSerializer().peekData()); } EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "limit": {} }})JSON", kAccount, 2 * kCount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->as_object().at("account_objects").as_array().size(), kCount * 2); EXPECT_EQ( output.result->as_object().at("marker").as_string(), fmt::format("{},{}", kIndex1, kNextpage) ); }); } TEST_F(RPCAccountObjectsHandlerTest, TypeFilter) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}, ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); std::vector bbs; // put 1 state and 1 offer auto const line1 = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); auto const offer = createOfferLedgerObject( kAccount, 10, 20, ripple::to_string(ripple::to_currency("USD")), ripple::to_string(ripple::xrpCurrency()), kAccount2, toBase58(ripple::xrpAccount()), kIndex1 ); bbs.push_back(line1.getSerializer().peekData()); bbs.push_back(offer.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "type": "offer" }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->as_object().at("account_objects").as_array().size(), 1); }); } TEST_F(RPCAccountObjectsHandlerTest, TypeFilterAmmType) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}, ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); std::vector bbs; // put 1 state and 1 amm auto const line1 = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); bbs.push_back(line1.getSerializer().peekData()); auto const ammObject = createAmmObject(kAccount, "XRP", toBase58(ripple::xrpAccount()), "JPY", kAccount2); bbs.push_back(ammObject.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "type": "amm" }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); auto const& accountObjects = output.result->as_object().at("account_objects").as_array(); ASSERT_EQ(accountObjects.size(), 1); EXPECT_EQ(accountObjects.front().at("LedgerEntryType").as_string(), "AMM"); }); } TEST_F(RPCAccountObjectsHandlerTest, TypeFilterReturnEmpty) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}, ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); std::vector bbs; auto const line1 = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); auto const offer = createOfferLedgerObject( kAccount, 10, 20, ripple::to_string(ripple::to_currency("USD")), ripple::to_string(ripple::xrpCurrency()), kAccount2, toBase58(ripple::xrpAccount()), kIndex1 ); bbs.push_back(line1.getSerializer().peekData()); bbs.push_back(offer.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "type": "check" }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->as_object().at("account_objects").as_array().size(), 0); }); } TEST_F(RPCAccountObjectsHandlerTest, DeletionBlockersOnlyFilter) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}, ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); auto const line = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); auto const channel = createPaymentChannelLedgerObject(kAccount, kAccount2, 100, 10, 32, kTxnId, 28); auto const offer = createOfferLedgerObject( kAccount, 10, 20, ripple::to_string(ripple::to_currency("USD")), ripple::to_string(ripple::xrpCurrency()), kAccount2, toBase58(ripple::xrpAccount()), kIndex1 ); std::vector bbs; bbs.push_back(line.getSerializer().peekData()); bbs.push_back(channel.getSerializer().peekData()); bbs.push_back(offer.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "deletion_blockers_only": true }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->as_object().at("account_objects").as_array().size(), 2); }); } TEST_F(RPCAccountObjectsHandlerTest, DeletionBlockersOnlyFilterWithTypeFilter) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}, ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); auto const line = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); auto const channel = createPaymentChannelLedgerObject(kAccount, kAccount2, 100, 10, 32, kTxnId, 28); std::vector bbs; bbs.push_back(line.getSerializer().peekData()); bbs.push_back(channel.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "deletion_blockers_only": true, "type": "payment_channel" }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->as_object().at("account_objects").as_array().size(), 1); }); } TEST_F(RPCAccountObjectsHandlerTest, DeletionBlockersOnlyFilterEmptyResult) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}, ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); auto const offer1 = createOfferLedgerObject( kAccount, 10, 20, ripple::to_string(ripple::to_currency("USD")), ripple::to_string(ripple::xrpCurrency()), kAccount2, toBase58(ripple::xrpAccount()), kIndex1 ); auto const offer2 = createOfferLedgerObject( kAccount, 20, 30, ripple::to_string(ripple::to_currency("USD")), ripple::to_string(ripple::xrpCurrency()), kAccount2, toBase58(ripple::xrpAccount()), kIndex1 ); std::vector bbs; bbs.push_back(offer1.getSerializer().peekData()); bbs.push_back(offer2.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "deletion_blockers_only": true }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->as_object().at("account_objects").as_array().size(), 0); }); } TEST_F( RPCAccountObjectsHandlerTest, DeletionBlockersOnlyFilterWithIncompatibleTypeYieldsEmptyResult ) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}, ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); auto const offer1 = createOfferLedgerObject( kAccount, 10, 20, ripple::to_string(ripple::to_currency("USD")), ripple::to_string(ripple::xrpCurrency()), kAccount2, toBase58(ripple::xrpAccount()), kIndex1 ); auto const offer2 = createOfferLedgerObject( kAccount, 20, 30, ripple::to_string(ripple::to_currency("USD")), ripple::to_string(ripple::xrpCurrency()), kAccount2, toBase58(ripple::xrpAccount()), kIndex1 ); std::vector bbs; bbs.push_back(offer1.getSerializer().peekData()); bbs.push_back(offer2.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "deletion_blockers_only": true, "type": "offer" }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->as_object().at("account_objects").as_array().size(), 0); }); } TEST_F(RPCAccountObjectsHandlerTest, NFTMixOtherObjects) { static constexpr auto kExpectedOut = R"JSON({ "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index": 30, "validated": true, "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "limit": 200, "account_objects": [ { "Flags": 0, "LedgerEntryType": "NFTokenPage", "NFTokens": [ { "NFToken": { "NFTokenID": "000827103B94ECBB7BF0A0A6ED62B3607801A27B65F4679F4AD1D4850000C0EA", "URI": "7777772E6F6B2E636F6D" } } ], "PreviousPageMin": "4B4E9C06F24296074F7BC48F92A97916C6DC5EA9659B25014D08E1BC983515BC", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "index": "4B4E9C06F24296074F7BC48F92A97916C6DC5EA9FFFFFFFFFFFFFFFFFFFFFFFF" }, { "Flags": 0, "LedgerEntryType": "NFTokenPage", "NFTokens": [ { "NFToken": { "NFTokenID": "000827103B94ECBB7BF0A0A6ED62B3607801A27B65F4679F4AD1D4850000C0EA", "URI": "7777772E6F6B2E636F6D" } } ], "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "index": "4B4E9C06F24296074F7BC48F92A97916C6DC5EA9659B25014D08E1BC983515BC" }, { "Balance": { "currency": "USD", "issuer": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", "value": "100" }, "Flags": 0, "HighLimit": { "currency": "USD", "issuer": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "value": "20" }, "LedgerEntryType": "RippleState", "LowLimit": { "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "10" }, "PreviousTxnID": "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879", "PreviousTxnLgrSeq": 123, "index": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC" } ] })JSON"; auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft page 1 auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; auto const nftPage2KK = ripple::keylet::nftpage(ripple::keylet::nftpage_min(account), ripple::uint256{kIndex1}).key; auto const nftpage1 = createNftTokenPage( std::vector{std::make_pair(kTokenId, "www.ok.com")}, nftPage2KK ); EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)) .WillOnce(Return(nftpage1.getSerializer().peekData())); // nft page 2 , end auto const nftpage2 = createNftTokenPage( std::vector{std::make_pair(kTokenId, "www.ok.com")}, std::nullopt ); EXPECT_CALL(*backend_, doFetchLedgerObject(nftPage2KK, 30, _)) .WillOnce(Return(nftpage2.getSerializer().peekData())); std::vector bbs; auto const line1 = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); bbs.push_back(line1.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}" }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(*output.result, json::parse(kExpectedOut)); }); } TEST_F(RPCAccountObjectsHandlerTest, NFTReachLimitReturnMarker) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto current = ripple::keylet::nftpage_max(account).key; std::string first{kIndex1}; std::ranges::sort(first); for (auto i = 0; i < 10; i++) { std::ranges::next_permutation(first); auto previous = ripple::keylet::nftpage( ripple::keylet::nftpage_min(account), ripple::uint256{first.c_str()} ) .key; auto const nftpage = createNftTokenPage( std::vector{std::make_pair(kTokenId, "www.ok.com")}, previous ); EXPECT_CALL(*backend_, doFetchLedgerObject(current, 30, _)) .WillOnce(Return(nftpage.getSerializer().peekData())); current = previous; } static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "limit": {} }})JSON", kAccount, 10 ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result.value().as_object().at("account_objects").as_array().size(), 10); EXPECT_EQ( output.result.value().as_object().at("marker").as_string(), fmt::format("{},{}", ripple::strHex(current), std::numeric_limits::max()) ); }); } TEST_F(RPCAccountObjectsHandlerTest, NFTReachLimitNoMarker) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto current = ripple::keylet::nftpage_max(account).key; std::string first{kIndex1}; std::ranges::sort(first); for (auto i = 0; i < 10; i++) { std::ranges::next_permutation(first); auto previous = ripple::keylet::nftpage( ripple::keylet::nftpage_min(account), ripple::uint256{first.c_str()} ) .key; auto const nftpage = createNftTokenPage( std::vector{std::make_pair(kTokenId, "www.ok.com")}, previous ); EXPECT_CALL(*backend_, doFetchLedgerObject(current, 30, _)) .WillOnce(Return(nftpage.getSerializer().peekData())); current = previous; } auto const nftpage11 = createNftTokenPage( std::vector{std::make_pair(kTokenId, "www.ok.com")}, std::nullopt ); EXPECT_CALL(*backend_, doFetchLedgerObject(current, 30, _)) .WillOnce(Return(nftpage11.getSerializer().peekData())); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "limit": {} }})JSON", kAccount, 11 ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result.value().as_object().at("account_objects").as_array().size(), 11); //"0000000000000000000000000000000000000000000000000000000000000000,4294967295" EXPECT_EQ( output.result.value().as_object().at("marker").as_string(), fmt::format( "{},{}", ripple::strHex(ripple::uint256(beast::zero)), std::numeric_limits::max() ) ); }); } TEST_F(RPCAccountObjectsHandlerTest, NFTMarker) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); std::string first{kIndex1}; auto current = ripple::keylet::nftpage( ripple::keylet::nftpage_min(account), ripple::uint256{first.c_str()} ) .key; auto const marker = current; std::ranges::sort(first); for (auto i = 0; i < 10; i++) { std::ranges::next_permutation(first); auto previous = ripple::keylet::nftpage( ripple::keylet::nftpage_min(account), ripple::uint256{first.c_str()} ) .key; auto const nftpage = createNftTokenPage( std::vector{std::make_pair(kTokenId, "www.ok.com")}, previous ); EXPECT_CALL(*backend_, doFetchLedgerObject(current, 30, _)) .WillOnce(Return(nftpage.getSerializer().peekData())); current = previous; } auto const nftpage11 = createNftTokenPage( std::vector{std::make_pair(kTokenId, "www.ok.com")}, std::nullopt ); EXPECT_CALL(*backend_, doFetchLedgerObject(current, 30, _)) .WillOnce(Return(nftpage11.getSerializer().peekData())); auto const ownerDir = createOwnerDirLedgerObject( {ripple::uint256{kIndex1}, ripple::uint256{kIndex1}, ripple::uint256{kIndex1}}, kIndex1 ); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); auto const line = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); auto const channel = createPaymentChannelLedgerObject(kAccount, kAccount2, 100, 10, 32, kTxnId, 28); auto const offer = createOfferLedgerObject( kAccount, 10, 20, ripple::to_string(ripple::to_currency("USD")), ripple::to_string(ripple::xrpCurrency()), kAccount2, toBase58(ripple::xrpAccount()), kIndex1 ); std::vector bbs; bbs.push_back(line.getSerializer().peekData()); bbs.push_back(channel.getSerializer().peekData()); bbs.push_back(offer.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "marker": "{},{}" }})JSON", kAccount, ripple::strHex(marker), std::numeric_limits::max() ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ( output.result.value().as_object().at("account_objects").as_array().size(), 11 + 3 ); EXPECT_FALSE(output.result.value().as_object().contains("marker")); }); } // when limit reached, happen to be the end of NFT page list TEST_F(RPCAccountObjectsHandlerTest, NFTMarkerNoMoreNFT) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject( {ripple::uint256{kIndex1}, ripple::uint256{kIndex1}, ripple::uint256{kIndex1}}, kIndex1 ); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); auto const line = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); auto const channel = createPaymentChannelLedgerObject(kAccount, kAccount2, 100, 10, 32, kTxnId, 28); auto const offer = createOfferLedgerObject( kAccount, 10, 20, ripple::to_string(ripple::to_currency("USD")), ripple::to_string(ripple::xrpCurrency()), kAccount2, toBase58(ripple::xrpAccount()), kIndex1 ); std::vector bbs; bbs.push_back(line.getSerializer().peekData()); bbs.push_back(channel.getSerializer().peekData()); bbs.push_back(offer.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "marker": "{},{}" }})JSON", kAccount, ripple::strHex(ripple::uint256{beast::zero}), std::numeric_limits::max() ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result.value().as_object().at("account_objects").as_array().size(), 3); EXPECT_FALSE(output.result.value().as_object().contains("marker")); }); } TEST_F(RPCAccountObjectsHandlerTest, NFTMarkerNotInRange) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "marker": "{},{}" }})JSON", kAccount, kIndex1, std::numeric_limits::max() ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), "invalidParams"); EXPECT_EQ(err.at("error_message").as_string(), "Invalid marker."); }); } TEST_F(RPCAccountObjectsHandlerTest, NFTMarkerNotExist) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); // return null for this marker auto const accountNftMax = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountNftMax, kMaxSeq, _)) .WillOnce(Return(std::nullopt)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "marker": "{},{}" }})JSON", kAccount, ripple::strHex(accountNftMax), std::numeric_limits::max() ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), "invalidParams"); EXPECT_EQ(err.at("error_message").as_string(), "Invalid marker."); }); } TEST_F(RPCAccountObjectsHandlerTest, NFTLimitAdjust) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); std::string first{kIndex1}; auto current = ripple::keylet::nftpage( ripple::keylet::nftpage_min(account), ripple::uint256{first.c_str()} ) .key; auto const marker = current; std::ranges::sort(first); for (auto i = 0; i < 10; i++) { std::ranges::next_permutation(first); auto previous = ripple::keylet::nftpage( ripple::keylet::nftpage_min(account), ripple::uint256{first.c_str()} ) .key; auto const nftpage = createNftTokenPage( std::vector{std::make_pair(kTokenId, "www.ok.com")}, previous ); EXPECT_CALL(*backend_, doFetchLedgerObject(current, 30, _)) .WillOnce(Return(nftpage.getSerializer().peekData())); current = previous; } auto const nftpage11 = createNftTokenPage( std::vector{std::make_pair(kTokenId, "www.ok.com")}, std::nullopt ); EXPECT_CALL(*backend_, doFetchLedgerObject(current, 30, _)) .WillOnce(Return(nftpage11.getSerializer().peekData())); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}, ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); auto const line = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); auto const channel = createPaymentChannelLedgerObject(kAccount, kAccount2, 100, 10, 32, kTxnId, 28); auto const offer = createOfferLedgerObject( kAccount, 10, 20, ripple::to_string(ripple::to_currency("USD")), ripple::to_string(ripple::xrpCurrency()), kAccount2, toBase58(ripple::xrpAccount()), kIndex1 ); std::vector bbs; bbs.push_back(line.getSerializer().peekData()); bbs.push_back(channel.getSerializer().peekData()); bbs.push_back(offer.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "marker": "{},{}", "limit": 12 }})JSON", kAccount, ripple::strHex(marker), std::numeric_limits::max() ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result.value().as_object().at("account_objects").as_array().size(), 12); // marker not in NFT "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC,0" EXPECT_EQ( output.result.value().as_object().at("marker").as_string(), fmt::format("{},{}", kIndex1, 0) ); }); } TEST_F(RPCAccountObjectsHandlerTest, FilterNFT) { static constexpr auto kExpectedOut = R"JSON({ "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index": 30, "validated": true, "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "limit": 200, "account_objects": [ { "Flags": 0, "LedgerEntryType": "NFTokenPage", "NFTokens": [ { "NFToken": { "NFTokenID": "000827103B94ECBB7BF0A0A6ED62B3607801A27B65F4679F4AD1D4850000C0EA", "URI": "7777772E6F6B2E636F6D" } } ], "PreviousPageMin": "4B4E9C06F24296074F7BC48F92A97916C6DC5EA9659B25014D08E1BC983515BC", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "index": "4B4E9C06F24296074F7BC48F92A97916C6DC5EA9FFFFFFFFFFFFFFFFFFFFFFFF" }, { "Flags": 0, "LedgerEntryType": "NFTokenPage", "NFTokens": [ { "NFToken": { "NFTokenID": "000827103B94ECBB7BF0A0A6ED62B3607801A27B65F4679F4AD1D4850000C0EA", "URI": "7777772E6F6B2E636F6D" } } ], "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", "PreviousTxnLgrSeq": 0, "index": "4B4E9C06F24296074F7BC48F92A97916C6DC5EA9659B25014D08E1BC983515BC" } ] })JSON"; auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft page 1 auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; auto const nftPage2KK = ripple::keylet::nftpage(ripple::keylet::nftpage_min(account), ripple::uint256{kIndex1}).key; auto const nftpage1 = createNftTokenPage( std::vector{std::make_pair(kTokenId, "www.ok.com")}, nftPage2KK ); EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)) .WillOnce(Return(nftpage1.getSerializer().peekData())); // nft page 2 , end auto const nftpage2 = createNftTokenPage( std::vector{std::make_pair(kTokenId, "www.ok.com")}, std::nullopt ); EXPECT_CALL(*backend_, doFetchLedgerObject(nftPage2KK, 30, _)) .WillOnce(Return(nftpage2.getSerializer().peekData())); std::vector bbs; auto const line1 = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); bbs.push_back(line1.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "type": "nft_page" }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(*output.result, json::parse(kExpectedOut)); }); } TEST_F(RPCAccountObjectsHandlerTest, NFTZeroMarkerNotAffectOtherMarker) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); static constexpr auto kLimit = 10; auto count = kLimit * 2; // put 20 items in owner dir, but only return 10 auto const ownerDir = createOwnerDirLedgerObject(std::vector(count, ripple::uint256{kIndex1}), kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); std::vector bbs; while (count-- != 0) { auto const line1 = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); bbs.push_back(line1.getSerializer().peekData()); } EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "limit": {}, "marker": "{},{}" }})JSON", kAccount, kLimit, ripple::strHex(ripple::uint256{beast::zero}), std::numeric_limits::max() ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->as_object().at("account_objects").as_array().size(), kLimit); EXPECT_EQ( output.result->as_object().at("marker").as_string(), fmt::format("{},{}", kIndex1, 0) ); }); } TEST_F(RPCAccountObjectsHandlerTest, LimitLessThanMin) { static auto const kExpectedOut = fmt::format( R"JSON({{ "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index": 30, "validated": true, "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "limit": {}, "account_objects": [ {{ "Balance": {{ "currency": "USD", "issuer": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", "value": "100" }}, "Flags": 0, "HighLimit": {{ "currency": "USD", "issuer": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "value": "20" }}, "LedgerEntryType": "RippleState", "LowLimit": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "10" }}, "PreviousTxnID": "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879", "PreviousTxnLgrSeq": 123, "index": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC" }} ] }})JSON", AccountObjectsHandler::kLimitMin ); auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); std::vector bbs; auto const line1 = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); bbs.push_back(line1.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "limit": {} }})JSON", kAccount, AccountObjectsHandler::kLimitMin - 1 ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(*output.result, json::parse(kExpectedOut)); }); } TEST_F(RPCAccountObjectsHandlerTest, LimitMoreThanMax) { static auto const kExpectedOut = fmt::format( R"JSON({{ "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index": 30, "validated": true, "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "limit": {}, "account_objects": [ {{ "Balance": {{ "currency": "USD", "issuer": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", "value": "100" }}, "Flags": 0, "HighLimit": {{ "currency": "USD", "issuer": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "value": "20" }}, "LedgerEntryType": "RippleState", "LowLimit": {{ "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "value": "10" }}, "PreviousTxnID": "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879", "PreviousTxnLgrSeq": 123, "index": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC" }} ] }})JSON", AccountObjectsHandler::kLimitMax ); auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); std::vector bbs; auto const line1 = createRippleStateLedgerObject( "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 ); bbs.push_back(line1.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "limit": {} }})JSON", kAccount, AccountObjectsHandler::kLimitMax + 1 ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(*output.result, json::parse(kExpectedOut)); }); } TEST_F(RPCAccountObjectsHandlerTest, TypeFilterMPTIssuanceType) { auto const ledgerinfo = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerinfo)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); std::vector bbs; // put 1 mpt issuance auto const issuanceObject = createMptIssuanceObject(kAccount, 2, "metadata"); bbs.push_back(issuanceObject.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "type": "mpt_issuance" }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); auto const& accountObjects = output.result->as_object().at("account_objects").as_array(); ASSERT_EQ(accountObjects.size(), 1); EXPECT_EQ(accountObjects.front().at("LedgerEntryType").as_string(), "MPTokenIssuance"); // make sure mptID is synethetically parsed if object is mptIssuance EXPECT_EQ( accountObjects.front().at("mpt_issuance_id").as_string(), ripple::to_string(ripple::makeMptID(2, getAccountIdWithString(kAccount))) ); }); } TEST_F(RPCAccountObjectsHandlerTest, TypeFilterMPTokenType) { auto const ledgerinfo = createLedgerHeader(kLedgerHash, kMaxSeq); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerinfo)); auto const account = getAccountIdWithString(kAccount); auto const accountKk = ripple::keylet::account(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kMaxSeq, _)) .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kIndex1}}, kIndex1); auto const ownerDirKk = ripple::keylet::ownerDir(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, 30, _)) .WillOnce(Return(ownerDir.getSerializer().peekData())); // nft null auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; EXPECT_CALL(*backend_, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt)); std::vector bbs; // put 1 mpt issuance auto const mptokenObject = createMpTokenObject(kAccount, ripple::makeMptID(2, getAccountIdWithString(kAccount))); bbs.push_back(mptokenObject.getSerializer().peekData()); EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); static auto const kInput = json::parse( fmt::format( R"JSON({{ "account": "{}", "type": "mptoken" }})JSON", kAccount ) ); auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; runSpawn([&](auto yield) { auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); auto const& accountObjects = output.result->as_object().at("account_objects").as_array(); ASSERT_EQ(accountObjects.size(), 1); EXPECT_EQ(accountObjects.front().at("LedgerEntryType").as_string(), "MPToken"); }); }