#include <test/jtx.h>

#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>

namespace xrpl {
namespace RPC {

class AccountLines_test : public beast::unit_test::suite
{
public:
    void
    testAccountLines()
    {
        testcase("account_lines");

        using namespace test::jtx;
        Env env(*this);
        {
            // account_lines with no account.
            auto const lines = env.rpc("json", "account_lines", "{ }");
            BEAST_EXPECT(
                lines[jss::result][jss::error_message] ==
                RPC::missing_field_error(jss::account)[jss::error_message]);
        }
        {
            // account_lines with a malformed account.
            Json::Value params;
            params[jss::account] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
            auto const lines = env.rpc("json", "account_lines", to_string(params));
            BEAST_EXPECT(
                lines[jss::result][jss::error_message] ==
                RPC::make_error(rpcACT_MALFORMED)[jss::error_message]);
        }
        {
            // test account non-string
            auto testInvalidAccountParam = [&](auto const& param) {
                Json::Value params;
                params[jss::account] = param;
                auto jrr = env.rpc("json", "account_lines", to_string(params))[jss::result];
                BEAST_EXPECT(jrr[jss::error] == "invalidParams");
                BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'.");
            };

            testInvalidAccountParam(1);
            testInvalidAccountParam(1.1);
            testInvalidAccountParam(true);
            testInvalidAccountParam(Json::Value(Json::nullValue));
            testInvalidAccountParam(Json::Value(Json::objectValue));
            testInvalidAccountParam(Json::Value(Json::arrayValue));
        }
        Account const alice{"alice"};
        {
            // account_lines on an unfunded account.
            Json::Value params;
            params[jss::account] = alice.human();
            auto const lines = env.rpc("json", "account_lines", to_string(params));
            BEAST_EXPECT(
                lines[jss::result][jss::error_message] ==
                RPC::make_error(rpcACT_NOT_FOUND)[jss::error_message]);
        }
        env.fund(XRP(10000), alice);
        env.close();
        LedgerHeader const ledger3Info = env.closed()->header();
        BEAST_EXPECT(ledger3Info.seq == 3);

        {
            // alice is funded but has no lines.  An empty array is returned.
            Json::Value params;
            params[jss::account] = alice.human();
            auto const lines = env.rpc("json", "account_lines", to_string(params));
            BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
            BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0);
        }
        {
            // Specify a ledger that doesn't exist.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::ledger_index] = "nonsense";
            auto const lines = env.rpc("json", "account_lines", to_string(params));
            BEAST_EXPECT(
                lines[jss::result][jss::error_message] ==
                "Invalid field 'ledger_index', not string or number.");
        }
        {
            // Specify a different ledger that doesn't exist.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::ledger_index] = 50000;
            auto const lines = env.rpc("json", "account_lines", to_string(params));
            BEAST_EXPECT(lines[jss::result][jss::error_message] == "ledgerNotFound");
        }
        // Create trust lines to share with alice.
        Account const gw1{"gw1"};
        env.fund(XRP(10000), gw1);
        std::vector<IOU> gw1Currencies;

        for (char c = 0; c <= ('Z' - 'A'); ++c)
        {
            // gw1 currencies have names "YAA" -> "YAZ".
            gw1Currencies.push_back(gw1[std::string("YA") + static_cast<char>('A' + c)]);
            IOU const& gw1Currency = gw1Currencies.back();

            // Establish trust lines.
            env(trust(alice, gw1Currency(100 + c)));
            env(pay(gw1, alice, gw1Currency(50 + c)));
        }
        env.close();
        LedgerHeader const ledger4Info = env.closed()->header();
        BEAST_EXPECT(ledger4Info.seq == 4);

        // Add another set of trust lines in another ledger so we can see
        // differences in historic ledgers.
        Account const gw2{"gw2"};
        env.fund(XRP(10000), gw2);

        // gw2 requires authorization.
        env(fset(gw2, asfRequireAuth));
        env.close();
        std::vector<IOU> gw2Currencies;

        for (char c = 0; c <= ('Z' - 'A'); ++c)
        {
            // gw2 currencies have names "ZAA" -> "ZAZ".
            gw2Currencies.push_back(gw2[std::string("ZA") + static_cast<char>('A' + c)]);
            IOU const& gw2Currency = gw2Currencies.back();

            // Establish trust lines.
            env(trust(alice, gw2Currency(200 + c)));
            env(trust(gw2, gw2Currency(0), alice, tfSetfAuth));
            env.close();
            env(pay(gw2, alice, gw2Currency(100 + c)));
            env.close();

            // Set flags on gw2 trust lines so we can look for them.
            env(trust(alice, gw2Currency(0), gw2, tfSetNoRipple | tfSetFreeze | tfSetDeepFreeze));
        }
        env.close();
        LedgerHeader const ledger58Info = env.closed()->header();
        BEAST_EXPECT(ledger58Info.seq == 58);

        // A re-usable test for historic ledgers.
        auto testAccountLinesHistory =
            [this, &env](Account const& account, LedgerHeader const& info, int count) {
                // Get account_lines by ledger index.
                Json::Value paramsSeq;
                paramsSeq[jss::account] = account.human();
                paramsSeq[jss::ledger_index] = info.seq;
                auto const linesSeq = env.rpc("json", "account_lines", to_string(paramsSeq));
                BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
                BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);

                // Get account_lines by ledger hash.
                Json::Value paramsHash;
                paramsHash[jss::account] = account.human();
                paramsHash[jss::ledger_hash] = to_string(info.hash);
                auto const linesHash = env.rpc("json", "account_lines", to_string(paramsHash));
                BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
                BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
            };

