diff --git a/src/ripple/app/misc/impl/AMMUtils.cpp b/src/ripple/app/misc/impl/AMMUtils.cpp index dcb403296c..1d787dbe4c 100644 --- a/src/ripple/app/misc/impl/AMMUtils.cpp +++ b/src/ripple/app/misc/impl/AMMUtils.cpp @@ -200,14 +200,17 @@ deleteAMMTrustLines( keylet::ownerDir(ammAccountID), [&](LedgerEntryType nodeType, uint256 const&, - std::shared_ptr& sleItem) -> TER { + std::shared_ptr& sleItem) -> std::pair { + // Skip AMM + if (nodeType == LedgerEntryType::ltAMM) + return {tesSUCCESS, SkipEntry::Yes}; // Should only have the trustlines if (nodeType != LedgerEntryType::ltRIPPLE_STATE) { JLOG(j.error()) << "deleteAMMTrustLines: deleting non-trustline " << nodeType; - return tecINTERNAL; + return {tecINTERNAL, SkipEntry::No}; } // Trustlines must have zero balance @@ -216,10 +219,12 @@ deleteAMMTrustLines( JLOG(j.error()) << "deleteAMMTrustLines: deleting trustline with " "non-zero balance."; - return tecINTERNAL; + return {tecINTERNAL, SkipEntry::No}; } - return deleteAMMTrustLine(sb, sleItem, ammAccountID, j); + return { + deleteAMMTrustLine(sb, sleItem, ammAccountID, j), + SkipEntry::No}; }, j, maxTrustlinesToDelete); @@ -255,6 +260,12 @@ deleteAMMAccount( return ter; auto const ownerDirKeylet = keylet::ownerDir(ammAccountID); + if (!sb.dirRemove( + ownerDirKeylet, (*ammSle)[sfOwnerNode], ammSle->key(), false)) + { + JLOG(j.error()) << "deleteAMMAccount: failed to remove dir link"; + return tecINTERNAL; + } if (sb.exists(ownerDirKeylet) && !sb.emptyDirDelete(ownerDirKeylet)) { JLOG(j.error()) << "deleteAMMAccount: cannot delete root dir node of " diff --git a/src/ripple/app/tx/impl/AMMCreate.cpp b/src/ripple/app/tx/impl/AMMCreate.cpp index 0c6874953a..55b1126fcd 100644 --- a/src/ripple/app/tx/impl/AMMCreate.cpp +++ b/src/ripple/app/tx/impl/AMMCreate.cpp @@ -279,6 +279,20 @@ applyCreate( // AMM creator gets the auction slot and the voting slot. initializeFeeAuctionVote( ctx_.view(), ammSle, account_, lptIss, ctx_.tx[sfTradingFee]); + + // Add owner directory to link the root account and AMM object. + if (auto const page = sb.dirInsert( + keylet::ownerDir(*ammAccount), + ammSle->key(), + describeOwnerDir(*ammAccount))) + { + ammSle->setFieldU64(sfOwnerNode, *page); + } + else + { + JLOG(j_.debug()) << "AMM Instance: failed to insert owner dir"; + return {tecDIR_FULL, false}; + } sb.insert(ammSle); // Send LPT to LP. diff --git a/src/ripple/app/tx/impl/DeleteAccount.cpp b/src/ripple/app/tx/impl/DeleteAccount.cpp index eeffc66d38..67545723a5 100644 --- a/src/ripple/app/tx/impl/DeleteAccount.cpp +++ b/src/ripple/app/tx/impl/DeleteAccount.cpp @@ -298,19 +298,19 @@ DeleteAccount::doApply() ownerDirKeylet, [&](LedgerEntryType nodeType, uint256 const& dirEntry, - std::shared_ptr& sleItem) -> TER { + std::shared_ptr& sleItem) -> std::pair { if (auto deleter = nonObligationDeleter(nodeType)) { TER const result{ deleter(ctx_.app, view(), account_, dirEntry, sleItem, j_)}; - return result; + return {result, SkipEntry::No}; } assert(!"Undeletable entry should be found in preclaim."); JLOG(j_.error()) << "DeleteAccount undeletable item not " "found in preclaim."; - return tecHAS_OBLIGATIONS; + return {tecHAS_OBLIGATIONS, SkipEntry::No}; }, ctx_.journal); if (ter != tesSUCCESS) diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index 2c8d2354e0..5680114a79 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -43,6 +43,7 @@ namespace ripple { enum class WaiveTransferFee : bool { No = false, Yes }; +enum class SkipEntry : bool { No = false, Yes }; //------------------------------------------------------------------------------ // @@ -458,6 +459,14 @@ transferXRP( [[nodiscard]] TER requireAuth(ReadView const& view, Issue const& issue, AccountID const& account); +/** Deleter function prototype. Returns the status of the entry deletion + * (if should not be skipped) and if the entry should be skipped. The status + * is always tesSUCCESS if the entry should be skipped. + */ +using EntryDeleter = std::function( + LedgerEntryType, + uint256 const&, + std::shared_ptr&)>; /** Cleanup owner directory entries on account delete. * Used for a regular and AMM accounts deletion. The caller * has to provide the deleter function, which handles details of @@ -469,8 +478,7 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account); cleanupOnAccountDelete( ApplyView& view, Keylet const& ownerDirKeylet, - std::function&)> - deleter, + EntryDeleter const& deleter, beast::Journal j, std::optional maxNodesToDelete = std::nullopt); diff --git a/src/ripple/ledger/impl/View.cpp b/src/ripple/ledger/impl/View.cpp index 75fd35782b..5050e8764e 100644 --- a/src/ripple/ledger/impl/View.cpp +++ b/src/ripple/ledger/impl/View.cpp @@ -1531,8 +1531,7 @@ TER cleanupOnAccountDelete( ApplyView& view, Keylet const& ownerDirKeylet, - std::function&)> - deleter, + EntryDeleter const& deleter, beast::Journal j, std::optional maxNodesToDelete) { @@ -1567,8 +1566,8 @@ cleanupOnAccountDelete( // Deleter handles the details of specific account-owned object // deletion - if (auto const ter = deleter(nodeType, dirEntry, sleItem); - ter != tesSUCCESS) + auto const [ter, skipEntry] = deleter(nodeType, dirEntry, sleItem); + if (ter != tesSUCCESS) return ter; // dirFirst() and dirNext() are like iterators with exposed @@ -1580,21 +1579,22 @@ cleanupOnAccountDelete( // "iterator state" is invalid. // // 1. During the process of getting an entry from the - // directory uDirEntry was incremented from 0 to 1. + // directory uDirEntry was incremented from 'it' to 'it'+1. // - // 2. We then deleted the entry at index 0, which means the - // entry that was at 1 has now moved to 0. + // 2. We then deleted the entry at index 'it', which means the + // entry that was at 'it'+1 has now moved to 'it'. // - // 3. So we verify that uDirEntry is indeed 1. Then we jam it - // back to zero to "un-invalidate" the iterator. - assert(uDirEntry == 1); - if (uDirEntry != 1) + // 3. So we verify that uDirEntry is indeed 'it'+1. Then we jam it + // back to 'it' to "un-invalidate" the iterator. + assert(uDirEntry >= 1); + if (uDirEntry == 0) { JLOG(j.error()) << "DeleteAccount iterator re-validation failed."; return tefBAD_LEDGER; } - uDirEntry = 0; + if (skipEntry == SkipEntry::No) + uDirEntry--; } while ( dirNext(view, ownerDirKeylet.key, sleDirNode, uDirEntry, dirEntry)); diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 9192513457..d9e7ca178c 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -279,6 +279,7 @@ LedgerFormats::LedgerFormats() {sfLPTokenBalance, soeREQUIRED}, {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, }, commonFields); diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index a27a564e11..567e63699e 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -160,6 +160,7 @@ JSS(alternatives); // out: PathRequest, RipplePathFind JSS(amendment_blocked); // out: NetworkOPs JSS(amendments); // in: AccountObjects, out: NetworkOPs JSS(amm); // out: amm_info +JSS(amm_account); // in: amm_info JSS(amount); // out: AccountChannels, amm_info JSS(amount2); // out: amm_info JSS(api_version); // in: many, out: Version diff --git a/src/ripple/rpc/handlers/AMMInfo.cpp b/src/ripple/rpc/handlers/AMMInfo.cpp index bcac9da171..11e124afb4 100644 --- a/src/ripple/rpc/handlers/AMMInfo.cpp +++ b/src/ripple/rpc/handlers/AMMInfo.cpp @@ -74,51 +74,96 @@ doAMMInfo(RPC::JsonContext& context) { auto const& params(context.params); Json::Value result; - std::optional accountID; - - Issue issue1; - Issue issue2; - - if (!params.isMember(jss::asset) || !params.isMember(jss::asset2)) - { - RPC::inject_error(rpcINVALID_PARAMS, result); - return result; - } - - if (auto const i = getIssue(params[jss::asset], context.j); !i) - { - RPC::inject_error(i.error(), result); - return result; - } - else - issue1 = *i; - if (auto const i = getIssue(params[jss::asset2], context.j); !i) - { - RPC::inject_error(i.error(), result); - return result; - } - else - issue2 = *i; std::shared_ptr ledger; result = RPC::lookupLedger(ledger, context); if (!ledger) return result; - if (params.isMember(jss::account)) + struct ValuesFromContextParams { - accountID = getAccount(params[jss::account], result); - if (!accountID || !ledger->read(keylet::account(*accountID))) + std::optional accountID; + Issue issue1; + Issue issue2; + std::shared_ptr amm; + }; + + auto getValuesFromContextParams = + [&]() -> Expected { + std::optional accountID; + std::optional issue1; + std::optional issue2; + std::optional ammID; + + if ((params.isMember(jss::asset) != params.isMember(jss::asset2)) || + (params.isMember(jss::asset) == params.isMember(jss::amm_account))) + return Unexpected(rpcINVALID_PARAMS); + + if (params.isMember(jss::asset)) { - RPC::inject_error(rpcACT_MALFORMED, result); - return result; + if (auto const i = getIssue(params[jss::asset], context.j)) + issue1 = *i; + else + return Unexpected(i.error()); } + + if (params.isMember(jss::asset2)) + { + if (auto const i = getIssue(params[jss::asset2], context.j)) + issue2 = *i; + else + return Unexpected(i.error()); + } + + if (params.isMember(jss::amm_account)) + { + auto const id = getAccount(params[jss::amm_account], result); + if (!id) + return Unexpected(rpcACT_MALFORMED); + auto const sle = ledger->read(keylet::account(*id)); + if (!sle) + return Unexpected(rpcACT_MALFORMED); + ammID = sle->getFieldH256(sfAMMID); + } + + assert( + (issue1.has_value() == issue2.has_value()) && + (issue1.has_value() != ammID.has_value())); + + if (params.isMember(jss::account)) + { + accountID = getAccount(params[jss::account], result); + if (!accountID || !ledger->read(keylet::account(*accountID))) + return Unexpected(rpcACT_MALFORMED); + } + + auto const ammKeylet = [&]() { + if (issue1 && issue2) + return keylet::amm(*issue1, *issue2); + assert(ammID); + return keylet::amm(*ammID); + }(); + auto const amm = ledger->read(ammKeylet); + if (!amm) + return Unexpected(rpcACT_NOT_FOUND); + if (!issue1 && !issue2) + { + issue1 = (*amm)[sfAsset]; + issue2 = (*amm)[sfAsset2]; + } + + return ValuesFromContextParams{ + accountID, *issue1, *issue2, std::move(amm)}; + }; + + auto const r = getValuesFromContextParams(); + if (!r) + { + RPC::inject_error(r.error(), result); + return result; } - auto const ammKeylet = keylet::amm(issue1, issue2); - auto const amm = ledger->read(ammKeylet); - if (!amm) - return rpcError(rpcACT_NOT_FOUND); + auto const& [accountID, issue1, issue2, amm] = *r; auto const ammAccountID = amm->getAccountID(sfAccount); diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index 3cf06bfe40..c7c6f3b847 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -105,7 +105,10 @@ public: ammRpcInfo( std::optional const& account = std::nullopt, std::optional const& ledgerIndex = std::nullopt, - std::optional> tokens = std::nullopt) const; + std::optional issue1 = std::nullopt, + std::optional issue2 = std::nullopt, + std::optional const& ammAccount = std::nullopt, + bool ignoreParams = false) const; /** Verify the AMM balances. */ @@ -150,7 +153,8 @@ public: STAmount const& asset2, IOUAmount const& balance, std::optional const& account = std::nullopt, - std::optional const& ledger_index = std::nullopt) const; + std::optional const& ledger_index = std::nullopt, + std::optional const& ammAccount = std::nullopt) const; [[nodiscard]] bool ammExists() const; diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index c09d496f43..dee1cb1bf5 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -136,26 +136,36 @@ Json::Value AMM::ammRpcInfo( std::optional const& account, std::optional const& ledgerIndex, - std::optional> tokens) const + std::optional issue1, + std::optional issue2, + std::optional const& ammAccount, + bool ignoreParams) const { Json::Value jv; if (account) jv[jss::account] = to_string(*account); if (ledgerIndex) jv[jss::ledger_index] = *ledgerIndex; - if (tokens) + if (!ignoreParams) { - jv[jss::asset] = - STIssue(sfAsset, tokens->first).getJson(JsonOptions::none); - jv[jss::asset2] = - STIssue(sfAsset2, tokens->second).getJson(JsonOptions::none); - } - else - { - jv[jss::asset] = - STIssue(sfAsset, asset1_.issue()).getJson(JsonOptions::none); - jv[jss::asset2] = - STIssue(sfAsset2, asset2_.issue()).getJson(JsonOptions::none); + if (issue1 || issue2) + { + if (issue1) + jv[jss::asset] = + STIssue(sfAsset, *issue1).getJson(JsonOptions::none); + if (issue2) + jv[jss::asset2] = + STIssue(sfAsset2, *issue2).getJson(JsonOptions::none); + } + else if (!ammAccount) + { + jv[jss::asset] = + STIssue(sfAsset, asset1_.issue()).getJson(JsonOptions::none); + jv[jss::asset2] = + STIssue(sfAsset2, asset2_.issue()).getJson(JsonOptions::none); + } + if (ammAccount) + jv[jss::amm_account] = to_string(*ammAccount); } auto jr = env_.rpc("json", "amm_info", to_string(jv)); if (jr.isObject() && jr.isMember(jss::result) && @@ -292,9 +302,11 @@ AMM::expectAmmRpcInfo( STAmount const& asset2, IOUAmount const& balance, std::optional const& account, - std::optional const& ledger_index) const + std::optional const& ledger_index, + std::optional const& ammAccount) const { - auto const jv = ammRpcInfo(account, ledger_index); + auto const jv = ammRpcInfo( + account, ledger_index, std::nullopt, std::nullopt, ammAccount); return expectAmmInfo(asset1, asset2, balance, jv); } diff --git a/src/test/rpc/AMMInfo_test.cpp b/src/test/rpc/AMMInfo_test.cpp index 94795f4857..1d9642539a 100644 --- a/src/test/rpc/AMMInfo_test.cpp +++ b/src/test/rpc/AMMInfo_test.cpp @@ -42,7 +42,7 @@ public: Account const gw("gw"); auto const USD = gw["USD"]; auto const jv = - ammAlice.ammRpcInfo({}, {}, {{USD.issue(), USD.issue()}}); + ammAlice.ammRpcInfo({}, {}, USD.issue(), USD.issue()); BEAST_EXPECT(jv[jss::error_message] == "Account not found."); }); @@ -52,6 +52,40 @@ public: auto const jv = ammAlice.ammRpcInfo(bogie.id()); BEAST_EXPECT(jv[jss::error_message] == "Account malformed."); }); + + // Invalid parameters + testAMM([&](AMM& ammAlice, Env&) { + std::vector, + std::optional, + std::optional, + bool>> + vals = { + {xrpIssue(), std::nullopt, std::nullopt, false}, + {std::nullopt, USD.issue(), std::nullopt, false}, + {xrpIssue(), std::nullopt, ammAlice.ammAccount(), false}, + {std::nullopt, USD.issue(), ammAlice.ammAccount(), false}, + {xrpIssue(), USD.issue(), ammAlice.ammAccount(), false}, + {std::nullopt, std::nullopt, std::nullopt, true}}; + for (auto const& [iss1, iss2, acct, ignoreParams] : vals) + { + auto const jv = ammAlice.ammRpcInfo( + std::nullopt, std::nullopt, iss1, iss2, acct, ignoreParams); + BEAST_EXPECT(jv[jss::error_message] == "Invalid parameters."); + } + }); + + // Invalid AMM account id + testAMM([&](AMM& ammAlice, Env&) { + Account bogie("bogie"); + auto const jv = ammAlice.ammRpcInfo( + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + bogie.id()); + BEAST_EXPECT(jv[jss::error_message] == "Account malformed."); + }); } void @@ -63,6 +97,13 @@ public: testAMM([&](AMM& ammAlice, Env&) { BEAST_EXPECT(ammAlice.expectAmmRpcInfo( XRP(10000), USD(10000), IOUAmount{10000000, 0})); + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRP(10000), + USD(10000), + IOUAmount{10000000, 0}, + std::nullopt, + std::nullopt, + ammAlice.ammAccount())); }); } @@ -91,53 +132,71 @@ public: env.fund(XRP(1000), bob, ed, bill); ammAlice.bid(alice, 100, std::nullopt, {carol, bob, ed, bill}); BEAST_EXPECT(ammAlice.expectAmmRpcInfo( - XRP(80000), USD(80000), IOUAmount{79994400})); - std::unordered_set authAccounts = { - carol.human(), bob.human(), ed.human(), bill.human()}; - auto const ammInfo = ammAlice.ammRpcInfo(); - auto const& amm = ammInfo[jss::amm]; - try + XRP(80000), + USD(80000), + IOUAmount{79994400}, + std::nullopt, + std::nullopt, + ammAlice.ammAccount())); + for (auto i = 0; i < 2; ++i) { - // votes - auto const voteSlots = amm[jss::vote_slots]; - for (std::uint8_t i = 0; i < 8; ++i) + std::unordered_set authAccounts = { + carol.human(), bob.human(), ed.human(), bill.human()}; + auto const ammInfo = i ? ammAlice.ammRpcInfo() + : ammAlice.ammRpcInfo( + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + ammAlice.ammAccount()); + auto const& amm = ammInfo[jss::amm]; + try { - if (!BEAST_EXPECT( - votes[voteSlots[i][jss::account].asString()] == - voteSlots[i][jss::trading_fee].asUInt() && - voteSlots[i][jss::vote_weight].asUInt() == 12500)) + // votes + auto const voteSlots = amm[jss::vote_slots]; + auto votesCopy = votes; + for (std::uint8_t i = 0; i < 8; ++i) + { + if (!BEAST_EXPECT( + votes[voteSlots[i][jss::account].asString()] == + voteSlots[i][jss::trading_fee].asUInt() && + voteSlots[i][jss::vote_weight].asUInt() == + 12500)) + return; + votes.erase(voteSlots[i][jss::account].asString()); + } + if (!BEAST_EXPECT(votes.empty())) return; - votes.erase(voteSlots[i][jss::account].asString()); - } - if (!BEAST_EXPECT(votes.empty())) - return; + votes = votesCopy; - // bid - auto const auctionSlot = amm[jss::auction_slot]; - for (std::uint8_t i = 0; i < 4; ++i) - { - if (!BEAST_EXPECT(authAccounts.contains( + // bid + auto const auctionSlot = amm[jss::auction_slot]; + for (std::uint8_t i = 0; i < 4; ++i) + { + if (!BEAST_EXPECT(authAccounts.contains( + auctionSlot[jss::auth_accounts][i][jss::account] + .asString()))) + return; + authAccounts.erase( auctionSlot[jss::auth_accounts][i][jss::account] - .asString()))) + .asString()); + } + if (!BEAST_EXPECT(authAccounts.empty())) return; - authAccounts.erase( - auctionSlot[jss::auth_accounts][i][jss::account] - .asString()); + BEAST_EXPECT( + auctionSlot[jss::account].asString() == alice.human() && + auctionSlot[jss::discounted_fee].asUInt() == 17 && + auctionSlot[jss::price][jss::value].asString() == + "5600" && + auctionSlot[jss::price][jss::currency].asString() == + to_string(ammAlice.lptIssue().currency) && + auctionSlot[jss::price][jss::issuer].asString() == + to_string(ammAlice.lptIssue().account)); + } + catch (std::exception const& e) + { + fail(e.what(), __FILE__, __LINE__); } - if (!BEAST_EXPECT(authAccounts.empty())) - return; - BEAST_EXPECT( - auctionSlot[jss::account].asString() == alice.human() && - auctionSlot[jss::discounted_fee].asUInt() == 17 && - auctionSlot[jss::price][jss::value].asString() == "5600" && - auctionSlot[jss::price][jss::currency].asString() == - to_string(ammAlice.lptIssue().currency) && - auctionSlot[jss::price][jss::issuer].asString() == - to_string(ammAlice.lptIssue().account)); - } - catch (std::exception const& e) - { - fail(e.what(), __FILE__, __LINE__); } }); } diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 7de5b73671..90d4e54684 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include @@ -552,10 +553,19 @@ public: Env env(*this); // Make a lambda we can use to get "account_objects" easily. - auto acct_objs = [&env](Account const& acct, char const* type) { + auto acct_objs = [&env]( + AccountID const& acct, + std::optional const& type, + std::optional limit = std::nullopt, + std::optional marker = std::nullopt) { Json::Value params; - params[jss::account] = acct.human(); - params[jss::type] = type; + params[jss::account] = to_string(acct); + if (type) + params[jss::type] = *type; + if (limit) + params[jss::limit] = *limit; + if (marker) + params[jss::marker] = *marker; params[jss::ledger_index] = "validated"; return env.rpc("json", "account_objects", to_string(params)); }; @@ -585,6 +595,7 @@ public: BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::signer_list), 0)); BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::state), 0)); BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::ticket), 0)); + BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::amm), 0)); // gw mints an NFT so we can find it. uint256 const nftID{token::getNextID(env, gw, 0u, tfTransferable)}; @@ -782,6 +793,67 @@ public: BEAST_EXPECT(aobjs[0u]["LedgerEntryType"] == jss::Escrow); } } + { + // Make a lambda to get the types + auto getTypes = [&](Json::Value const& resp, + std::vector& typesOut) { + auto const objs = resp[jss::result][jss::account_objects]; + for (auto const& obj : resp[jss::result][jss::account_objects]) + typesOut.push_back( + obj[sfLedgerEntryType.fieldName].asString()); + std::sort(typesOut.begin(), typesOut.end()); + }; + // Make a lambda we can use to check the number of fetched + // account objects and their ledger type + auto expectObjects = + [&](Json::Value const& resp, + std::vector const& types) -> bool { + if (!acct_objs_is_size(resp, types.size())) + return false; + std::vector typesOut; + getTypes(resp, typesOut); + return types == typesOut; + }; + // Find AMM objects + AMM amm(env, gw, XRP(1'000), USD(1'000)); + amm.deposit(alice, USD(1)); + // AMM account has 4 objects: AMM object and 3 trustlines + auto const lines = getAccountLines(env, amm.ammAccount()); + BEAST_EXPECT(lines[jss::lines].size() == 3); + // request AMM only, doesn't depend on the limit + BEAST_EXPECT( + acct_objs_is_size(acct_objs(amm.ammAccount(), jss::amm), 1)); + // request first two objects + auto resp = acct_objs(amm.ammAccount(), std::nullopt, 2); + std::vector typesOut; + getTypes(resp, typesOut); + // request next two objects + resp = acct_objs( + amm.ammAccount(), + std::nullopt, + 10, + resp[jss::result][jss::marker].asString()); + getTypes(resp, typesOut); + BEAST_EXPECT( + (typesOut == + std::vector{ + jss::AMM.c_str(), + jss::RippleState.c_str(), + jss::RippleState.c_str(), + jss::RippleState.c_str()})); + // filter by state: there are three trustlines + resp = acct_objs(amm.ammAccount(), jss::state, 10); + BEAST_EXPECT(expectObjects( + resp, + {jss::RippleState.c_str(), + jss::RippleState.c_str(), + jss::RippleState.c_str()})); + // AMM account doesn't own offers + BEAST_EXPECT( + acct_objs_is_size(acct_objs(amm.ammAccount(), jss::offer), 0)); + // gw account doesn't own AMM object + BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::amm), 0)); + } // Run up the number of directory entries so gw has two // directory nodes.