        // Alice should have no trust lines in ledger 3.
        testAccountLinesHistory(alice, ledger3Info, 0);

        // Alice should have 26 trust lines in ledger 4.
        testAccountLinesHistory(alice, ledger4Info, 26);

        // Alice should have 52 trust lines in ledger 58.
        testAccountLinesHistory(alice, ledger58Info, 52);

        {
            // Invalid to specify both index and hash
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::ledger_hash] = to_string(ledger4Info.hash);
            params[jss::ledger_index] = ledger58Info.seq;
            auto const lines = env.rpc("json", "account_lines", to_string(params))[jss::result];
            BEAST_EXPECT(lines[jss::error] == "invalidParams");
            BEAST_EXPECT(
                lines[jss::error_message] ==
                "Exactly one of 'ledger_hash' or 'ledger_index' can be "
                "specified.");
        }
        {
            // Invalid index
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::ledger_index] = Json::objectValue;
            auto const lines = env.rpc("json", "account_lines", to_string(params))[jss::result];
            BEAST_EXPECT(lines[jss::error] == "invalidParams");
            BEAST_EXPECT(
                lines[jss::error_message] == "Invalid field 'ledger_index', not string or number.");
        }
        {
            // alice should have 52 trust lines in the current ledger.
            Json::Value params;
            params[jss::account] = alice.human();
            auto const lines = env.rpc("json", "account_lines", to_string(params));
            BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
            BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
        }
        {
            // alice should have 26 trust lines with gw1.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::peer] = gw1.human();
            auto const lines = env.rpc("json", "account_lines", to_string(params));
            BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
            BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);

            // Check no ripple is not set for trustlines between alice and gw1
            auto const& line = lines[jss::result][jss::lines][0u];
            BEAST_EXPECT(!line[jss::no_ripple].isMember(jss::no_ripple));
        }
        {
            // Use a malformed peer.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::peer] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
            auto const lines = env.rpc("json", "account_lines", to_string(params));
            BEAST_EXPECT(
                lines[jss::result][jss::error_message] ==
                RPC::make_error(rpcACT_MALFORMED)[jss::error_message]);
        }
        {
            // A negative limit should fail.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::limit] = -1;
            auto const lines = env.rpc("json", "account_lines", to_string(params));
            BEAST_EXPECT(
                lines[jss::result][jss::error_message] ==
                RPC::expected_field_message(jss::limit, "unsigned integer"));
        }
        {
            // Limit the response to 1 trust line.
            Json::Value paramsA;
            paramsA[jss::account] = alice.human();
            paramsA[jss::limit] = 1;
            auto const linesA = env.rpc("json", "account_lines", to_string(paramsA));
            BEAST_EXPECT(linesA[jss::result][jss::lines].isArray());
            BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1);

            // Pick up from where the marker left off.  We should get 51.
            auto marker = linesA[jss::result][jss::marker].asString();
            Json::Value paramsB;
            paramsB[jss::account] = alice.human();
            paramsB[jss::marker] = marker;
            auto const linesB = env.rpc("json", "account_lines", to_string(paramsB));
            BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
            BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);

            // Go again from where the marker left off, but set a limit of 3.
            Json::Value paramsC;
            paramsC[jss::account] = alice.human();
            paramsC[jss::limit] = 3;
            paramsC[jss::marker] = marker;
            auto const linesC = env.rpc("json", "account_lines", to_string(paramsC));
            BEAST_EXPECT(linesC[jss::result][jss::lines].isArray());
            BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3);

            // Mess with the marker so it becomes bad and check for the error.
            marker[5] = marker[5] == '7' ? '8' : '7';
            Json::Value paramsD;
            paramsD[jss::account] = alice.human();
            paramsD[jss::marker] = marker;
            auto const linesD = env.rpc("json", "account_lines", to_string(paramsD));
            BEAST_EXPECT(
                linesD[jss::result][jss::error_message] ==
                RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
        }
        {
            // A non-string marker should also fail.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::marker] = true;
            auto const lines = env.rpc("json", "account_lines", to_string(params));
            BEAST_EXPECT(
                lines[jss::result][jss::error_message] ==
                RPC::expected_field_message(jss::marker, "string"));
        }
        {
            // Check that the flags we expect from alice to gw2 are present.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::limit] = 10;
            params[jss::peer] = gw2.human();
            auto const lines = env.rpc("json", "account_lines", to_string(params));
            auto const& line = lines[jss::result][jss::lines][0u];
            BEAST_EXPECT(line[jss::freeze].asBool() == true);
            BEAST_EXPECT(line[jss::deep_freeze].asBool() == true);
            BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
            BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
        }
        {
            // Check that the flags we expect from gw2 to alice are present.
            Json::Value paramsA;
            paramsA[jss::account] = gw2.human();
            paramsA[jss::limit] = 1;
            paramsA[jss::peer] = alice.human();
            auto const linesA = env.rpc("json", "account_lines", to_string(paramsA));
            auto const& lineA = linesA[jss::result][jss::lines][0u];
            BEAST_EXPECT(lineA[jss::freeze_peer].asBool() == true);
            BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true);
            BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
            BEAST_EXPECT(lineA[jss::authorized].asBool() == true);

            // Continue from the returned marker to make sure that works.
            BEAST_EXPECT(linesA[jss::result].isMember(jss::marker));
            auto const marker = linesA[jss::result][jss::marker].asString();
            Json::Value paramsB;
            paramsB[jss::account] = gw2.human();
            paramsB[jss::limit] = 25;
            paramsB[jss::marker] = marker;
            paramsB[jss::peer] = alice.human();
            auto const linesB = env.rpc("json", "account_lines", to_string(paramsB));
            BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
            BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25);
            BEAST_EXPECT(!linesB[jss::result].isMember(jss::marker));
        }
    }

    void
    testAccountLinesMarker()
    {
        testcase("Entry pointed to by marker is not owned by account");
        using namespace test::jtx;
        Env env(*this);

        // The goal of this test is observe account_lines RPC calls return an
        // error message when the SLE pointed to by the marker is not owned by
        // the Account being traversed.
        //
        // To start, we'll create an environment with some trust lines, offers
        // and a signers list.
        Account const alice{"alice"};
        Account const becky{"becky"};
        Account const gw1{"gw1"};
        env.fund(XRP(10000), alice, becky, gw1);
        env.close();

        // Give alice a SignerList.
        Account const bogie{"bogie"};
        env(signers(alice, 2, {{bogie, 3}}));
        env.close();

        auto const EUR = gw1["EUR"];
        env(trust(alice, EUR(200)));
        env(trust(becky, EUR(200)));
        env.close();

        // Get all account objects for alice and verify that her
        // signerlist is first.  This is only a (reliable) coincidence of
        // object naming.  So if any of alice's objects are renamed this
        // may fail.
        Json::Value aliceObjectsParams;
        aliceObjectsParams[jss::account] = alice.human();
        aliceObjectsParams[jss::limit] = 10;
        Json::Value const aliceObjects =
            env.rpc("json", "account_objects", to_string(aliceObjectsParams));
        Json::Value const& aliceSignerList = aliceObjects[jss::result][jss::account_objects][0u];
        if (!(aliceSignerList[sfLedgerEntryType.jsonName] == jss::SignerList))
        {
            fail(
                "alice's account objects are misordered.  "
                "Please reorder the objects so the SignerList is first.",
                __FILE__,
                __LINE__);
            return;
        }

        // Get account_lines for alice.  Limit at 1, so we get a marker
        // pointing to her SignerList.
        Json::Value aliceLines1Params;
        aliceLines1Params[jss::account] = alice.human();
        aliceLines1Params[jss::limit] = 1;
        auto const aliceLines1 = env.rpc("json", "account_lines", to_string(aliceLines1Params));
        BEAST_EXPECT(aliceLines1[jss::result].isMember(jss::marker));

        // Verify that the marker points at the signer list.
        std::string const aliceMarker = aliceLines1[jss::result][jss::marker].asString();
        std::string const markerIndex = aliceMarker.substr(0, aliceMarker.find(','));
        BEAST_EXPECT(markerIndex == aliceSignerList[jss::index].asString());

        // When we fetch Alice's remaining lines we should find one and no more.
        Json::Value aliceLines2Params;
        aliceLines2Params[jss::account] = alice.human();
        aliceLines2Params[jss::marker] = aliceMarker;
        auto const aliceLines2 = env.rpc("json", "account_lines", to_string(aliceLines2Params));
        BEAST_EXPECT(aliceLines2[jss::result][jss::lines].size() == 1);
        BEAST_EXPECT(!aliceLines2[jss::result].isMember(jss::marker));

        // Get account lines for becky's account, using alice's SignerList as a
        // marker. This should cause an error.
        Json::Value beckyLinesParams;
        beckyLinesParams[jss::account] = becky.human();
        beckyLinesParams[jss::marker] = aliceMarker;
        auto const beckyLines = env.rpc("json", "account_lines", to_string(beckyLinesParams));
        BEAST_EXPECT(beckyLines[jss::result].isMember(jss::error_message));
    }

    void
    testAccountLineDelete()
    {
        testcase("Entry pointed to by marker is removed");
        using namespace test::jtx;
        Env env(*this);

        // The goal here is to observe account_lines marker behavior if the
        // entry pointed at by a returned marker is removed from the ledger.
        //
        // It isn't easy to explicitly delete a trust line, so we do so in a
        // round-about fashion.  It takes 4 actors:
        //   o Gateway gw1 issues USD
        //   o alice offers to buy 100 USD for 100 XRP.
        //   o becky offers to sell 100 USD for 100 XRP.
        // There will now be an inferred trustline between alice and gw1.
        //   o alice pays her 100 USD to cheri.
        // alice should now have no USD and no trustline to gw1.
        Account const alice{"alice"};
        Account const becky{"becky"};
        Account const cheri{"cheri"};
        Account const gw1{"gw1"};
        Account const gw2{"gw2"};
        env.fund(XRP(10000), alice, becky, cheri, gw1, gw2);
        env.close();

        auto const USD = gw1["USD"];
        auto const AUD = gw1["AUD"];
        auto const EUR = gw2["EUR"];
        env(trust(alice, USD(200)));
        env(trust(alice, AUD(200)));
        env(trust(becky, EUR(200)));
        env(trust(cheri, EUR(200)));
        env.close();

        // becky gets 100 USD from gw1.
        env(pay(gw2, becky, EUR(100)));
        env.close();

        // alice offers to buy 100 EUR for 100 XRP.
        env(offer(alice, EUR(100), XRP(100)));
        env.close();

        // becky offers to buy 100 XRP for 100 EUR.
        env(offer(becky, XRP(100), EUR(100)));
        env.close();

        // Get account_lines for alice.  Limit at 1, so we get a marker.
        Json::Value linesBegParams;
        linesBegParams[jss::account] = alice.human();
        linesBegParams[jss::limit] = 2;
        auto const linesBeg = env.rpc("json", "account_lines", to_string(linesBegParams));
        BEAST_EXPECT(linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD");
        BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker));

        // alice pays 100 EUR to cheri.
        env(pay(alice, cheri, EUR(100)));
        env.close();

        // Since alice paid all her EUR to cheri, alice should no longer
        // have a trust line to gw1.  So the old marker should now be invalid.
        Json::Value linesEndParams;
        linesEndParams[jss::account] = alice.human();
        linesEndParams[jss::marker] = linesBeg[jss::result][jss::marker];
        auto const linesEnd = env.rpc("json", "account_lines", to_string(linesEndParams));
        BEAST_EXPECT(
            linesEnd[jss::result][jss::error_message] ==
            RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
    }

    void
    testAccountLinesWalkMarkers()
    {
        testcase("Marker can point to any appropriate ledger entry type");
        using namespace test::jtx;
        using namespace std::chrono_literals;
        Env env(*this);

        // The goal of this test is observe account_lines RPC calls return an
        // error message when the SLE pointed to by the marker is not owned by
        // the Account being traversed.
        //
        // To start, we'll create an environment with some trust lines, offers
        // and a signers list.
        Account const alice{"alice"};
        Account const becky{"becky"};
        Account const gw1{"gw1"};
        env.fund(XRP(10000), alice, becky, gw1);
        env.close();

        auto payChan = [](Account const& account,
                          Account const& to,
                          STAmount const& amount,
                          NetClock::duration const& settleDelay,
                          PublicKey const& pk) {
            Json::Value jv;
            jv[jss::TransactionType] = jss::PaymentChannelCreate;
            jv[jss::Account] = account.human();
            jv[jss::Destination] = to.human();
            jv[jss::Amount] = amount.getJson(JsonOptions::none);
            jv["SettleDelay"] = settleDelay.count();
            jv["PublicKey"] = strHex(pk.slice());
            return jv;
        };

        // Test all available object types. Not all of these objects will be
        // included in the search, nor found by `account_objects`. If that ever
        // changes for any reason, this test will help catch that.
        //
        // SignerList, for alice
        Account const bogie{"bogie"};
        env(signers(alice, 2, {{bogie, 3}}));
        env.close();

        // SignerList, includes alice
        env(signers(becky, 2, {{alice, 3}}));
        env.close();

        // Trust lines
        auto const EUR = gw1["EUR"];
        env(trust(alice, EUR(200)));
        env(trust(becky, EUR(200)));
        env.close();

        // Escrow, in each direction
        env(escrow::create(alice, becky, XRP(1000)), escrow::finish_time(env.now() + 1s));
        env(escrow::create(becky, alice, XRP(1000)), escrow::finish_time(env.now() + 1s));

        // Pay channels, in each direction
        env(payChan(alice, becky, XRP(1000), 100s, alice.pk()));
        env(payChan(becky, alice, XRP(1000), 100s, becky.pk()));

        // Mint NFTs, for each account
        uint256 const aliceNFtokenID = token::getNextID(env, alice, 0, tfTransferable);
        env(token::mint(alice, 0), txflags(tfTransferable));

        uint256 const beckyNFtokenID = token::getNextID(env, becky, 0, tfTransferable);
        env(token::mint(becky, 0), txflags(tfTransferable));

        // NFT Offers, for each other's NFTs
        env(token::createOffer(alice, beckyNFtokenID, drops(1)), token::owner(becky));
        env(token::createOffer(becky, aliceNFtokenID, drops(1)), token::owner(alice));

        env(token::createOffer(becky, beckyNFtokenID, drops(1)),
            txflags(tfSellNFToken),
            token::destination(alice));
        env(token::createOffer(alice, aliceNFtokenID, drops(1)),
            txflags(tfSellNFToken),
            token::destination(becky));

        env(token::createOffer(gw1, beckyNFtokenID, drops(1)),
            token::owner(becky),
            token::destination(alice));
        env(token::createOffer(gw1, aliceNFtokenID, drops(1)),
            token::owner(alice),
            token::destination(becky));

        env(token::createOffer(becky, beckyNFtokenID, drops(1)), txflags(tfSellNFToken));
        env(token::createOffer(alice, aliceNFtokenID, drops(1)), txflags(tfSellNFToken));

        // Checks, in each direction
        env(check::create(alice, becky, XRP(50)));
        env(check::create(becky, alice, XRP(50)));

        // Deposit preauth, in each direction
        env(deposit::auth(alice, becky));
        env(deposit::auth(becky, alice));

        // Offers, one where alice is the owner, and one where alice is the
        // issuer
        auto const USDalice = alice["USD"];
        env(offer(alice, EUR(10), XRP(100)));
        env(offer(becky, USDalice(10), XRP(100)));

        // Tickets
        env(ticket::create(alice, 2));

        // Add another trustline for good measure
        auto const BTCbecky = becky["BTC"];
        env(trust(alice, BTCbecky(200)));

        env.close();

        {
            // Now make repeated calls to `account_lines` with a limit of 1.
            // That should iterate all of alice's relevant objects, even though
            // the list will be empty for most calls.
            auto getNextLine =
                [](Env& env, Account const& alice, std::optional<std::string> const marker) {
                    Json::Value params(Json::objectValue);
                    params[jss::account] = alice.human();
                    params[jss::limit] = 1;
                    if (marker)
                        params[jss::marker] = *marker;

                    return env.rpc("json", "account_lines", to_string(params));
                };

            auto aliceLines = getNextLine(env, alice, std::nullopt);
            constexpr std::size_t expectedIterations = 16;
            constexpr std::size_t expectedLines = 2;
            constexpr std::size_t expectedNFTs = 1;
            std::size_t foundLines = 0;

            auto hasMarker = [](auto const& aliceLines) {
                return aliceLines[jss::result].isMember(jss::marker);
            };
            auto marker = [](auto const& aliceLines) {
                return aliceLines[jss::result][jss::marker].asString();
            };
            auto checkLines = [](auto const& aliceLines) {
                return aliceLines.isMember(jss::result) &&
                    !aliceLines[jss::result].isMember(jss::error_message) &&
                    aliceLines[jss::result].isMember(jss::lines) &&
                    aliceLines[jss::result][jss::lines].isArray() &&
                    aliceLines[jss::result][jss::lines].size() <= 1;
            };

            BEAST_EXPECT(hasMarker(aliceLines));
            BEAST_EXPECT(checkLines(aliceLines));
            BEAST_EXPECT(aliceLines[jss::result][jss::lines].size() == 0);

            int iterations = 1;

            while (hasMarker(aliceLines))
            {
                // Iterate through the markers
                aliceLines = getNextLine(env, alice, marker(aliceLines));
                BEAST_EXPECT(checkLines(aliceLines));
                foundLines += aliceLines[jss::result][jss::lines].size();
                ++iterations;
            }
            BEAST_EXPECT(expectedLines == foundLines);

            Json::Value aliceObjectsParams2;
            aliceObjectsParams2[jss::account] = alice.human();
            aliceObjectsParams2[jss::limit] = 200;
            Json::Value const aliceObjects =
                env.rpc("json", "account_objects", to_string(aliceObjectsParams2));
            BEAST_EXPECT(aliceObjects.isMember(jss::result));
            BEAST_EXPECT(!aliceObjects[jss::result].isMember(jss::error_message));
            BEAST_EXPECT(aliceObjects[jss::result].isMember(jss::account_objects));
            BEAST_EXPECT(aliceObjects[jss::result][jss::account_objects].isArray());
            // account_objects does not currently return NFTPages. If
            // that ever changes, without also changing account_lines,
            // this test will need to be updated.
            BEAST_EXPECT(
                aliceObjects[jss::result][jss::account_objects].size() ==
                iterations + expectedNFTs);
            // If ledger object association ever changes, for whatever
            // reason, this test will need to be updated.
            BEAST_EXPECTS(iterations == expectedIterations, std::to_string(iterations));

            // Get becky's objects just to confirm that they're symmetrical
            Json::Value beckyObjectsParams;
            beckyObjectsParams[jss::account] = becky.human();
            beckyObjectsParams[jss::limit] = 200;
            Json::Value const beckyObjects =
                env.rpc("json", "account_objects", to_string(beckyObjectsParams));
            BEAST_EXPECT(beckyObjects.isMember(jss::result));
            BEAST_EXPECT(!beckyObjects[jss::result].isMember(jss::error_message));
            BEAST_EXPECT(beckyObjects[jss::result].isMember(jss::account_objects));
            BEAST_EXPECT(beckyObjects[jss::result][jss::account_objects].isArray());
            // becky should have the same number of objects as alice, except the
            // 2 tickets that only alice created.
            BEAST_EXPECT(
                beckyObjects[jss::result][jss::account_objects].size() ==
                aliceObjects[jss::result][jss::account_objects].size() - 2);
        }
    }

    // test API V2
    void
    testAccountLines2()
    {
        testcase("V2: account_lines");

        using namespace test::jtx;
        Env env(*this);
        {
            // account_lines with mal-formed json2 (missing id field).
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            auto const lines = env.rpc("json2", to_string(request));
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
        }
        {
            // account_lines with no account.
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            request[jss::id] = 5;
            auto const lines = env.rpc("json2", to_string(request));
            BEAST_EXPECT(
                lines[jss::error][jss::message] ==
                RPC::missing_field_error(jss::account)[jss::error_message]);
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
        }
        {
            // account_lines with a malformed account.
            Json::Value params;
            params[jss::account] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            request[jss::id] = 5;
            request[jss::params] = params;
            auto const lines = env.rpc("json2", to_string(request));
            BEAST_EXPECT(
                lines[jss::error][jss::message] ==
                RPC::make_error(rpcACT_MALFORMED)[jss::error_message]);
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
        }
        Account const alice{"alice"};
        {
            // account_lines on an unfunded account.
            Json::Value params;
            params[jss::account] = alice.human();
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            request[jss::id] = 5;
            request[jss::params] = params;
            auto const lines = env.rpc("json2", to_string(request));
            BEAST_EXPECT(
                lines[jss::error][jss::message] ==
                RPC::make_error(rpcACT_NOT_FOUND)[jss::error_message]);
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
        }
        env.fund(XRP(10000), alice);
        env.close();
        LedgerHeader const ledger3Info = env.closed()->header();
        BEAST_EXPECT(ledger3Info.seq == 3);

        {
            // alice is funded but has no lines.  An empty array is returned.
            Json::Value params;
            params[jss::account] = alice.human();
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            request[jss::id] = 5;
            request[jss::params] = params;
            auto const lines = env.rpc("json2", to_string(request));
            BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
            BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0);
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
        }
        {
            // Specify a ledger that doesn't exist.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::ledger_index] = "nonsense";
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            request[jss::id] = 5;
            request[jss::params] = params;
            auto const lines = env.rpc("json2", to_string(request));
            BEAST_EXPECT(
                lines[jss::error][jss::message] ==
                "Invalid field 'ledger_index', not string or number.");
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
        }
        {
            // Specify a different ledger that doesn't exist.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::ledger_index] = 50000;
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            request[jss::id] = 5;
            request[jss::params] = params;
            auto const lines = env.rpc("json2", to_string(request));
            BEAST_EXPECT(lines[jss::error][jss::message] == "ledgerNotFound");
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
        }
        // Create trust lines to share with alice.
        Account const gw1{"gw1"};
        env.fund(XRP(10000), gw1);
        std::vector<IOU> gw1Currencies;

        for (char c = 0; c <= ('Z' - 'A'); ++c)
        {
            // gw1 currencies have names "YAA" -> "YAZ".
            gw1Currencies.push_back(gw1[std::string("YA") + static_cast<char>('A' + c)]);
            IOU const& gw1Currency = gw1Currencies.back();

            // Establish trust lines.
            env(trust(alice, gw1Currency(100 + c)));
            env(pay(gw1, alice, gw1Currency(50 + c)));
        }
        env.close();
        LedgerHeader const ledger4Info = env.closed()->header();
        BEAST_EXPECT(ledger4Info.seq == 4);

        // Add another set of trust lines in another ledger so we can see
        // differences in historic ledgers.
        Account const gw2{"gw2"};
        env.fund(XRP(10000), gw2);

        // gw2 requires authorization.
        env(fset(gw2, asfRequireAuth));
        env.close();
        std::vector<IOU> gw2Currencies;

        for (char c = 0; c <= ('Z' - 'A'); ++c)
        {
            // gw2 currencies have names "ZAA" -> "ZAZ".
            gw2Currencies.push_back(gw2[std::string("ZA") + static_cast<char>('A' + c)]);
            IOU const& gw2Currency = gw2Currencies.back();

            // Establish trust lines.
            env(trust(alice, gw2Currency(200 + c)));
            env(trust(gw2, gw2Currency(0), alice, tfSetfAuth));
            env.close();
            env(pay(gw2, alice, gw2Currency(100 + c)));
            env.close();

            // Set flags on gw2 trust lines so we can look for them.
            env(trust(alice, gw2Currency(0), gw2, tfSetNoRipple | tfSetFreeze | tfSetDeepFreeze));
        }
        env.close();
        LedgerHeader const ledger58Info = env.closed()->header();
        BEAST_EXPECT(ledger58Info.seq == 58);

        // A re-usable test for historic ledgers.
        auto testAccountLinesHistory = [this, &env](
                                           Account const& account,
                                           LedgerHeader const& info,
                                           int count) {
            // Get account_lines by ledger index.
            Json::Value paramsSeq;
            paramsSeq[jss::account] = account.human();
            paramsSeq[jss::ledger_index] = info.seq;
            Json::Value requestSeq;
            requestSeq[jss::method] = "account_lines";
            requestSeq[jss::jsonrpc] = "2.0";
            requestSeq[jss::ripplerpc] = "2.0";
            requestSeq[jss::id] = 5;
            requestSeq[jss::params] = paramsSeq;
            auto const linesSeq = env.rpc("json2", to_string(requestSeq));
            BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
            BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
            BEAST_EXPECT(linesSeq.isMember(jss::jsonrpc) && linesSeq[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(linesSeq.isMember(jss::ripplerpc) && linesSeq[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5);

            // Get account_lines by ledger hash.
            Json::Value paramsHash;
            paramsHash[jss::account] = account.human();
            paramsHash[jss::ledger_hash] = to_string(info.hash);
            Json::Value requestHash;
            requestHash[jss::method] = "account_lines";
            requestHash[jss::jsonrpc] = "2.0";
            requestHash[jss::ripplerpc] = "2.0";
            requestHash[jss::id] = 5;
            requestHash[jss::params] = paramsHash;
            auto const linesHash = env.rpc("json2", to_string(requestHash));
            BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
            BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
            BEAST_EXPECT(linesHash.isMember(jss::jsonrpc) && linesHash[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(linesHash.isMember(jss::ripplerpc) && linesHash[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(linesHash.isMember(jss::id) && linesHash[jss::id] == 5);
        };

        // Alice should have no trust lines in ledger 3.
        testAccountLinesHistory(alice, ledger3Info, 0);

        // Alice should have 26 trust lines in ledger 4.
        testAccountLinesHistory(alice, ledger4Info, 26);

        // Alice should have 52 trust lines in ledger 58.
        testAccountLinesHistory(alice, ledger58Info, 52);

        {
            // Surprisingly, it's valid to specify both index and hash, in
            // which case the hash wins.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::ledger_hash] = to_string(ledger4Info.hash);
            params[jss::ledger_index] = ledger58Info.seq;
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            request[jss::id] = 5;
            request[jss::params] = params;
            auto const lines = env.rpc("json2", to_string(request));
            BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
            BEAST_EXPECT(
                lines[jss::error][jss::message] ==
                "Exactly one of 'ledger_hash' or 'ledger_index' can be "
                "specified.");
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
        }
        {
            // alice should have 52 trust lines in the current ledger.
            Json::Value params;
            params[jss::account] = alice.human();
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            request[jss::id] = 5;
            request[jss::params] = params;
            auto const lines = env.rpc("json2", to_string(request));
            BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
            BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
        }
        {
            // alice should have 26 trust lines with gw1.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::peer] = gw1.human();
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            request[jss::id] = 5;
            request[jss::params] = params;
            auto const lines = env.rpc("json2", to_string(request));
            BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
            BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
        }
        {
            // Use a malformed peer.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::peer] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            request[jss::id] = 5;
            request[jss::params] = params;
            auto const lines = env.rpc("json2", to_string(request));
            BEAST_EXPECT(
                lines[jss::error][jss::message] ==
                RPC::make_error(rpcACT_MALFORMED)[jss::error_message]);
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
        }
        {
            // A negative limit should fail.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::limit] = -1;
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            request[jss::id] = 5;
            request[jss::params] = params;
            auto const lines = env.rpc("json2", to_string(request));
            BEAST_EXPECT(
                lines[jss::error][jss::message] ==
                RPC::expected_field_message(jss::limit, "unsigned integer"));
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
        }
        {
            // Limit the response to 1 trust line.
            Json::Value paramsA;
            paramsA[jss::account] = alice.human();
            paramsA[jss::limit] = 1;
            Json::Value requestA;
            requestA[jss::method] = "account_lines";
            requestA[jss::jsonrpc] = "2.0";
            requestA[jss::ripplerpc] = "2.0";
            requestA[jss::id] = 5;
            requestA[jss::params] = paramsA;
            auto const linesA = env.rpc("json2", to_string(requestA));
            BEAST_EXPECT(linesA[jss::result][jss::lines].isArray());
            BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1);
            BEAST_EXPECT(linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);

            // Pick up from where the marker left off.  We should get 51.
            auto marker = linesA[jss::result][jss::marker].asString();
            Json::Value paramsB;
            paramsB[jss::account] = alice.human();
            paramsB[jss::marker] = marker;
            Json::Value requestB;
            requestB[jss::method] = "account_lines";
            requestB[jss::jsonrpc] = "2.0";
            requestB[jss::ripplerpc] = "2.0";
            requestB[jss::id] = 5;
            requestB[jss::params] = paramsB;
            auto const linesB = env.rpc("json2", to_string(requestB));
            BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
            BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);
            BEAST_EXPECT(linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);

            // Go again from where the marker left off, but set a limit of 3.
            Json::Value paramsC;
            paramsC[jss::account] = alice.human();
            paramsC[jss::limit] = 3;
            paramsC[jss::marker] = marker;
            Json::Value requestC;
            requestC[jss::method] = "account_lines";
            requestC[jss::jsonrpc] = "2.0";
            requestC[jss::ripplerpc] = "2.0";
            requestC[jss::id] = 5;
            requestC[jss::params] = paramsC;
            auto const linesC = env.rpc("json2", to_string(requestC));
            BEAST_EXPECT(linesC[jss::result][jss::lines].isArray());
            BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3);
            BEAST_EXPECT(linesC.isMember(jss::jsonrpc) && linesC[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(linesC.isMember(jss::ripplerpc) && linesC[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(linesC.isMember(jss::id) && linesC[jss::id] == 5);

            // Mess with the marker so it becomes bad and check for the error.
            marker[5] = marker[5] == '7' ? '8' : '7';
            Json::Value paramsD;
            paramsD[jss::account] = alice.human();
            paramsD[jss::marker] = marker;
            Json::Value requestD;
            requestD[jss::method] = "account_lines";
            requestD[jss::jsonrpc] = "2.0";
            requestD[jss::ripplerpc] = "2.0";
            requestD[jss::id] = 5;
            requestD[jss::params] = paramsD;
            auto const linesD = env.rpc("json2", to_string(requestD));
            BEAST_EXPECT(
                linesD[jss::error][jss::message] ==
                RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
            BEAST_EXPECT(linesD.isMember(jss::jsonrpc) && linesD[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(linesD.isMember(jss::ripplerpc) && linesD[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(linesD.isMember(jss::id) && linesD[jss::id] == 5);
        }
        {
            // A non-string marker should also fail.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::marker] = true;
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            request[jss::id] = 5;
            request[jss::params] = params;
            auto const lines = env.rpc("json2", to_string(request));
            BEAST_EXPECT(
                lines[jss::error][jss::message] ==
                RPC::expected_field_message(jss::marker, "string"));
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
        }
        {
            // Check that the flags we expect from alice to gw2 are present.
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::limit] = 10;
            params[jss::peer] = gw2.human();
            Json::Value request;
            request[jss::method] = "account_lines";
            request[jss::jsonrpc] = "2.0";
            request[jss::ripplerpc] = "2.0";
            request[jss::id] = 5;
            request[jss::params] = params;
            auto const lines = env.rpc("json2", to_string(request));
            auto const& line = lines[jss::result][jss::lines][0u];
            BEAST_EXPECT(line[jss::freeze].asBool() == true);
            BEAST_EXPECT(line[jss::deep_freeze].asBool() == true);
            BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
            BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
            BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
        }
        {
            // Check that the flags we expect from gw2 to alice are present.
            Json::Value paramsA;
            paramsA[jss::account] = gw2.human();
            paramsA[jss::limit] = 1;
            paramsA[jss::peer] = alice.human();
            Json::Value requestA;
            requestA[jss::method] = "account_lines";
            requestA[jss::jsonrpc] = "2.0";
            requestA[jss::ripplerpc] = "2.0";
            requestA[jss::id] = 5;
            requestA[jss::params] = paramsA;
            auto const linesA = env.rpc("json2", to_string(requestA));
            auto const& lineA = linesA[jss::result][jss::lines][0u];
            BEAST_EXPECT(lineA[jss::freeze_peer].asBool() == true);
            BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true);
            BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
            BEAST_EXPECT(lineA[jss::authorized].asBool() == true);
            BEAST_EXPECT(linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);

            // Continue from the returned marker to make sure that works.
            BEAST_EXPECT(linesA[jss::result].isMember(jss::marker));
            auto const marker = linesA[jss::result][jss::marker].asString();
            Json::Value paramsB;
            paramsB[jss::account] = gw2.human();
            paramsB[jss::limit] = 25;
            paramsB[jss::marker] = marker;
            paramsB[jss::peer] = alice.human();
            Json::Value requestB;
            requestB[jss::method] = "account_lines";
            requestB[jss::jsonrpc] = "2.0";
            requestB[jss::ripplerpc] = "2.0";
            requestB[jss::id] = 5;
            requestB[jss::params] = paramsB;
            auto const linesB = env.rpc("json2", to_string(requestB));
            BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
            BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25);
            BEAST_EXPECT(!linesB[jss::result].isMember(jss::marker));
            BEAST_EXPECT(linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
        }
    }

    // test API V2
    void
    testAccountLineDelete2()
    {
        testcase("V2: account_lines with removed marker");

        using namespace test::jtx;
        Env env(*this);

        // The goal here is to observe account_lines marker behavior if the
        // entry pointed at by a returned marker is removed from the ledger.
        //
        // It isn't easy to explicitly delete a trust line, so we do so in a
        // round-about fashion.  It takes 4 actors:
        //   o Gateway gw1 issues EUR
        //   o alice offers to buy 100 EUR for 100 XRP.
        //   o becky offers to sell 100 EUR for 100 XRP.
        // There will now be an inferred trustline between alice and gw2.
        //   o alice pays her 100 EUR to cheri.
        // alice should now have no EUR and no trustline to gw2.
        Account const alice{"alice"};
        Account const becky{"becky"};
        Account const cheri{"cheri"};
        Account const gw1{"gw1"};
        Account const gw2{"gw2"};
        env.fund(XRP(10000), alice, becky, cheri, gw1, gw2);
        env.close();

        auto const USD = gw1["USD"];
        auto const AUD = gw1["AUD"];
        auto const EUR = gw2["EUR"];
        env(trust(alice, USD(200)));
        env(trust(alice, AUD(200)));
        env(trust(becky, EUR(200)));
        env(trust(cheri, EUR(200)));
        env.close();

        // becky gets 100 EUR from gw1.
        env(pay(gw2, becky, EUR(100)));
        env.close();

        // alice offers to buy 100 EUR for 100 XRP.
        env(offer(alice, EUR(100), XRP(100)));
        env.close();

        // becky offers to buy 100 XRP for 100 EUR.
        env(offer(becky, XRP(100), EUR(100)));
        env.close();

        // Get account_lines for alice.  Limit at 1, so we get a marker.
        Json::Value linesBegParams;
        linesBegParams[jss::account] = alice.human();
        linesBegParams[jss::limit] = 2;
        Json::Value linesBegRequest;
        linesBegRequest[jss::method] = "account_lines";
        linesBegRequest[jss::jsonrpc] = "2.0";
        linesBegRequest[jss::ripplerpc] = "2.0";
        linesBegRequest[jss::id] = 5;
        linesBegRequest[jss::params] = linesBegParams;
        auto const linesBeg = env.rpc("json2", to_string(linesBegRequest));
        BEAST_EXPECT(linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD");
        BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker));
        BEAST_EXPECT(linesBeg.isMember(jss::jsonrpc) && linesBeg[jss::jsonrpc] == "2.0");
        BEAST_EXPECT(linesBeg.isMember(jss::ripplerpc) && linesBeg[jss::ripplerpc] == "2.0");
        BEAST_EXPECT(linesBeg.isMember(jss::id) && linesBeg[jss::id] == 5);

        // alice pays 100 USD to cheri.
        env(pay(alice, cheri, EUR(100)));
        env.close();

        // Since alice paid all her EUR to cheri, alice should no longer
        // have a trust line to gw1.  So the old marker should now be invalid.
        Json::Value linesEndParams;
        linesEndParams[jss::account] = alice.human();
        linesEndParams[jss::marker] = linesBeg[jss::result][jss::marker];
        Json::Value linesEndRequest;
        linesEndRequest[jss::method] = "account_lines";
        linesEndRequest[jss::jsonrpc] = "2.0";
        linesEndRequest[jss::ripplerpc] = "2.0";
        linesEndRequest[jss::id] = 5;
        linesEndRequest[jss::params] = linesEndParams;
        auto const linesEnd = env.rpc("json2", to_string(linesEndRequest));
        BEAST_EXPECT(
            linesEnd[jss::error][jss::message] ==
            RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
        BEAST_EXPECT(linesEnd.isMember(jss::jsonrpc) && linesEnd[jss::jsonrpc] == "2.0");
        BEAST_EXPECT(linesEnd.isMember(jss::ripplerpc) && linesEnd[jss::ripplerpc] == "2.0");
        BEAST_EXPECT(linesEnd.isMember(jss::id) && linesEnd[jss::id] == 5);
    }

    void
    run() override
    {
        testAccountLines();
        testAccountLinesMarker();
        testAccountLineDelete();
        testAccountLinesWalkMarkers();
        testAccountLines2();
        testAccountLineDelete2();
    }
};

BEAST_DEFINE_TESTSUITE(AccountLines, rpc, xrpl);

}  // namespace RPC
}  // namespace xrpl
