#include <test/jtx.h>
#include <test/jtx/PathSet.h>
#include <test/jtx/WSClient.h>

#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/jss.h>

namespace xrpl {
namespace test {

class OfferBaseUtil_test : public beast::unit_test::suite
{
    static XRPAmount
    reserve(jtx::Env& env, std::uint32_t count)
    {
        return env.current()->fees().accountReserve(count);
    }

    static std::uint32_t
    lastClose(jtx::Env& env)
    {
        return env.current()->header().parentCloseTime.time_since_epoch().count();
    }

    static auto
    ledgerEntryOffer(jtx::Env& env, jtx::Account const& acct, std::uint32_t offer_seq)
    {
        Json::Value jvParams;
        jvParams[jss::offer][jss::account] = acct.human();
        jvParams[jss::offer][jss::seq] = offer_seq;
        return env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
    }

    static auto
    getBookOffers(jtx::Env& env, Issue const& taker_pays, Issue const& taker_gets)
    {
        Json::Value jvbp;
        jvbp[jss::ledger_index] = "current";
        jvbp[jss::taker_pays][jss::currency] = to_string(taker_pays.currency);
        jvbp[jss::taker_pays][jss::issuer] = to_string(taker_pays.account);
        jvbp[jss::taker_gets][jss::currency] = to_string(taker_gets.currency);
        jvbp[jss::taker_gets][jss::issuer] = to_string(taker_gets.account);
        return env.rpc("json", "book_offers", to_string(jvbp))[jss::result];
    }

public:
    void
    testRmFundedOffer(FeatureBitset features)
    {
        testcase("Incorrect Removal of Funded Offers");

        // We need at least two paths. One at good quality and one at bad
        // quality.  The bad quality path needs two offer books in a row.
        // Each offer book should have two offers at the same quality, the
        // offers should be completely consumed, and the payment should
        // should require both offers to be satisfied. The first offer must
        // be "taker gets" XRP. Old, broken would remove the first
        // "taker gets" xrp offer, even though the offer is still funded and
        // not used for the payment.

        using namespace jtx;
        Env env{*this, features};

        auto const gw = Account{"gateway"};
        auto const USD = gw["USD"];
        auto const BTC = gw["BTC"];
        Account const alice{"alice"};
        Account const bob{"bob"};
        Account const carol{"carol"};

        env.fund(XRP(10000), alice, bob, carol, gw);
        env.close();
        env.trust(USD(1000), alice, bob, carol);
        env.trust(BTC(1000), alice, bob, carol);

        env(pay(gw, alice, BTC(1000)));

        env(pay(gw, carol, USD(1000)));
        env(pay(gw, carol, BTC(1000)));

        // Must be two offers at the same quality
        // "taker gets" must be XRP
        // (Different amounts so I can distinguish the offers)
        env(offer(carol, BTC(49), XRP(49)));
        env(offer(carol, BTC(51), XRP(51)));

        // Offers for the poor quality path
        // Must be two offers at the same quality
        env(offer(carol, XRP(50), USD(50)));
        env(offer(carol, XRP(50), USD(50)));

        // Offers for the good quality path
        env(offer(carol, BTC(1), USD(100)));

        PathSet paths(Path(XRP, USD), Path(USD));

        env(pay(alice, bob, USD(100)),
            json(paths.json()),
            sendmax(BTC(1000)),
            txflags(tfPartialPayment));

        env.require(balance(bob, USD(100)));
        BEAST_EXPECT(
            !isOffer(env, carol, BTC(1), USD(100)) && isOffer(env, carol, BTC(49), XRP(49)));
    }

    void
    testCanceledOffer(FeatureBitset features)
    {
        testcase("Removing Canceled Offers");

        using namespace jtx;
        Env env{*this, features};

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const USD = gw["USD"];

        env.fund(XRP(10000), alice, gw);
        env.close();
        env.trust(USD(100), alice);
        env.close();

        env(pay(gw, alice, USD(50)));
        env.close();

        auto const offer1Seq = env.seq(alice);

        env(offer(alice, XRP(500), USD(100)), require(offers(alice, 1)));
        env.close();

        BEAST_EXPECT(isOffer(env, alice, XRP(500), USD(100)));

        // cancel the offer above and replace it with a new offer
        auto const offer2Seq = env.seq(alice);

        env(offer(alice, XRP(300), USD(100)),
            json(jss::OfferSequence, offer1Seq),
            require(offers(alice, 1)));
        env.close();

        BEAST_EXPECT(
            isOffer(env, alice, XRP(300), USD(100)) && !isOffer(env, alice, XRP(500), USD(100)));

        // Test canceling non-existent offer.
        //      auto const offer3Seq = env.seq (alice);

        env(offer(alice, XRP(400), USD(200)),
            json(jss::OfferSequence, offer1Seq),
            require(offers(alice, 2)));
        env.close();

        BEAST_EXPECT(
            isOffer(env, alice, XRP(300), USD(100)) && isOffer(env, alice, XRP(400), USD(200)));

        // Test cancellation now with OfferCancel tx
        auto const offer4Seq = env.seq(alice);
        env(offer(alice, XRP(222), USD(111)), require(offers(alice, 3)));
        env.close();

        BEAST_EXPECT(isOffer(env, alice, XRP(222), USD(111)));
        env(offer_cancel(alice, offer4Seq));
        env.close();
        BEAST_EXPECT(env.seq(alice) == offer4Seq + 2);

        BEAST_EXPECT(!isOffer(env, alice, XRP(222), USD(111)));

        // Create an offer that both fails with a tecEXPIRED code and removes
        // an offer.  Show that the attempt to remove the offer fails.
        env.require(offers(alice, 2));

        env(offer(alice, XRP(5), USD(2)),
            json(sfExpiration.fieldName, lastClose(env)),
            json(jss::OfferSequence, offer2Seq),
            ter(tecEXPIRED));
        env.close();

        env.require(offers(alice, 2));
        BEAST_EXPECT(isOffer(env, alice, XRP(300), USD(100)));  // offer2
        BEAST_EXPECT(!isOffer(env, alice, XRP(5), USD(2)));     // expired
    }

    void
    testTinyPayment(FeatureBitset features)
    {
        testcase("Tiny payments");

        // Regression test for tiny payments
        using namespace jtx;
        using namespace std::chrono_literals;
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const carol = Account{"carol"};
        auto const gw = Account{"gw"};

        auto const USD = gw["USD"];
        auto const EUR = gw["EUR"];

        Env env{*this, features};

        env.fund(XRP(10000), alice, bob, carol, gw);
        env.close();
        env.trust(USD(1000), alice, bob, carol);
        env.trust(EUR(1000), alice, bob, carol);
        env(pay(gw, alice, USD(100)));
        env(pay(gw, carol, EUR(100)));

        // Create more offers than the loop max count in DeliverNodeReverse
        // Note: the DeliverNodeReverse code has been removed; however since
        // this is a regression test the original test is being left as-is for
        // now.
        for (int i = 0; i < 101; ++i)
            env(offer(carol, USD(1), EUR(2)));

        env(pay(alice, bob, EUR(epsilon)), path(~EUR), sendmax(USD(100)));
    }

    void
    testXRPTinyPayment(FeatureBitset features)
    {
        testcase("XRP Tiny payments");

        // Regression test for tiny xrp payments
        // In some cases, when the payment code calculates
        // the amount of xrp needed as input to an xrp->iou offer
        // it would incorrectly round the amount to zero (even when
        // round-up was set to true).
        // The bug would cause funded offers to be incorrectly removed
        // because the code thought they were unfunded.
        // The conditions to trigger the bug are:
        // 1) When we calculate the amount of input xrp needed for an offer
        //    from xrp->iou, the amount is less than 1 drop (after rounding
        //    up the float representation).
        // 2) There is another offer in the same book with a quality
        //    sufficiently bad that when calculating the input amount
        //    needed the amount is not set to zero.

        using namespace jtx;
        using namespace std::chrono_literals;
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const carol = Account{"carol"};
        auto const dan = Account{"dan"};
        auto const erin = Account{"erin"};
        auto const gw = Account{"gw"};

        auto const USD = gw["USD"];
        Env env{*this, features};

        env.fund(XRP(10000), alice, bob, carol, dan, erin, gw);
        env.close();
        env.trust(USD(1000), alice, bob, carol, dan, erin);
        env.close();
        env(pay(gw, carol, USD(0.99999)));
        env(pay(gw, dan, USD(1)));
        env(pay(gw, erin, USD(1)));
        env.close();

        // Carol doesn't quite have enough funds for this offer
        // The amount left after this offer is taken will cause
        // STAmount to incorrectly round to zero when the next offer
        // (at a good quality) is considered. (when the now removed
        // stAmountCalcSwitchover2 patch was inactive)
        env(offer(carol, drops(1), USD(0.99999)));
        // Offer at a quality poor enough so when the input xrp is
        // calculated  in the reverse pass, the amount is not zero.
        env(offer(dan, XRP(100), USD(1)));

        env.close();
        // This is the funded offer that will be incorrectly removed.
        // It is considered after the offer from carol, which leaves a
        // tiny amount left to pay. When calculating the amount of xrp
        // needed for this offer, it will incorrectly compute zero in both
        // the forward and reverse passes (when the now removed
        // stAmountCalcSwitchover2 was inactive.)
        env(offer(erin, drops(2), USD(1)));

        env(pay(alice, bob, USD(1)),
            path(~USD),
            sendmax(XRP(102)),
            txflags(tfNoRippleDirect | tfPartialPayment));

        env.require(offers(carol, 0), offers(dan, 1));

        // offer was correctly consumed. There is still some
        // liquidity left on that offer.
        env.require(balance(erin, USD(0.99999)), offers(erin, 1));
    }

    void
    testRmSmallIncreasedQOffersXRP(FeatureBitset features)
    {
        testcase("Rm small increased q offers XRP");

        // Carol places an offer, but cannot fully fund the offer. When her
        // funding is taken into account, the offer's quality drops below its
        // initial quality and has an input amount of 1 drop. This is removed as
        // an offer that may block offer books.

        using namespace jtx;
        using namespace std::chrono_literals;
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const carol = Account{"carol"};
        auto const gw = Account{"gw"};

        auto const USD = gw["USD"];

        // Test offer crossing
        for (auto crossBothOffers : {false, true})
        {
            Env env{*this, features};

            env.fund(XRP(10000), alice, bob, carol, gw);
            env.close();
            env.trust(USD(1000), alice, bob, carol);
            // underfund carol's offer
            auto initialCarolUSD = USD(0.499);
            env(pay(gw, carol, initialCarolUSD));
            env(pay(gw, bob, USD(100)));
            env.close();
            // This offer is underfunded
            env(offer(carol, drops(1), USD(1)));
            env.close();
            // offer at a lower quality
            env(offer(bob, drops(2), USD(1), tfPassive));
            env.close();
            env.require(offers(bob, 1), offers(carol, 1));

            // alice places an offer that crosses carol's; depending on
            // "crossBothOffers" it may cross bob's as well
            auto aliceTakerGets = crossBothOffers ? drops(2) : drops(1);
            env(offer(alice, USD(1), aliceTakerGets));
            env.close();

            env.require(
                offers(carol, 0),
                balance(
                    carol,
                    initialCarolUSD));  // offer is removed but not taken
            if (crossBothOffers)
            {
                env.require(offers(alice, 0), balance(alice, USD(1)));  // alice's offer is crossed
            }
            else
            {
                env.require(
                    offers(alice, 1), balance(alice, USD(0)));  // alice's offer is not crossed
            }
        }

        // Test payments
        for (auto partialPayment : {false, true})
        {
            Env env{*this, features};

            env.fund(XRP(10000), alice, bob, carol, gw);
            env.close();
            env.trust(USD(1000), alice, bob, carol);
            env.close();
            auto const initialCarolUSD = USD(0.999);
            env(pay(gw, carol, initialCarolUSD));
            env.close();
            env(pay(gw, bob, USD(100)));
            env.close();
            env(offer(carol, drops(1), USD(1)));
            env.close();
            env(offer(bob, drops(2), USD(2), tfPassive));
            env.close();
            env.require(offers(bob, 1), offers(carol, 1));

            std::uint32_t const flags =
                partialPayment ? (tfNoRippleDirect | tfPartialPayment) : tfNoRippleDirect;

            TER const expectedTer = partialPayment ? TER{tesSUCCESS} : TER{tecPATH_PARTIAL};

            env(pay(alice, bob, USD(5)),
                path(~USD),
                sendmax(XRP(1)),
                txflags(flags),
                ter(expectedTer));
            env.close();

            if (isTesSuccess(expectedTer))
            {
                env.require(offers(carol, 0));
                env.require(balance(carol,
                                    initialCarolUSD));  // offer is removed but not taken
            }
            else
            {
                // TODO: Offers are not removed when payments fail
                // If that is addressed, the test should show that carol's
                // offer is removed but not taken, as in the other branch of
                // this if statement
            }
        }
    }

    void
    testRmSmallIncreasedQOffersIOU(FeatureBitset features)
    {
        testcase("Rm small increased q offers IOU");

        // Carol places an offer, but cannot fully fund the offer. When her
        // funding is taken into account, the offer's quality drops below its
        // initial quality and has an input amount of 1 drop. This is removed as
        // an offer that may block offer books.

        using namespace jtx;
        using namespace std::chrono_literals;
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const carol = Account{"carol"};
        auto const gw = Account{"gw"};

        auto const USD = gw["USD"];
        auto const EUR = gw["EUR"];

        auto tinyAmount = [&](IOU const& iou) -> PrettyAmount {
            STAmount amt(
                iou.issue(),
                /*mantissa*/ 1,
                /*exponent*/ -81);
            return PrettyAmount(amt, iou.account.name());
        };

        // Test offer crossing
        for (auto crossBothOffers : {false, true})
        {
            Env env{*this, features};

            env.fund(XRP(10000), alice, bob, carol, gw);
            env.close();
            env.trust(USD(1000), alice, bob, carol);
            env.trust(EUR(1000), alice, bob, carol);
            // underfund carol's offer
            auto initialCarolUSD = tinyAmount(USD);
            env(pay(gw, carol, initialCarolUSD));
            env(pay(gw, bob, USD(100)));
            env(pay(gw, alice, EUR(100)));
            env.close();
            // This offer is underfunded
            env(offer(carol, EUR(1), USD(10)));
            env.close();
            // offer at a lower quality
            env(offer(bob, EUR(1), USD(5), tfPassive));
            env.close();
            env.require(offers(bob, 1), offers(carol, 1));

            // alice places an offer that crosses carol's; depending on
            // "crossBothOffers" it may cross bob's as well
            // Whatever
            auto aliceTakerGets = crossBothOffers ? EUR(0.2) : EUR(0.1);
            env(offer(alice, USD(1), aliceTakerGets));
            env.close();

            env.require(
                offers(carol, 0),
                balance(
                    carol,
                    initialCarolUSD));  // offer is removed but not taken
            if (crossBothOffers)
            {
                env.require(offers(alice, 0), balance(alice, USD(1)));  // alice's offer is crossed
            }
            else
            {
                env.require(
                    offers(alice, 1), balance(alice, USD(0)));  // alice's offer is not crossed
            }
        }

        // Test payments
        for (auto partialPayment : {false, true})
        {
            Env env{*this, features};

            env.fund(XRP(10000), alice, bob, carol, gw);
            env.close();
            env.trust(USD(1000), alice, bob, carol);
            env.trust(EUR(1000), alice, bob, carol);
            env.close();
            // underfund carol's offer
            auto const initialCarolUSD = tinyAmount(USD);
            env(pay(gw, carol, initialCarolUSD));
            env(pay(gw, bob, USD(100)));
            env(pay(gw, alice, EUR(100)));
            env.close();
            // This offer is underfunded
            env(offer(carol, EUR(1), USD(2)));
            env.close();
            env(offer(bob, EUR(2), USD(4), tfPassive));
            env.close();
            env.require(offers(bob, 1), offers(carol, 1));

            std::uint32_t const flags =
                partialPayment ? (tfNoRippleDirect | tfPartialPayment) : tfNoRippleDirect;

            TER const expectedTer = partialPayment ? TER{tesSUCCESS} : TER{tecPATH_PARTIAL};

            env(pay(alice, bob, USD(5)),
                path(~USD),
                sendmax(EUR(10)),
                txflags(flags),
                ter(expectedTer));
            env.close();

            if (isTesSuccess(expectedTer))
            {
                env.require(offers(carol, 0));
                env.require(balance(carol,
                                    initialCarolUSD));  // offer is removed but not taken
            }
            else
            {
                // TODO: Offers are not removed when payments fail
                // If that is addressed, the test should show that carol's
                // offer is removed but not taken, as in the other branch of
                // this if statement
            }
        }
    }

    void
    testEnforceNoRipple(FeatureBitset features)
    {
        testcase("Enforce No Ripple");

        using namespace jtx;

        auto const gw = Account{"gateway"};
        auto const USD = gw["USD"];
        auto const BTC = gw["BTC"];
        auto const EUR = gw["EUR"];
        Account const alice{"alice"};
        Account const bob{"bob"};
        Account const carol{"carol"};
        Account const dan{"dan"};

        {
            // No ripple with an implied account step after an offer
            Env env{*this, features};

            auto const gw1 = Account{"gw1"};
            auto const USD1 = gw1["USD"];
            auto const gw2 = Account{"gw2"};
            auto const USD2 = gw2["USD"];

            env.fund(XRP(10000), alice, noripple(bob), carol, dan, gw1, gw2);
            env.close();
            env.trust(USD1(1000), alice, carol, dan);
            env(trust(bob, USD1(1000), tfSetNoRipple));
            env.trust(USD2(1000), alice, carol, dan);
            env(trust(bob, USD2(1000), tfSetNoRipple));

            env(pay(gw1, dan, USD1(50)));
            env(pay(gw1, bob, USD1(50)));
            env(pay(gw2, bob, USD2(50)));

            env(offer(dan, XRP(50), USD1(50)));

            env(pay(alice, carol, USD2(50)),
                path(~USD1, bob),
                sendmax(XRP(50)),
                txflags(tfNoRippleDirect),
                ter(tecPATH_DRY));
        }
        {
            // Make sure payment works with default flags
            Env env{*this, features};

            auto const gw1 = Account{"gw1"};
            auto const USD1 = gw1["USD"];
            auto const gw2 = Account{"gw2"};
            auto const USD2 = gw2["USD"];

            env.fund(XRP(10000), alice, bob, carol, dan, gw1, gw2);
            env.close();
            env.trust(USD1(1000), alice, bob, carol, dan);
            env.trust(USD2(1000), alice, bob, carol, dan);

            env(pay(gw1, dan, USD1(50)));
            env(pay(gw1, bob, USD1(50)));
            env(pay(gw2, bob, USD2(50)));

            env(offer(dan, XRP(50), USD1(50)));

            env(pay(alice, carol, USD2(50)),
                path(~USD1, bob),
                sendmax(XRP(50)),
                txflags(tfNoRippleDirect));

            env.require(balance(alice, xrpMinusFee(env, 10000 - 50)));
            env.require(balance(bob, USD1(100)));
            env.require(balance(bob, USD2(0)));
            env.require(balance(carol, USD2(50)));
        }
    }

    void
    testInsufficientReserve(FeatureBitset features)
    {
        testcase("Insufficient Reserve");

        // If an account places an offer and its balance
        // *before* the transaction began isn't high enough
        // to meet the reserve *after* the transaction runs,
        // then no offer should go on the books but if the
        // offer partially or fully crossed the tx succeeds.

        using namespace jtx;

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const carol = Account{"carol"};
        auto const USD = gw["USD"];

        auto const usdOffer = USD(1000);
        auto const xrpOffer = XRP(1000);

        // No crossing:
        {
            Env env{*this, features};

            env.fund(XRP(1000000), gw);

            auto const f = env.current()->fees().base;
            auto const r = reserve(env, 0);

            env.fund(r + f, alice);

            env(trust(alice, usdOffer), ter(tesSUCCESS));
            env(pay(gw, alice, usdOffer), ter(tesSUCCESS));
            env(offer(alice, xrpOffer, usdOffer), ter(tecINSUF_RESERVE_OFFER));

            env.require(balance(alice, r - f), owners(alice, 1));
        }

        // Partial cross:
        {
            Env env{*this, features};

            env.fund(XRP(1000000), gw);

            auto const f = env.current()->fees().base;
            auto const r = reserve(env, 0);

            auto const usdOffer2 = USD(500);
            auto const xrpOffer2 = XRP(500);

            env.fund(r + f + xrpOffer, bob);

            env(offer(bob, usdOffer2, xrpOffer2), ter(tesSUCCESS));
            env.fund(r + f, alice);

            env(trust(alice, usdOffer), ter(tesSUCCESS));
            env(pay(gw, alice, usdOffer), ter(tesSUCCESS));
            env(offer(alice, xrpOffer, usdOffer), ter(tesSUCCESS));

            env.require(
                balance(alice, r - f + xrpOffer2),
                balance(alice, usdOffer2),
                owners(alice, 1),
                balance(bob, r + xrpOffer2),
                balance(bob, usdOffer2),
                owners(bob, 1));
        }

        // Account has enough reserve as is, but not enough
        // if an offer were added. Attempt to sell IOUs to
        // buy XRP. If it fully crosses, we succeed.
        {
            Env env{*this, features};

            env.fund(XRP(1000000), gw);

            auto const f = env.current()->fees().base;
            auto const r = reserve(env, 0);

            auto const usdOffer2 = USD(500);
            auto const xrpOffer2 = XRP(500);

            env.fund(r + f + xrpOffer, bob, carol);

            env(offer(bob, usdOffer2, xrpOffer2), ter(tesSUCCESS));
            env(offer(carol, usdOffer, xrpOffer), ter(tesSUCCESS));

            env.fund(r + f, alice);

            env(trust(alice, usdOffer), ter(tesSUCCESS));
            env(pay(gw, alice, usdOffer), ter(tesSUCCESS));
            env(offer(alice, xrpOffer, usdOffer), ter(tesSUCCESS));

            env.require(
                balance(alice, r - f + xrpOffer),
                balance(alice, USD(0)),
                owners(alice, 1),
                balance(bob, r + xrpOffer2),
                balance(bob, usdOffer2),
                owners(bob, 1),
                balance(carol, r + xrpOffer2),
                balance(carol, usdOffer2),
                owners(carol, 2));
        }
    }

    // Helper function that returns the Offers on an account.
    static std::vector<std::shared_ptr<SLE const>>
    offersOnAccount(jtx::Env& env, jtx::Account account)
    {
        std::vector<std::shared_ptr<SLE const>> result;
        forEachItem(*env.current(), account, [&result](std::shared_ptr<SLE const> const& sle) {
            if (sle->getType() == ltOFFER)
                result.push_back(sle);
        });
        return result;
    }

    void
    testFillModes(FeatureBitset features)
    {
        testcase("Fill Modes");

        using namespace jtx;

        auto const startBalance = XRP(1000000);
        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const USD = gw["USD"];

        // Fill or Kill - unless we fully cross, just charge a fee and don't
        // place the offer on the books.  But also clean up expired offers
        // that are discovered along the way.
        {
            Env env{*this, features};

            auto const f = env.current()->fees().base;

            env.fund(startBalance, gw, alice, bob);
            env.close();

            // bob creates an offer that expires before the next ledger close.
            env(offer(bob, USD(500), XRP(500)),
                json(sfExpiration.fieldName, lastClose(env) + 1),
                ter(tesSUCCESS));

            // The offer expires (it's not removed yet).
            env.close();
            env.require(owners(bob, 1), offers(bob, 1));

            // bob creates the offer that will be crossed.
            env(offer(bob, USD(500), XRP(500)), ter(tesSUCCESS));
            env.close();
            env.require(owners(bob, 2), offers(bob, 2));

            env(trust(alice, USD(1000)), ter(tesSUCCESS));
            env(pay(gw, alice, USD(1000)), ter(tesSUCCESS));

            // Order that can't be filled but will remove bob's expired offer:
            {
                TER const killedCode{TER{tecKILLED}};
                env(offer(alice, XRP(1000), USD(1000)), txflags(tfFillOrKill), ter(killedCode));
            }
            env.require(
                balance(alice, startBalance - (f * 2)),
                balance(alice, USD(1000)),
                owners(alice, 1),
                offers(alice, 0),
                balance(bob, startBalance - (f * 2)),
                balance(bob, USD(none)),
                owners(bob, 1),
                offers(bob, 1));

            // Order that can be filled
            env(offer(alice, XRP(500), USD(500)), txflags(tfFillOrKill), ter(tesSUCCESS));

            env.require(
                balance(alice, startBalance - (f * 3) + XRP(500)),
                balance(alice, USD(500)),
                owners(alice, 1),
                offers(alice, 0),
                balance(bob, startBalance - (f * 2) - XRP(500)),
                balance(bob, USD(500)),
                owners(bob, 1),
                offers(bob, 0));
        }

        // Immediate or Cancel - cross as much as possible
        // and add nothing on the books:
        {
            Env env{*this, features};

            auto const f = env.current()->fees().base;

            env.fund(startBalance, gw, alice, bob);
            env.close();

            env(trust(alice, USD(1000)), ter(tesSUCCESS));
            env(pay(gw, alice, USD(1000)), ter(tesSUCCESS));

            // No cross:
            {
                TER const expectedCode = tecKILLED;
                env(offer(alice, XRP(1000), USD(1000)),
                    txflags(tfImmediateOrCancel),
                    ter(expectedCode));
            }

            env.require(
                balance(alice, startBalance - f - f),
                balance(alice, USD(1000)),
                owners(alice, 1),
                offers(alice, 0));

            // Partially cross:
            env(offer(bob, USD(50), XRP(50)), ter(tesSUCCESS));
            env(offer(alice, XRP(1000), USD(1000)), txflags(tfImmediateOrCancel), ter(tesSUCCESS));

            env.require(
                balance(alice, startBalance - f - f - f + XRP(50)),
                balance(alice, USD(950)),
                owners(alice, 1),
                offers(alice, 0),
                balance(bob, startBalance - f - XRP(50)),
                balance(bob, USD(50)),
                owners(bob, 1),
                offers(bob, 0));

            // Fully cross:
            env(offer(bob, USD(50), XRP(50)), ter(tesSUCCESS));
            env(offer(alice, XRP(50), USD(50)), txflags(tfImmediateOrCancel), ter(tesSUCCESS));

            env.require(
                balance(alice, startBalance - f - f - f - f + XRP(100)),
                balance(alice, USD(900)),
                owners(alice, 1),
                offers(alice, 0),
                balance(bob, startBalance - f - f - XRP(100)),
                balance(bob, USD(100)),
                owners(bob, 1),
                offers(bob, 0));
        }

        // tfPassive -- place the offer without crossing it.
        {
            Env env(*this, features);

            env.fund(startBalance, gw, alice, bob);
            env.close();

            env(trust(bob, USD(1000)));
            env.close();

            env(pay(gw, bob, USD(1000)));
            env.close();

            env(offer(alice, USD(1000), XRP(2000)));
            env.close();

            auto const aliceOffers = offersOnAccount(env, alice);
            BEAST_EXPECT(aliceOffers.size() == 1);
            for (auto offerPtr : aliceOffers)
            {
                auto const& offer = *offerPtr;
                BEAST_EXPECT(offer[sfTakerGets] == XRP(2000));
                BEAST_EXPECT(offer[sfTakerPays] == USD(1000));
            }

            // bob creates a passive offer that could cross alice's.
            // bob's offer should stay in the ledger.
            env(offer(bob, XRP(2000), USD(1000), tfPassive));
            env.close();
            env.require(offers(alice, 1));

            auto const bobOffers = offersOnAccount(env, bob);
            BEAST_EXPECT(bobOffers.size() == 1);
            for (auto offerPtr : bobOffers)
            {
                auto const& offer = *offerPtr;
                BEAST_EXPECT(offer[sfTakerGets] == USD(1000));
                BEAST_EXPECT(offer[sfTakerPays] == XRP(2000));
            }

            // It should be possible for gw to cross both of those offers.
            env(offer(gw, XRP(2000), USD(1000)));
            env.close();
            env.require(offers(alice, 0));
            env.require(offers(gw, 0));
            env.require(offers(bob, 1));

            env(offer(gw, USD(1000), XRP(2000)));
            env.close();
            env.require(offers(bob, 0));
            env.require(offers(gw, 0));
        }

        // tfPassive -- cross only offers of better quality.
        {
            Env env(*this, features);

            env.fund(startBalance, gw, "alice", "bob");
            env.close();

            env(trust("bob", USD(1000)));
            env.close();

            env(pay(gw, "bob", USD(1000)));
            env(offer("alice", USD(500), XRP(1001)));
            env.close();

            env(offer("alice", USD(500), XRP(1000)));
            env.close();

            auto const aliceOffers = offersOnAccount(env, "alice");
            BEAST_EXPECT(aliceOffers.size() == 2);

            // bob creates a passive offer.  That offer should cross one
            // of alice's (the one with better quality) and leave alice's
            // other offer untouched.
            env(offer("bob", XRP(2000), USD(1000), tfPassive));
            env.close();
            env.require(offers("alice", 1));

            auto const bobOffers = offersOnAccount(env, "bob");
            BEAST_EXPECT(bobOffers.size() == 1);
            for (auto offerPtr : bobOffers)
            {
                auto const& offer = *offerPtr;
                BEAST_EXPECT(offer[sfTakerGets] == USD(499.5));
                BEAST_EXPECT(offer[sfTakerPays] == XRP(999));
            }
        }
    }

    void
    testMalformed(FeatureBitset features)
    {
        testcase("Malformed Detection");

        using namespace jtx;

        auto const startBalance = XRP(1000000);
        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const USD = gw["USD"];

        Env env{*this, features};

        env.fund(startBalance, gw, alice);
        env.close();

        // Order that has invalid flags
        env(offer(alice, USD(1000), XRP(1000)),
            txflags(tfImmediateOrCancel + 1),
            ter(temINVALID_FLAG));
        env.require(balance(alice, startBalance), owners(alice, 0), offers(alice, 0));

        // Order with incompatible flags
        env(offer(alice, USD(1000), XRP(1000)),
            txflags(tfImmediateOrCancel | tfFillOrKill),
            ter(temINVALID_FLAG));
        env.require(balance(alice, startBalance), owners(alice, 0), offers(alice, 0));

        // Sell and buy the same asset
        {
            // Alice tries an XRP to XRP order:
            env(offer(alice, XRP(1000), XRP(1000)), ter(temBAD_OFFER));
            env.require(owners(alice, 0), offers(alice, 0));

            // Alice tries an IOU to IOU order:
            env(trust(alice, USD(1000)), ter(tesSUCCESS));
            env(pay(gw, alice, USD(1000)), ter(tesSUCCESS));
            env(offer(alice, USD(1000), USD(1000)), ter(temREDUNDANT));
            env.require(owners(alice, 1), offers(alice, 0));
        }

        // Offers with negative amounts
        {
            env(offer(alice, -USD(1000), XRP(1000)), ter(temBAD_OFFER));
            env.require(owners(alice, 1), offers(alice, 0));

            env(offer(alice, USD(1000), -XRP(1000)), ter(temBAD_OFFER));
            env.require(owners(alice, 1), offers(alice, 0));
        }

        // Offer with a bad expiration
        {
            env(offer(alice, USD(1000), XRP(1000)),
                json(sfExpiration.fieldName, std::uint32_t(0)),
                ter(temBAD_EXPIRATION));
            env.require(owners(alice, 1), offers(alice, 0));
        }

        // Offer with a bad offer sequence
        {
            env(offer(alice, USD(1000), XRP(1000)),
                json(jss::OfferSequence, std::uint32_t(0)),
                ter(temBAD_SEQUENCE));
            env.require(owners(alice, 1), offers(alice, 0));
        }

        // Use XRP as a currency code
        {
            auto const BAD = IOU(gw, badCurrency());

            env(offer(alice, XRP(1000), BAD(1000)), ter(temBAD_CURRENCY));
            env.require(owners(alice, 1), offers(alice, 0));
        }
    }

    void
    testExpiration(FeatureBitset features)
    {
        testcase("Offer Expiration");

        using namespace jtx;

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const USD = gw["USD"];

        auto const startBalance = XRP(1000000);
        auto const usdOffer = USD(1000);
        auto const xrpOffer = XRP(1000);

        Env env{*this, features};

        env.fund(startBalance, gw, alice, bob);
        env.close();

        auto const f = env.current()->fees().base;

        env(trust(alice, usdOffer), ter(tesSUCCESS));
        env(pay(gw, alice, usdOffer), ter(tesSUCCESS));
        env.close();
        env.require(
            balance(alice, startBalance - f),
            balance(alice, usdOffer),
            offers(alice, 0),
            owners(alice, 1));

        env(offer(alice, xrpOffer, usdOffer),
            json(sfExpiration.fieldName, lastClose(env)),
            ter(tecEXPIRED));

        env.require(
            balance(alice, startBalance - f - f),
            balance(alice, usdOffer),
            offers(alice, 0),
            owners(alice, 1));
        env.close();

        // Add an offer that expires before the next ledger close
        env(offer(alice, xrpOffer, usdOffer),
            json(sfExpiration.fieldName, lastClose(env) + 1),
            ter(tesSUCCESS));
        env.require(
            balance(alice, startBalance - f - f - f),
            balance(alice, usdOffer),
            offers(alice, 1),
            owners(alice, 2));

        // The offer expires (it's not removed yet)
        env.close();
        env.require(
            balance(alice, startBalance - f - f - f),
            balance(alice, usdOffer),
            offers(alice, 1),
            owners(alice, 2));

        // Add offer - the expired offer is removed
        env(offer(bob, usdOffer, xrpOffer), ter(tesSUCCESS));
        env.require(
            balance(alice, startBalance - f - f - f),
            balance(alice, usdOffer),
            offers(alice, 0),
            owners(alice, 1),
            balance(bob, startBalance - f),
            balance(bob, USD(none)),
            offers(bob, 1),
            owners(bob, 1));
    }

    void
    testUnfundedCross(FeatureBitset features)
    {
        testcase("Unfunded Crossing");

        using namespace jtx;

        auto const gw = Account{"gateway"};
        auto const USD = gw["USD"];

        auto const usdOffer = USD(1000);
        auto const xrpOffer = XRP(1000);

        Env env{*this, features};

        env.fund(XRP(1000000), gw);
        env.close();

        // The fee that's charged for transactions
        auto const f = env.current()->fees().base;

        // Account is at the reserve, and will dip below once
        // fees are subtracted.
        env.fund(reserve(env, 0), "alice");
        env.close();
        env(offer("alice", usdOffer, xrpOffer), ter(tecUNFUNDED_OFFER));
        env.require(balance("alice", reserve(env, 0) - f), owners("alice", 0));

        // Account has just enough for the reserve and the
        // fee.
        env.fund(reserve(env, 0) + f, "bob");
        env.close();
        env(offer("bob", usdOffer, xrpOffer), ter(tecUNFUNDED_OFFER));
        env.require(balance("bob", reserve(env, 0)), owners("bob", 0));

        // Account has enough for the reserve, the fee and
        // the offer, and a bit more, but not enough for the
        // reserve after the offer is placed.
        env.fund(reserve(env, 0) + f + XRP(1), "carol");
        env.close();
        env(offer("carol", usdOffer, xrpOffer), ter(tecINSUF_RESERVE_OFFER));
        env.require(balance("carol", reserve(env, 0) + XRP(1)), owners("carol", 0));

        // Account has enough for the reserve plus one
        // offer, and the fee.
        env.fund(reserve(env, 1) + f, "dan");
        env.close();
        env(offer("dan", usdOffer, xrpOffer), ter(tesSUCCESS));
        env.require(balance("dan", reserve(env, 1)), owners("dan", 1));

        // Account has enough for the reserve plus one
        // offer, the fee and the entire offer amount.
        env.fund(reserve(env, 1) + f + xrpOffer, "eve");
        env.close();
        env(offer("eve", usdOffer, xrpOffer), ter(tesSUCCESS));
        env.require(balance("eve", reserve(env, 1) + xrpOffer), owners("eve", 1));
    }

    void
    testSelfCross(bool use_partner, FeatureBitset features)
    {
        testcase(std::string("Self-crossing") + (use_partner ? ", with partner account" : ""));

        using namespace jtx;

        auto const gw = Account{"gateway"};
        auto const partner = Account{"partner"};
        auto const USD = gw["USD"];
        auto const BTC = gw["BTC"];

        Env env{*this, features};
        env.close();

        env.fund(XRP(10000), gw);
        if (use_partner)
        {
            env.fund(XRP(10000), partner);
            env.close();
            env(trust(partner, USD(100)));
            env(trust(partner, BTC(500)));
            env.close();
            env(pay(gw, partner, USD(100)));
            env(pay(gw, partner, BTC(500)));
        }
        auto const& account_to_test = use_partner ? partner : gw;

        env.close();
        env.require(offers(account_to_test, 0));

        // PART 1:
        // we will make two offers that can be used to bridge BTC to USD
        // through XRP
        env(offer(account_to_test, BTC(250), XRP(1000)));
        env.require(offers(account_to_test, 1));

        // validate that the book now shows a BTC for XRP offer
        BEAST_EXPECT(isOffer(env, account_to_test, BTC(250), XRP(1000)));

        auto const secondLegSeq = env.seq(account_to_test);
        env(offer(account_to_test, XRP(1000), USD(50)));
        env.require(offers(account_to_test, 2));

        // validate that the book also shows a XRP for USD offer
        BEAST_EXPECT(isOffer(env, account_to_test, XRP(1000), USD(50)));

        // now make an offer that will cross and auto-bridge, meaning
        // the outstanding offers will be taken leaving us with none
        env(offer(account_to_test, USD(50), BTC(250)));

        auto jrr = getBookOffers(env, USD, BTC);
        BEAST_EXPECT(jrr[jss::offers].isArray());
        BEAST_EXPECT(jrr[jss::offers].size() == 0);

        jrr = getBookOffers(env, BTC, XRP);
        BEAST_EXPECT(jrr[jss::offers].isArray());
        BEAST_EXPECT(jrr[jss::offers].size() == 0);

        // NOTE :
        // At this point, all offers are expected to be consumed.
        {
            auto acctOffers = offersOnAccount(env, account_to_test);

            BEAST_EXPECT(acctOffers.empty());
            for (auto const& offerPtr : acctOffers)
            {
                auto const& offer = *offerPtr;
                BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
                BEAST_EXPECT(offer[sfTakerGets] == USD(0));
                BEAST_EXPECT(offer[sfTakerPays] == XRP(0));
            }
        }

        // cancel that lingering second offer so that it doesn't interfere
        // with the next set of offers we test. This will not be needed once
        // the bridging bug is fixed
        env(offer_cancel(account_to_test, secondLegSeq));
        env.require(offers(account_to_test, 0));

        // PART 2:
        // simple direct crossing  BTC to USD and then USD to BTC which causes
        // the first offer to be replaced
        env(offer(account_to_test, BTC(250), USD(50)));
        env.require(offers(account_to_test, 1));

        // validate that the book shows one BTC for USD offer and no USD for
        // BTC offers
        BEAST_EXPECT(isOffer(env, account_to_test, BTC(250), USD(50)));

        jrr = getBookOffers(env, USD, BTC);
        BEAST_EXPECT(jrr[jss::offers].isArray());
        BEAST_EXPECT(jrr[jss::offers].size() == 0);

        // this second offer would self-cross directly, so it causes the first
        // offer by the same owner/taker to be removed
        env(offer(account_to_test, USD(50), BTC(250)));
        env.require(offers(account_to_test, 1));

        // validate that we now have just the second offer...the first
        // was removed
        jrr = getBookOffers(env, BTC, USD);
        BEAST_EXPECT(jrr[jss::offers].isArray());
        BEAST_EXPECT(jrr[jss::offers].size() == 0);

        BEAST_EXPECT(isOffer(env, account_to_test, USD(50), BTC(250)));
    }

    void
    testNegativeBalance(FeatureBitset features)
    {
        // This test creates an offer test for negative balance
        // with transfer fees and minuscule funds.
        testcase("Negative Balance");

        using namespace jtx;

        // This is one of the few tests where fixReducedOffersV2 changes the
        // results.  So test both with and without fixReducedOffersV2.
        for (FeatureBitset localFeatures :
             {features - fixReducedOffersV2, features | fixReducedOffersV2})
        {
            Env env{*this, localFeatures};

            auto const gw = Account{"gateway"};
            auto const alice = Account{"alice"};
            auto const bob = Account{"bob"};
            auto const USD = gw["USD"];
            auto const BTC = gw["BTC"];

            // these *interesting* amounts were taken
            // from the original JS test that was ported here
            auto const gw_initial_balance = drops(1149999730);
            auto const alice_initial_balance = drops(499946999680);
            auto const bob_initial_balance = drops(10199999920);
            auto const small_amount = STAmount{bob["USD"].issue(), UINT64_C(2710505431213761), -33};

            env.fund(gw_initial_balance, gw);
            env.fund(alice_initial_balance, alice);
            env.fund(bob_initial_balance, bob);
            env.close();

            env(rate(gw, 1.005));

            env(trust(alice, USD(500)));
            env(trust(bob, USD(50)));
            env(trust(gw, alice["USD"](100)));

            env(pay(gw, alice, alice["USD"](50)));
            env(pay(gw, bob, small_amount));

            env(offer(alice, USD(50), XRP(150000)));

            // unfund the offer
            env(pay(alice, gw, USD(100)));

            // drop the trust line (set to 0)
            env(trust(gw, alice["USD"](0)));

            // verify balances
            auto jrr = ledgerEntryState(env, alice, gw, "USD");
            BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "50");

            jrr = ledgerEntryState(env, bob, gw, "USD");
            BEAST_EXPECT(
                jrr[jss::node][sfBalance.fieldName][jss::value] == "-2710505431213761e-33");

            // create crossing offer
            std::uint32_t const bobOfferSeq = env.seq(bob);
            env(offer(bob, XRP(2000), USD(1)));

            if (localFeatures[fixReducedOffersV2])
            {
                // With the rounding introduced by fixReducedOffersV2, bob's
                // offer does not cross alice's offer and goes straight into
                // the ledger.
                jrr = ledgerEntryState(env, bob, gw, "USD");
                BEAST_EXPECT(
                    jrr[jss::node][sfBalance.fieldName][jss::value] == "-2710505431213761e-33");

                Json::Value const bobOffer = ledgerEntryOffer(env, bob, bobOfferSeq)[jss::node];
                BEAST_EXPECT(bobOffer[sfTakerGets.jsonName][jss::value] == "1");
                BEAST_EXPECT(bobOffer[sfTakerPays.jsonName] == "2000000000");
                return;
            }

            // verify balances again.
            //
            // NOTE:
            // Here a difference in the rounding modes of our two offer
            // crossing algorithms becomes apparent.  The old offer crossing
            // would consume small_amount and transfer no XRP.  The new offer
            // crossing transfers a single drop, rather than no drops.
            auto const crossingDelta = drops(1);

            jrr = ledgerEntryState(env, alice, gw, "USD");
            BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "50");
            BEAST_EXPECT(
                env.balance(alice, xrpIssue()) ==
                alice_initial_balance - env.current()->fees().base * 3 - crossingDelta);

            jrr = ledgerEntryState(env, bob, gw, "USD");
            BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "0");
            BEAST_EXPECT(
                env.balance(bob, xrpIssue()) ==
                bob_initial_balance - env.current()->fees().base * 2 + crossingDelta);
        }
    }

    void
    testOfferCrossWithXRP(bool reverse_order, FeatureBitset features)
    {
        testcase(
            std::string("Offer Crossing with XRP, ") + (reverse_order ? "Reverse" : "Normal") +
            " order");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const USD = gw["USD"];

        env.fund(XRP(10000), gw, alice, bob);
        env.close();

        env(trust(alice, USD(1000)));
        env(trust(bob, USD(1000)));

        env(pay(gw, alice, alice["USD"](500)));

        if (reverse_order)
            env(offer(bob, USD(1), XRP(4000)));

        env(offer(alice, XRP(150000), USD(50)));

        if (!reverse_order)
            env(offer(bob, USD(1), XRP(4000)));

        // Existing offer pays better than this wants.
        // Fully consume existing offer.
        // Pay 1 USD, get 4000 XRP.

        auto jrr = ledgerEntryState(env, bob, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-1");
        jrr = ledgerEntryRoot(env, bob);
        BEAST_EXPECT(
            jrr[jss::node][sfBalance.fieldName] ==
            to_string(
                (XRP(10000) - XRP(reverse_order ? 4000 : 3000) - env.current()->fees().base * 2)
                    .xrp()));

        jrr = ledgerEntryState(env, alice, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-499");
        jrr = ledgerEntryRoot(env, alice);
        BEAST_EXPECT(
            jrr[jss::node][sfBalance.fieldName] ==
            to_string(
                (XRP(10000) + XRP(reverse_order ? 4000 : 3000) - env.current()->fees().base * 2)
                    .xrp()));
    }

    void
    testOfferCrossWithLimitOverride(FeatureBitset features)
    {
        testcase("Offer Crossing with Limit Override");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const USD = gw["USD"];

        env.fund(XRP(100000), gw, alice, bob);
        env.close();

        env(trust(alice, USD(1000)));

        env(pay(gw, alice, alice["USD"](500)));

        env(offer(alice, XRP(150000), USD(50)));
        env(offer(bob, USD(1), XRP(3000)));

        auto jrr = ledgerEntryState(env, bob, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-1");
        jrr = ledgerEntryRoot(env, bob);
        BEAST_EXPECT(
            jrr[jss::node][sfBalance.fieldName] ==
            to_string((XRP(100000) - XRP(3000) - env.current()->fees().base * 1).xrp()));

        jrr = ledgerEntryState(env, alice, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-499");
        jrr = ledgerEntryRoot(env, alice);
        BEAST_EXPECT(
            jrr[jss::node][sfBalance.fieldName] ==
            to_string((XRP(100000) + XRP(3000) - env.current()->fees().base * 2).xrp()));
    }

    void
    testOfferAcceptThenCancel(FeatureBitset features)
    {
        testcase("Offer Accept then Cancel.");

        using namespace jtx;

        Env env{*this, features};

        auto const USD = env.master["USD"];

        auto const nextOfferSeq = env.seq(env.master);
        env(offer(env.master, XRP(500), USD(100)));
        env.close();

        env(offer_cancel(env.master, nextOfferSeq));
        BEAST_EXPECT(env.seq(env.master) == nextOfferSeq + 2);

        // ledger_accept, call twice and verify no odd behavior
        env.close();
        env.close();
        BEAST_EXPECT(env.seq(env.master) == nextOfferSeq + 2);
    }

    void
    testOfferCancelPastAndFuture(FeatureBitset features)
    {
        testcase("Offer Cancel Past and Future Sequence.");

        using namespace jtx;

        Env env{*this, features};

        auto const alice = Account{"alice"};

        auto const nextOfferSeq = env.seq(env.master);
        env.fund(XRP(10000), alice);
        env.close();

        env(offer_cancel(env.master, nextOfferSeq));

        env(offer_cancel(env.master, env.seq(env.master)), ter(temBAD_SEQUENCE));

        env(offer_cancel(env.master, env.seq(env.master) + 1), ter(temBAD_SEQUENCE));

        env.close();
    }

    void
    testCurrencyConversionEntire(FeatureBitset features)
    {
        testcase("Currency Conversion: Entire Offer");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const USD = gw["USD"];

        env.fund(XRP(10000), gw, alice, bob);
        env.close();
        env.require(owners(bob, 0));

        env(trust(alice, USD(100)));
        env(trust(bob, USD(1000)));

        env.require(owners(alice, 1), owners(bob, 1));

        env(pay(gw, alice, alice["USD"](100)));
        auto const bobOfferSeq = env.seq(bob);
        env(offer(bob, USD(100), XRP(500)));

        env.require(owners(alice, 1), owners(bob, 2));
        auto jro = ledgerEntryOffer(env, bob, bobOfferSeq);
        BEAST_EXPECT(jro[jss::node][jss::TakerGets] == XRP(500).value().getText());
        BEAST_EXPECT(jro[jss::node][jss::TakerPays] == USD(100).value().getJson(JsonOptions::none));

        env(pay(alice, alice, XRP(500)), sendmax(USD(100)));

        auto jrr = ledgerEntryState(env, alice, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "0");
        jrr = ledgerEntryRoot(env, alice);
        BEAST_EXPECT(
            jrr[jss::node][sfBalance.fieldName] ==
            to_string((XRP(10000) + XRP(500) - env.current()->fees().base * 2).xrp()));

        jrr = ledgerEntryState(env, bob, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-100");

        jro = ledgerEntryOffer(env, bob, bobOfferSeq);
        BEAST_EXPECT(jro[jss::error] == "entryNotFound");

        env.require(owners(alice, 1), owners(bob, 1));
    }

    void
    testCurrencyConversionIntoDebt(FeatureBitset features)
    {
        testcase("Currency Conversion: Offerer Into Debt");

        using namespace jtx;

        Env env{*this, features};

        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const carol = Account{"carol"};

        env.fund(XRP(10000), alice, bob, carol);
        env.close();

        env(trust(alice, carol["EUR"](2000)));
        env(trust(bob, alice["USD"](100)));
        env(trust(carol, bob["EUR"](1000)));

        auto const bobOfferSeq = env.seq(bob);
        env(offer(bob, alice["USD"](50), carol["EUR"](200)), ter(tecUNFUNDED_OFFER));

        env(offer(alice, carol["EUR"](200), alice["USD"](50)));

        auto jro = ledgerEntryOffer(env, bob, bobOfferSeq);
        BEAST_EXPECT(jro[jss::error] == "entryNotFound");
    }

    void
    testCurrencyConversionInParts(FeatureBitset features)
    {
        testcase("Currency Conversion: In Parts");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const USD = gw["USD"];

        env.fund(XRP(10000), gw, alice, bob);
        env.close();

        env(trust(alice, USD(200)));
        env(trust(bob, USD(1000)));

        env(pay(gw, alice, alice["USD"](200)));

        auto const bobOfferSeq = env.seq(bob);
        env(offer(bob, USD(100), XRP(500)));

        env(pay(alice, alice, XRP(200)), sendmax(USD(100)));

        // The previous payment reduced the remaining offer amount by 200 XRP
        auto jro = ledgerEntryOffer(env, bob, bobOfferSeq);
        BEAST_EXPECT(jro[jss::node][jss::TakerGets] == XRP(300).value().getText());
        BEAST_EXPECT(jro[jss::node][jss::TakerPays] == USD(60).value().getJson(JsonOptions::none));

        // the balance between alice and gw is 160 USD..200 less the 40 taken
        // by the offer
        auto jrr = ledgerEntryState(env, alice, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-160");
        // alice now has 200 more XRP from the payment
        jrr = ledgerEntryRoot(env, alice);
        BEAST_EXPECT(
            jrr[jss::node][sfBalance.fieldName] ==
            to_string((XRP(10000) + XRP(200) - env.current()->fees().base * 2).xrp()));

        // bob got 40 USD from partial consumption of the offer
        jrr = ledgerEntryState(env, bob, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-40");

        // Alice converts USD to XRP which should fail
        // due to PartialPayment.
        env(pay(alice, alice, XRP(600)), sendmax(USD(100)), ter(tecPATH_PARTIAL));

        // Alice converts USD to XRP, should succeed because
        // we permit partial payment
        env(pay(alice, alice, XRP(600)), sendmax(USD(100)), txflags(tfPartialPayment));

        // Verify the offer was consumed
        jro = ledgerEntryOffer(env, bob, bobOfferSeq);
        BEAST_EXPECT(jro[jss::error] == "entryNotFound");

        // verify balances look right after the partial payment
        // only 300 XRP should be have been payed since that's all
        // that remained in the offer from bob. The alice balance is now
        // 100 USD because another 60 USD were transferred to bob in the second
        // payment
        jrr = ledgerEntryState(env, alice, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-100");
        jrr = ledgerEntryRoot(env, alice);
        BEAST_EXPECT(
            jrr[jss::node][sfBalance.fieldName] ==
            to_string((XRP(10000) + XRP(200) + XRP(300) - env.current()->fees().base * 4).xrp()));

        // bob now has 100 USD - 40 from the first payment and 60 from the
        // second (partial) payment
        jrr = ledgerEntryState(env, bob, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-100");
    }

    void
    testCrossCurrencyStartXRP(FeatureBitset features)
    {
        testcase("Cross Currency Payment: Start with XRP");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const carol = Account{"carol"};
        auto const USD = gw["USD"];

        env.fund(XRP(10000), gw, alice, bob, carol);
        env.close();

        env(trust(carol, USD(1000)));
        env(trust(bob, USD(2000)));

        env(pay(gw, carol, carol["USD"](500)));

        auto const carolOfferSeq = env.seq(carol);
        env(offer(carol, XRP(500), USD(50)));

        env(pay(alice, bob, USD(25)), sendmax(XRP(333)));

        auto jrr = ledgerEntryState(env, bob, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-25");

        jrr = ledgerEntryState(env, carol, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-475");

        auto jro = ledgerEntryOffer(env, carol, carolOfferSeq);
        BEAST_EXPECT(jro[jss::node][jss::TakerGets] == USD(25).value().getJson(JsonOptions::none));
        BEAST_EXPECT(jro[jss::node][jss::TakerPays] == XRP(250).value().getText());
    }

    void
    testCrossCurrencyEndXRP(FeatureBitset features)
    {
        testcase("Cross Currency Payment: End with XRP");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const carol = Account{"carol"};
        auto const USD = gw["USD"];

        env.fund(XRP(10000), gw, alice, bob, carol);
        env.close();

        env(trust(alice, USD(1000)));
        env(trust(carol, USD(2000)));

        env(pay(gw, alice, alice["USD"](500)));

        auto const carolOfferSeq = env.seq(carol);
        env(offer(carol, USD(50), XRP(500)));

        env(pay(alice, bob, XRP(250)), sendmax(USD(333)));

        auto jrr = ledgerEntryState(env, alice, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-475");

        jrr = ledgerEntryState(env, carol, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-25");

        jrr = ledgerEntryRoot(env, bob);
        BEAST_EXPECT(
            jrr[jss::node][sfBalance.fieldName] ==
            std::to_string(XRP(10000).value().mantissa() + XRP(250).value().mantissa()));

        auto jro = ledgerEntryOffer(env, carol, carolOfferSeq);
        BEAST_EXPECT(jro[jss::node][jss::TakerGets] == XRP(250).value().getText());
        BEAST_EXPECT(jro[jss::node][jss::TakerPays] == USD(25).value().getJson(JsonOptions::none));
    }

    void
    testCrossCurrencyBridged(FeatureBitset features)
    {
        testcase("Cross Currency Payment: Bridged");

        using namespace jtx;

        Env env{*this, features};

        auto const gw1 = Account{"gateway_1"};
        auto const gw2 = Account{"gateway_2"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const carol = Account{"carol"};
        auto const dan = Account{"dan"};
        auto const USD = gw1["USD"];
        auto const EUR = gw2["EUR"];

        env.fund(XRP(10000), gw1, gw2, alice, bob, carol, dan);
        env.close();

        env(trust(alice, USD(1000)));
        env(trust(bob, EUR(1000)));
        env(trust(carol, USD(1000)));
        env(trust(dan, EUR(1000)));

        env(pay(gw1, alice, alice["USD"](500)));
        env(pay(gw2, dan, dan["EUR"](400)));

        auto const carolOfferSeq = env.seq(carol);
        env(offer(carol, USD(50), XRP(500)));

        auto const danOfferSeq = env.seq(dan);
        env(offer(dan, XRP(500), EUR(50)));

        Json::Value jtp{Json::arrayValue};
        jtp[0u][0u][jss::currency] = "XRP";
        env(pay(alice, bob, EUR(30)), json(jss::Paths, jtp), sendmax(USD(333)));

        auto jrr = ledgerEntryState(env, alice, gw1, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "470");

        jrr = ledgerEntryState(env, bob, gw2, "EUR");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-30");

        jrr = ledgerEntryState(env, carol, gw1, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-30");

        jrr = ledgerEntryState(env, dan, gw2, "EUR");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-370");

        auto jro = ledgerEntryOffer(env, carol, carolOfferSeq);
        BEAST_EXPECT(jro[jss::node][jss::TakerGets] == XRP(200).value().getText());
        BEAST_EXPECT(jro[jss::node][jss::TakerPays] == USD(20).value().getJson(JsonOptions::none));

        jro = ledgerEntryOffer(env, dan, danOfferSeq);
        BEAST_EXPECT(
            jro[jss::node][jss::TakerGets] == gw2["EUR"](20).value().getJson(JsonOptions::none));
        BEAST_EXPECT(jro[jss::node][jss::TakerPays] == XRP(200).value().getText());
    }

    void
    testBridgedSecondLegDry(FeatureBitset features)
    {
        // At least with Taker bridging, a sensitivity was identified if the
        // second leg goes dry before the first one.  This test exercises that
        // case.
        testcase("Auto Bridged Second Leg Dry");

        using namespace jtx;
        Env env(*this, features);

        Account const alice{"alice"};
        Account const bob{"bob"};
        Account const carol{"carol"};
        Account const gw{"gateway"};
        auto const USD = gw["USD"];
        auto const EUR = gw["EUR"];

        env.fund(XRP(100000000), alice, bob, carol, gw);
        env.close();

        env.trust(USD(10), alice);
        env.close();
        env(pay(gw, alice, USD(10)));
        env.trust(USD(10), carol);
        env.close();
        env(pay(gw, carol, USD(3)));

        env(offer(alice, EUR(2), XRP(1)));
        env(offer(alice, EUR(2), XRP(1)));

        env(offer(alice, XRP(1), USD(4)));
        env(offer(carol, XRP(1), USD(3)));
        env.close();

        // Bob offers to buy 10 USD for 10 EUR.
        //  1. He spends 2 EUR taking Alice's auto-bridged offers and
        //     gets 4 USD for that.
        //  2. He spends another 2 EUR taking Alice's last EUR->XRP offer and
        //     Carol's XRP-USD offer.  He gets 3 USD for that.
        // The key for this test is that Alice's XRP->USD leg goes dry before
        // Alice's EUR->XRP.  The XRP->USD leg is the second leg which showed
        // some sensitivity.
        env.trust(EUR(10), bob);
        env.close();
        env(pay(gw, bob, EUR(10)));
        env.close();
        env(offer(bob, USD(10), EUR(10)));
        env.close();

        env.require(balance(bob, USD(7)));
        env.require(balance(bob, EUR(6)));
        env.require(offers(bob, 1));
        env.require(owners(bob, 3));

        env.require(balance(alice, USD(6)));
        env.require(balance(alice, EUR(4)));
        env.require(offers(alice, 0));
        env.require(owners(alice, 2));

        env.require(balance(carol, USD(0)));
        env.require(balance(carol, EUR(none)));

        env.require(offers(carol, 0));
        env.require(owners(carol, 1));
    }

    void
    testOfferFeesConsumeFunds(FeatureBitset features)
    {
        testcase("Offer Fees Consume Funds");

        using namespace jtx;

        Env env{*this, features};

        auto const gw1 = Account{"gateway_1"};
        auto const gw2 = Account{"gateway_2"};
        auto const gw3 = Account{"gateway_3"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const USD1 = gw1["USD"];
        auto const USD2 = gw2["USD"];
        auto const USD3 = gw3["USD"];

        // Provide micro amounts to compensate for fees to make results round
        // nice.
        // reserve: Alice has 3 entries in the ledger, via trust lines
        // fees:
        //  1 for each trust limit == 3 (alice < mtgox/amazon/bitstamp) +
        //  1 for payment          == 4
        auto const starting_xrp =
            XRP(100) + env.current()->fees().accountReserve(3) + env.current()->fees().base * 4;

        env.fund(starting_xrp, gw1, gw2, gw3, alice, bob);
        env.close();

        env(trust(alice, USD1(1000)));
        env(trust(alice, USD2(1000)));
        env(trust(alice, USD3(1000)));
        env(trust(bob, USD1(1000)));
        env(trust(bob, USD2(1000)));

        env(pay(gw1, bob, bob["USD"](500)));

        env(offer(bob, XRP(200), USD1(200)));
        // Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available.
        // Ask for more than available to prove reserve works.
        env(offer(alice, USD1(200), XRP(200)));

        auto jrr = ledgerEntryState(env, alice, gw1, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "100");
        jrr = ledgerEntryRoot(env, alice);
        BEAST_EXPECT(
            jrr[jss::node][sfBalance.fieldName] ==
            STAmount(env.current()->fees().accountReserve(3)).getText());

        jrr = ledgerEntryState(env, bob, gw1, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-400");
    }

    void
    testOfferCreateThenCross(FeatureBitset features)
    {
        testcase("Offer Create, then Cross");

        using namespace jtx;

        for (auto NumberSwitchOver : {false, true})
        {
            Env env{*this, features};
            if (NumberSwitchOver)
            {
                env.enableFeature(fixUniversalNumber);
            }
            else
            {
                env.disableFeature(fixUniversalNumber);
            }

            auto const gw = Account{"gateway"};
            auto const alice = Account{"alice"};
            auto const bob = Account{"bob"};
            auto const USD = gw["USD"];

            env.fund(XRP(10000), gw, alice, bob);
            env.close();

            env(rate(gw, 1.005));

            env(trust(alice, USD(1000)));
            env(trust(bob, USD(1000)));
            env(trust(gw, alice["USD"](50)));

            env(pay(gw, bob, bob["USD"](1)));
            env(pay(alice, gw, USD(50)));

            env(trust(gw, alice["USD"](0)));

            env(offer(alice, USD(50), XRP(150000)));
            env(offer(bob, XRP(100), USD(0.1)));

            auto jrr = ledgerEntryState(env, alice, gw, "USD");
            BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "49.96666666666667");

            jrr = ledgerEntryState(env, bob, gw, "USD");
            Json::Value const bobUSD = jrr[jss::node][sfBalance.fieldName][jss::value];
            if (!NumberSwitchOver)
            {
                BEAST_EXPECT(bobUSD == "-0.966500000033334");
            }
            else
            {
                BEAST_EXPECT(bobUSD == "-0.9665000000333333");
            }
        }
    }

    void
    testSellFlagBasic(FeatureBitset features)
    {
        testcase("Offer tfSell: Basic Sell");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const USD = gw["USD"];

        auto const starting_xrp =
            XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2;

        env.fund(starting_xrp, gw, alice, bob);
        env.close();

        env(trust(alice, USD(1000)));
        env(trust(bob, USD(1000)));

        env(pay(gw, bob, bob["USD"](500)));

        env(offer(bob, XRP(200), USD(200)), json(jss::Flags, tfSell));
        // Alice has 350 + fees - a reserve of 50 = 250 reserve = 100 available.
        // Alice has 350 + fees - a reserve of 50 = 250 reserve = 100 available.
        // Ask for more than available to prove reserve works.
        env(offer(alice, USD(200), XRP(200)), json(jss::Flags, tfSell));

        auto jrr = ledgerEntryState(env, alice, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-100");
        jrr = ledgerEntryRoot(env, alice);
        BEAST_EXPECT(
            jrr[jss::node][sfBalance.fieldName] ==
            STAmount(env.current()->fees().accountReserve(1)).getText());

        jrr = ledgerEntryState(env, bob, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-400");
    }

    void
    testSellFlagExceedLimit(FeatureBitset features)
    {
        testcase("Offer tfSell: 2x Sell Exceed Limit");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const USD = gw["USD"];

        auto const starting_xrp =
            XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2;

        env.fund(starting_xrp, gw, alice, bob);
        env.close();

        env(trust(alice, USD(150)));
        env(trust(bob, USD(1000)));

        env(pay(gw, bob, bob["USD"](500)));

        env(offer(bob, XRP(100), USD(200)));
        // Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available.
        // Ask for more than available to prove reserve works.
        // Taker pays 100 USD for 100 XRP.
        // Selling XRP.
        // Will sell all 100 XRP and get more USD than asked for.
        env(offer(alice, USD(100), XRP(100)), json(jss::Flags, tfSell));

        auto jrr = ledgerEntryState(env, alice, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-200");
        jrr = ledgerEntryRoot(env, alice);
        BEAST_EXPECT(
            jrr[jss::node][sfBalance.fieldName] ==
            STAmount(env.current()->fees().accountReserve(1)).getText());

        jrr = ledgerEntryState(env, bob, gw, "USD");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-300");
    }

    void
    testGatewayCrossCurrency(FeatureBitset features)
    {
        testcase("Client Issue #535: Gateway Cross Currency");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const XTS = gw["XTS"];
        auto const XXX = gw["XXX"];

        auto const starting_xrp =
            XRP(100.1) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2;

        env.fund(starting_xrp, gw, alice, bob);
        env.close();

        env(trust(alice, XTS(1000)));
        env(trust(alice, XXX(1000)));
        env(trust(bob, XTS(1000)));
        env(trust(bob, XXX(1000)));

        env(pay(gw, alice, alice["XTS"](100)));
        env(pay(gw, alice, alice["XXX"](100)));
        env(pay(gw, bob, bob["XTS"](100)));
        env(pay(gw, bob, bob["XXX"](100)));

        env(offer(alice, XTS(100), XXX(100)));

        // WS client is used here because the RPC client could not
        // be convinced to pass the build_path argument
        auto wsc = makeWSClient(env.app().config());
        Json::Value payment;
        payment[jss::secret] = toBase58(generateSeed("bob"));
        payment[jss::id] = env.seq(bob);
        payment[jss::build_path] = true;
        payment[jss::tx_json] = pay(bob, bob, bob["XXX"](1));
        payment[jss::tx_json][jss::Sequence] =
            env.current()->read(keylet::account(bob.id()))->getFieldU32(sfSequence);
        payment[jss::tx_json][jss::Fee] = to_string(env.current()->fees().base);
        payment[jss::tx_json][jss::SendMax] = bob["XTS"](1.5).value().getJson(JsonOptions::none);
        auto jrr = wsc->invoke("submit", payment);
        BEAST_EXPECT(jrr[jss::status] == "success");
        BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
        if (wsc->version() == 2)
        {
            BEAST_EXPECT(jrr.isMember(jss::jsonrpc) && jrr[jss::jsonrpc] == "2.0");
            BEAST_EXPECT(jrr.isMember(jss::ripplerpc) && jrr[jss::ripplerpc] == "2.0");
            BEAST_EXPECT(jrr.isMember(jss::id) && jrr[jss::id] == 5);
        }

        jrr = ledgerEntryState(env, alice, gw, "XTS");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-101");
        jrr = ledgerEntryState(env, alice, gw, "XXX");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-99");

        jrr = ledgerEntryState(env, bob, gw, "XTS");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-99");
        jrr = ledgerEntryState(env, bob, gw, "XXX");
        BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-101");
    }

    // Helper function that validates a *defaulted* trustline: one that has
    // no unusual flags set and doesn't have high or low limits set. Such a
    // trustline may have an actual balance (it can be created automatically
    // if a user places an offer to acquire an IOU for which they don't have
    // a trust line defined). If the trustline is not defaulted then the tests
    // will not pass.
    void
    verifyDefaultTrustline(
        jtx::Env& env,
        jtx::Account const& account,
        jtx::PrettyAmount const& expectBalance)
    {
        auto const sleTrust = env.le(keylet::line(account.id(), expectBalance.value().issue()));
        BEAST_EXPECT(sleTrust);
        if (sleTrust)
        {
            Issue const issue = expectBalance.value().issue();
            bool const accountLow = account.id() < issue.account;

            STAmount low{issue};
            STAmount high{issue};

            low.setIssuer(accountLow ? account.id() : issue.account);
            high.setIssuer(accountLow ? issue.account : account.id());

            BEAST_EXPECT(sleTrust->getFieldAmount(sfLowLimit) == low);
            BEAST_EXPECT(sleTrust->getFieldAmount(sfHighLimit) == high);

            STAmount actualBalance{sleTrust->getFieldAmount(sfBalance)};
            if (!accountLow)
                actualBalance.negate();

            BEAST_EXPECT(actualBalance == expectBalance);
        }
    }

    void
    testPartialCross(FeatureBitset features)
    {
        // Test a number of different corner cases regarding adding a
        // possibly crossable offer to an account.  The test is table
        // driven so it should be easy to add or remove tests.
        testcase("Partial Crossing");

        using namespace jtx;

        auto const gw = Account("gateway");
        auto const USD = gw["USD"];

        Env env{*this, features};

        env.fund(XRP(10000000), gw);
        env.close();

        // The fee that's charged for transactions
        auto const f = env.current()->fees().base;

        // To keep things simple all offers are 1 : 1 for XRP : USD.
        enum preTrustType { noPreTrust, gwPreTrust, acctPreTrust };
        struct TestData
        {
            std::string account;      // Account operated on
            STAmount fundXrp;         // Account funded with
            int bookAmount;           // USD -> XRP offer on the books
            preTrustType preTrust;    // If true, pre-establish trust line
            int offerAmount;          // Account offers this much XRP -> USD
            TER tec;                  // Returned tec code
            STAmount spentXrp;        // Amount removed from fundXrp
            PrettyAmount balanceUsd;  // Balance on account end
            int offers;               // Offers on account
            int owners;               // Owners on account
        };

        // clang-format off
        TestData const tests[]{
            // acct                     fundXrp        bookAmt   preTrust  offerAmount                   tec     spentXrp       balanceUSD offers  owners
            {"ann",             reserve(env, 0) + 0 * f,    1,   noPreTrust, 1000,      tecUNFUNDED_OFFER,               f, USD(      0),    0, 0},  // Account is at the reserve, and will dip below once fees are subtracted.
            {"bev",             reserve(env, 0) + 1 * f,    1,   noPreTrust, 1000,      tecUNFUNDED_OFFER,               f, USD(      0),    0, 0},  // Account has just enough for the reserve and the fee.
            {"cam",             reserve(env, 0) + 2 * f,    0,   noPreTrust, 1000, tecINSUF_RESERVE_OFFER,               f, USD(      0),    0, 0},  // Account has enough for the reserve, the fee and the offer, and a bit more, but not enough for the reserve after the offer is placed.
            {"deb", drops(10) + reserve(env, 0) + 1 * f,    1,   noPreTrust, 1000,             tesSUCCESS, drops(10)   + f, USD(0.00001),    0, 1},  // Account has enough to buy a little USD then the offer runs dry.
            {"eve",             reserve(env, 1) + 0 * f,    0,   noPreTrust, 1000,             tesSUCCESS,               f, USD(      0),    1, 1},  // No offer to cross
            {"flo",             reserve(env, 1) + 0 * f,    1,   noPreTrust, 1000,             tesSUCCESS, XRP(   1)   + f, USD(      1),    0, 1},
            {"gay",             reserve(env, 1) + 1 * f, 1000,   noPreTrust, 1000,             tesSUCCESS, XRP(  50)   + f, USD(     50),    0, 1},
            {"hye", XRP(1000)                   + 1 * f, 1000,   noPreTrust, 1000,             tesSUCCESS, XRP( 800)   + f, USD(    800),    0, 1},
            {"ivy", XRP(   1) + reserve(env, 1) + 1 * f,    1,   noPreTrust, 1000,             tesSUCCESS, XRP(   1)   + f, USD(      1),    0, 1},
            {"joy", XRP(   1) + reserve(env, 2) + 1 * f,    1,   noPreTrust, 1000,             tesSUCCESS, XRP(   1)   + f, USD(      1),    1, 2},
            {"kim", XRP( 900) + reserve(env, 2) + 1 * f,  999,   noPreTrust, 1000,             tesSUCCESS, XRP( 999)   + f, USD(    999),    0, 1},
            {"liz", XRP( 998) + reserve(env, 0) + 1 * f,  999,   noPreTrust, 1000,             tesSUCCESS, XRP( 998)   + f, USD(    998),    0, 1},
            {"meg", XRP( 998) + reserve(env, 1) + 1 * f,  999,   noPreTrust, 1000,             tesSUCCESS, XRP( 999)   + f, USD(    999),    0, 1},
            {"nia", XRP( 998) + reserve(env, 2) + 1 * f,  999,   noPreTrust, 1000,             tesSUCCESS, XRP( 999)   + f, USD(    999),    1, 2},
            {"ova", XRP( 999) + reserve(env, 0) + 1 * f, 1000,   noPreTrust, 1000,             tesSUCCESS, XRP( 999)   + f, USD(    999),    0, 1},
            {"pam", XRP( 999) + reserve(env, 1) + 1 * f, 1000,   noPreTrust, 1000,             tesSUCCESS, XRP(1000)   + f, USD(   1000),    0, 1},
            {"rae", XRP( 999) + reserve(env, 2) + 1 * f, 1000,   noPreTrust, 1000,             tesSUCCESS, XRP(1000)   + f, USD(   1000),    0, 1},
            {"sue", XRP(1000) + reserve(env, 2) + 1 * f,    0,   noPreTrust, 1000,             tesSUCCESS,               f, USD(      0),    1, 1},
            //---------------- Pre-established trust lines ---------------------
            {"abe",             reserve(env, 0) + 0 * f,    1,   gwPreTrust, 1000,      tecUNFUNDED_OFFER,               f, USD(      0),    0, 0},
            {"bud",             reserve(env, 0) + 1 * f,    1,   gwPreTrust, 1000,      tecUNFUNDED_OFFER,               f, USD(      0),    0, 0},
            {"che",             reserve(env, 0) + 2 * f,    0,   gwPreTrust, 1000, tecINSUF_RESERVE_OFFER,               f, USD(      0),    0, 0},
            {"dan", drops(10) + reserve(env, 0) + 1 * f,    1,   gwPreTrust, 1000,             tesSUCCESS, drops(10)   + f, USD(0.00001),    0, 0},
            {"eli", XRP(  20) + reserve(env, 0) + 1 * f, 1000,   gwPreTrust, 1000,             tesSUCCESS, XRP(20) + 1 * f, USD(     20),    0, 0},
            {"fyn",             reserve(env, 1) + 0 * f,    0,   gwPreTrust, 1000,             tesSUCCESS,               f, USD(      0),    1, 1},
            {"gar",             reserve(env, 1) + 0 * f,    1,   gwPreTrust, 1000,             tesSUCCESS, XRP( 1) +     f, USD(      1),    1, 1},
            {"hal",             reserve(env, 1) + 1 * f,    1,   gwPreTrust, 1000,             tesSUCCESS, XRP( 1) +     f, USD(      1),    1, 1},

            {"ned",             reserve(env, 1) + 0 * f,    1, acctPreTrust, 1000,      tecUNFUNDED_OFFER,           2 * f, USD(      0),    0, 1},
            {"ole",             reserve(env, 1) + 1 * f,    1, acctPreTrust, 1000,      tecUNFUNDED_OFFER,           2 * f, USD(      0),    0, 1},
            {"pat",             reserve(env, 1) + 2 * f,    0, acctPreTrust, 1000,      tecUNFUNDED_OFFER,           2 * f, USD(      0),    0, 1},
            {"quy",             reserve(env, 1) + 2 * f,    1, acctPreTrust, 1000,      tecUNFUNDED_OFFER,           2 * f, USD(      0),    0, 1},
            {"ron",             reserve(env, 1) + 3 * f,    0, acctPreTrust, 1000, tecINSUF_RESERVE_OFFER,           2 * f, USD(      0),    0, 1},
            {"syd", drops(10) + reserve(env, 1) + 2 * f,    1, acctPreTrust, 1000,             tesSUCCESS, drops(10) + 2 * f, USD(0.00001),    0, 1},
            {"ted", XRP(  20) + reserve(env, 1) + 2 * f, 1000, acctPreTrust, 1000,             tesSUCCESS, XRP(20) + 2 * f, USD(     20),    0, 1},
            {"uli",             reserve(env, 2) + 0 * f,    0, acctPreTrust, 1000, tecINSUF_RESERVE_OFFER,           2 * f, USD(      0),    0, 1},
            {"vic",             reserve(env, 2) + 0 * f,    1, acctPreTrust, 1000,             tesSUCCESS, XRP( 1) + 2 * f, USD(      1),    0, 1},
            {"wes",             reserve(env, 2) + 1 * f,    0, acctPreTrust, 1000,             tesSUCCESS,           2 * f, USD(      0),    1, 2},
            {"xan",             reserve(env, 2) + 1 * f,    1, acctPreTrust, 1000,             tesSUCCESS, XRP( 1) + 2 * f, USD(      1),    1, 2},
        };
        // clang-format on

        for (auto const& t : tests)
        {
            auto const acct = Account(t.account);
            env.fund(t.fundXrp, acct);
            env.close();

            // Make sure gateway has no current offers.
            env.require(offers(gw, 0));

            // The gateway optionally creates an offer that would be crossed.
            auto const book = t.bookAmount;
            if (book != 0)
                env(offer(gw, XRP(book), USD(book)));
            env.close();
            std::uint32_t const gwOfferSeq = env.seq(gw) - 1;

            // Optionally pre-establish a trustline between gw and acct.
            if (t.preTrust == gwPreTrust)
                env(trust(gw, acct["USD"](1)));
            env.close();

            // Optionally pre-establish a trustline between acct and gw.
            // Note this is not really part of the test, so we expect there
            // to be enough XRP reserve for acct to create the trust line.
            if (t.preTrust == acctPreTrust)
                env(trust(acct, USD(1)));
            env.close();

            {
                // Acct creates an offer.  This is the heart of the test.
                auto const acctOffer = t.offerAmount;
                env(offer(acct, USD(acctOffer), XRP(acctOffer)), ter(t.tec));
                env.close();
            }
            std::uint32_t const acctOfferSeq = env.seq(acct) - 1;

            BEAST_EXPECT(env.balance(acct, USD.issue()) == t.balanceUsd);
            BEAST_EXPECT(env.balance(acct, xrpIssue()) == t.fundXrp - t.spentXrp);
            env.require(offers(acct, t.offers));
            env.require(owners(acct, t.owners));

            auto acctOffers = offersOnAccount(env, acct);
            BEAST_EXPECT(acctOffers.size() == t.offers);
            if (!acctOffers.empty() && (t.offers != 0))
            {
                auto const& acctOffer = *(acctOffers.front());

                auto const leftover = t.offerAmount - t.bookAmount;
                BEAST_EXPECT(acctOffer[sfTakerGets] == XRP(leftover));
                BEAST_EXPECT(acctOffer[sfTakerPays] == USD(leftover));
            }

            if (t.preTrust == noPreTrust)
            {
                if (t.balanceUsd.value().signum() != 0)
                {
                    // Verify the correct contents of the trustline
                    verifyDefaultTrustline(env, acct, t.balanceUsd);
                }
                else
                {
                    // Verify that no trustline was created.
                    auto const sleTrust = env.le(keylet::line(acct, USD.issue()));
                    BEAST_EXPECT(!sleTrust);
                }
            }

            // Give the next loop a clean slate by canceling any left-overs
            // in the offers.
            env(offer_cancel(acct, acctOfferSeq));
            env(offer_cancel(gw, gwOfferSeq));
            env.close();
        }
    }

    void
    testXRPDirectCross(FeatureBitset features)
    {
        testcase("XRP Direct Crossing");

        using namespace jtx;

        auto const gw = Account("gateway");
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const USD = gw["USD"];

        auto const usdOffer = USD(1000);
        auto const xrpOffer = XRP(1000);

        Env env{*this, features};

        env.fund(XRP(1000000), gw, bob);
        env.close();

        // The fee that's charged for transactions.
        auto const fee = env.current()->fees().base;

        // alice's account has enough for the reserve, one trust line plus two
        // offers, and two fees.
        env.fund(reserve(env, 2) + fee * 2, alice);
        env.close();

        env(trust(alice, usdOffer));

        env.close();

        env(pay(gw, alice, usdOffer));
        env.close();
        env.require(balance(alice, usdOffer), offers(alice, 0), offers(bob, 0));

        // The scenario:
        //   o alice has USD but wants XRP.
        //   o bob has XRP but wants USD.
        auto const aliceXRP = env.balance(alice);
        auto const bobXRP = env.balance(bob);

        env(offer(alice, xrpOffer, usdOffer));
        env.close();
        env(offer(bob, usdOffer, xrpOffer));

        env.close();
        env.require(
            balance(alice, USD(0)),
            balance(bob, usdOffer),
            balance(alice, aliceXRP + xrpOffer - fee),
            balance(bob, bobXRP - xrpOffer - fee),
            offers(alice, 0),
            offers(bob, 0));

        verifyDefaultTrustline(env, bob, usdOffer);

        // Make two more offers that leave one of the offers non-dry.
        env(offer(alice, USD(999), XRP(999)));
        env(offer(bob, xrpOffer, usdOffer));

        env.close();
        env.require(balance(alice, USD(999)));
        env.require(balance(bob, USD(1)));
        env.require(offers(alice, 0));
        verifyDefaultTrustline(env, bob, USD(1));
        {
            auto const bobOffers = offersOnAccount(env, bob);
            BEAST_EXPECT(bobOffers.size() == 1);
            auto const& bobOffer = *(bobOffers.front());

            BEAST_EXPECT(bobOffer[sfLedgerEntryType] == ltOFFER);
            BEAST_EXPECT(bobOffer[sfTakerGets] == USD(1));
            BEAST_EXPECT(bobOffer[sfTakerPays] == XRP(1));
        }
    }

    void
    testDirectCross(FeatureBitset features)
    {
        testcase("Direct Crossing");

        using namespace jtx;

        auto const gw = Account("gateway");
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const USD = gw["USD"];
        auto const EUR = gw["EUR"];

        auto const usdOffer = USD(1000);
        auto const eurOffer = EUR(1000);

        Env env{*this, features};

        env.fund(XRP(1000000), gw);
        env.close();

        // The fee that's charged for transactions.
        auto const fee = env.current()->fees().base;

        // Each account has enough for the reserve, two trust lines, one
        // offer, and two fees.
        env.fund(reserve(env, 3) + fee * 3, alice);
        env.fund(reserve(env, 3) + fee * 2, bob);
        env.close();

        env(trust(alice, usdOffer));
        env(trust(bob, eurOffer));
        env.close();

        env(pay(gw, alice, usdOffer));
        env(pay(gw, bob, eurOffer));
        env.close();

        env.require(balance(alice, usdOffer), balance(bob, eurOffer));

        // The scenario:
        //   o alice has USD but wants EUR.
        //   o bob has EUR but wants USD.
        env(offer(alice, eurOffer, usdOffer));
        env(offer(bob, usdOffer, eurOffer));

        env.close();
        env.require(
            balance(alice, eurOffer), balance(bob, usdOffer), offers(alice, 0), offers(bob, 0));

        // Alice's offer crossing created a default EUR trustline and
        // Bob's offer crossing created a default USD trustline:
        verifyDefaultTrustline(env, alice, eurOffer);
        verifyDefaultTrustline(env, bob, usdOffer);

        // Make two more offers that leave one of the offers non-dry.
        // Guarantee the order of application by putting a close()
        // between them.
        env(offer(bob, eurOffer, usdOffer));
        env.close();

        env(offer(alice, USD(999), eurOffer));
        env.close();

        env.require(offers(alice, 0));
        env.require(offers(bob, 1));

        env.require(balance(alice, USD(999)));
        env.require(balance(alice, EUR(1)));
        env.require(balance(bob, USD(1)));
        env.require(balance(bob, EUR(999)));

        {
            auto bobOffers = offersOnAccount(env, bob);
            if (BEAST_EXPECT(bobOffers.size() == 1))
            {
                auto const& bobOffer = *(bobOffers.front());

                BEAST_EXPECT(bobOffer[sfTakerGets] == USD(1));
                BEAST_EXPECT(bobOffer[sfTakerPays] == EUR(1));
            }
        }

        // alice makes one more offer that cleans out bob's offer.
        env(offer(alice, USD(1), EUR(1)));
        env.close();

        env.require(balance(alice, USD(1000)));
        env.require(balance(alice, EUR(none)));
        env.require(balance(bob, USD(none)));
        env.require(balance(bob, EUR(1000)));
        env.require(offers(alice, 0));
        env.require(offers(bob, 0));

        // The two trustlines that were generated by offers should be gone.
        BEAST_EXPECT(!env.le(keylet::line(alice.id(), EUR.issue())));
        BEAST_EXPECT(!env.le(keylet::line(bob.id(), USD.issue())));

        // Make two more offers that leave one of the offers non-dry. We
        // need to properly sequence the transactions:
        env(offer(alice, EUR(999), usdOffer));
        env.close();

        env(offer(bob, usdOffer, eurOffer));
        env.close();

        env.require(offers(alice, 0));
        env.require(offers(bob, 0));

        env.require(balance(alice, USD(0)));
        env.require(balance(alice, EUR(999)));
        env.require(balance(bob, USD(1000)));
        env.require(balance(bob, EUR(1)));
    }

    void
    testBridgedCross(FeatureBitset features)
    {
        testcase("Bridged Crossing");

        using namespace jtx;

        auto const gw = Account("gateway");
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const carol = Account("carol");
        auto const USD = gw["USD"];
        auto const EUR = gw["EUR"];

        auto const usdOffer = USD(1000);
        auto const eurOffer = EUR(1000);

        Env env{*this, features};

        env.fund(XRP(1000000), gw, alice, bob, carol);
        env.close();

        env(trust(alice, usdOffer));
        env(trust(carol, eurOffer));
        env.close();
        env(pay(gw, alice, usdOffer));
        env(pay(gw, carol, eurOffer));
        env.close();

        // The scenario:
        //   o alice has USD but wants XRP.
        //   o bob has XRP but wants EUR.
        //   o carol has EUR but wants USD.
        // Note that carol's offer must come last.  If carol's offer is placed
        // before bob's or alice's, then autobridging will not occur.
        env(offer(alice, XRP(1000), usdOffer));
        env(offer(bob, eurOffer, XRP(1000)));
        auto const bobXrpBalance = env.balance(bob);
        env.close();

        // carol makes an offer that partially consumes alice and bob's offers.
        env(offer(carol, USD(400), EUR(400)));
        env.close();

        env.require(
            balance(alice, USD(600)),
            balance(bob, EUR(400)),
            balance(carol, USD(400)),
            balance(bob, bobXrpBalance - XRP(400)),
            offers(carol, 0));
        verifyDefaultTrustline(env, bob, EUR(400));
        verifyDefaultTrustline(env, carol, USD(400));
        {
            auto const aliceOffers = offersOnAccount(env, alice);
            BEAST_EXPECT(aliceOffers.size() == 1);
            auto const& aliceOffer = *(aliceOffers.front());

            BEAST_EXPECT(aliceOffer[sfLedgerEntryType] == ltOFFER);
            BEAST_EXPECT(aliceOffer[sfTakerGets] == USD(600));
            BEAST_EXPECT(aliceOffer[sfTakerPays] == XRP(600));
        }
        {
            auto const bobOffers = offersOnAccount(env, bob);
            BEAST_EXPECT(bobOffers.size() == 1);
            auto const& bobOffer = *(bobOffers.front());

            BEAST_EXPECT(bobOffer[sfLedgerEntryType] == ltOFFER);
            BEAST_EXPECT(bobOffer[sfTakerGets] == XRP(600));
            BEAST_EXPECT(bobOffer[sfTakerPays] == EUR(600));
        }

        // carol makes an offer that exactly consumes alice and bob's offers.
        env(offer(carol, USD(600), EUR(600)));
        env.close();

        env.require(
            balance(alice, USD(0)),
            balance(bob, eurOffer),
            balance(carol, usdOffer),
            balance(bob, bobXrpBalance - XRP(1000)),
            offers(bob, 0),
            offers(carol, 0));
        verifyDefaultTrustline(env, bob, EUR(1000));
        verifyDefaultTrustline(env, carol, USD(1000));

        // In pre-flow code alice's offer is left empty in the ledger.
        auto const aliceOffers = offersOnAccount(env, alice);
        if (!aliceOffers.empty())
        {
            BEAST_EXPECT(aliceOffers.size() == 1);
            auto const& aliceOffer = *(aliceOffers.front());

            BEAST_EXPECT(aliceOffer[sfLedgerEntryType] == ltOFFER);
            BEAST_EXPECT(aliceOffer[sfTakerGets] == USD(0));
            BEAST_EXPECT(aliceOffer[sfTakerPays] == XRP(0));
        }
    }

    void
    testSellOffer(FeatureBitset features)
    {
        // Test a number of different corner cases regarding offer crossing
        // when the tfSell flag is set.  The test is table driven so it
        // should be easy to add or remove tests.
        testcase("Sell Offer");

        using namespace jtx;

        auto const gw = Account("gateway");
        auto const USD = gw["USD"];

        Env env{*this, features};

        env.fund(XRP(10000000), gw);
        env.close();

        // The fee that's charged for transactions
        auto const f = env.current()->fees().base;

        // To keep things simple all offers are 1 : 1 for XRP : USD.
        enum preTrustType { noPreTrust, gwPreTrust, acctPreTrust };
        struct TestData
        {
            std::string account;  // Account operated on
            STAmount fundXrp;     // XRP acct funded with
            STAmount fundUSD;     // USD acct funded with
            STAmount gwGets;      // gw's offer
            STAmount gwPays;      //
            STAmount acctGets;    // acct's offer
            STAmount acctPays;    //
            TER tec;              // Returned tec code
            STAmount spentXrp;    // Amount removed from fundXrp
            STAmount finalUsd;    // Final USD balance on acct
            int offers;           // Offers on acct
            int owners;           // Owners on acct
            STAmount takerGets;   // Remainder of acct's offer
            STAmount takerPays;   //

            // Constructor with takerGets/takerPays
            TestData(
                std::string&& account_,      // Account operated on
                STAmount const& fundXrp_,    // XRP acct funded with
                STAmount const& fundUSD_,    // USD acct funded with
                STAmount const& gwGets_,     // gw's offer
                STAmount const& gwPays_,     //
                STAmount const& acctGets_,   // acct's offer
                STAmount const& acctPays_,   //
                TER tec_,                    // Returned tec code
                STAmount const& spentXrp_,   // Amount removed from fundXrp
                STAmount const& finalUsd_,   // Final USD balance on acct
                int offers_,                 // Offers on acct
                int owners_,                 // Owners on acct
                STAmount const& takerGets_,  // Remainder of acct's offer
                STAmount const& takerPays_)  //
                : account(std::move(account_))
                , fundXrp(fundXrp_)
                , fundUSD(fundUSD_)
                , gwGets(gwGets_)
                , gwPays(gwPays_)
                , acctGets(acctGets_)
                , acctPays(acctPays_)
                , tec(tec_)
                , spentXrp(spentXrp_)
                , finalUsd(finalUsd_)
                , offers(offers_)
                , owners(owners_)
                , takerGets(takerGets_)
                , takerPays(takerPays_)
            {
            }

            // Constructor without takerGets/takerPays
            TestData(
                std::string&& account_,     // Account operated on
                STAmount const& fundXrp_,   // XRP acct funded with
                STAmount const& fundUSD_,   // USD acct funded with
                STAmount const& gwGets_,    // gw's offer
                STAmount const& gwPays_,    //
                STAmount const& acctGets_,  // acct's offer
                STAmount const& acctPays_,  //
                TER tec_,                   // Returned tec code
                STAmount const& spentXrp_,  // Amount removed from fundXrp
                STAmount const& finalUsd_,  // Final USD balance on acct
                int offers_,                // Offers on acct
                int owners_)                // Owners on acct
                : TestData(
                      std::move(account_),
                      fundXrp_,
                      fundUSD_,
                      gwGets_,
                      gwPays_,
                      acctGets_,
                      acctPays_,
                      tec_,
                      spentXrp_,
                      finalUsd_,
                      offers_,
                      owners_,
                      STAmount{0},
                      STAmount{0})
            {
            }
        };

        // clang-format off
        TestData const tests[]{
            // acct pays XRP
            // acct                           fundXrp  fundUSD   gwGets   gwPays acctGets acctPays                     tec            spentXrp  finalUSD offers  owners  takerGets  takerPays
            {"ann", XRP(10) + reserve(env, 0) + 1 * f, USD( 0), XRP(10), USD( 5), USD(10), XRP(10), tecINSUF_RESERVE_OFFER, XRP(  0) + (1 * f), USD( 0),      0,      0},
            {"bev", XRP(10) + reserve(env, 1) + 1 * f, USD( 0), XRP(10), USD( 5), USD(10), XRP(10),             tesSUCCESS, XRP(  0) + (1 * f), USD( 0),      1,      1,   XRP(10), USD(10)},
            {"cam", XRP(10) + reserve(env, 0) + 1 * f, USD( 0), XRP(10), USD(10), USD(10), XRP(10),             tesSUCCESS, XRP( 10) + (1 * f), USD(10),      0,      1},
            {"deb", XRP(10) + reserve(env, 0) + 1 * f, USD( 0), XRP(10), USD(20), USD(10), XRP(10),             tesSUCCESS, XRP( 10) + (1 * f), USD(20),      0,      1},
            {"eve", XRP(10) + reserve(env, 0) + 1 * f, USD( 0), XRP(10), USD(20), USD( 5), XRP( 5),             tesSUCCESS, XRP(  5) + (1 * f), USD(10),      0,      1},
            {"flo", XRP(10) + reserve(env, 0) + 1 * f, USD( 0), XRP(10), USD(20), USD(20), XRP(20),             tesSUCCESS, XRP( 10) + (1 * f), USD(20),      0,      1},
            {"gay", XRP(20) + reserve(env, 1) + 1 * f, USD( 0), XRP(10), USD(20), USD(20), XRP(20),             tesSUCCESS, XRP( 10) + (1 * f), USD(20),      0,      1},
            {"hye", XRP(20) + reserve(env, 2) + 1 * f, USD( 0), XRP(10), USD(20), USD(20), XRP(20),             tesSUCCESS, XRP( 10) + (1 * f), USD(20),      1,      2,   XRP(10), USD(10)},
            // acct pays USD
            {"meg",           reserve(env, 1) + 2 * f, USD(10), USD(10), XRP( 5), XRP(10), USD(10), tecINSUF_RESERVE_OFFER, XRP(  0) + (2 * f), USD(10),      0,      1},
            {"nia",           reserve(env, 2) + 2 * f, USD(10), USD(10), XRP( 5), XRP(10), USD(10),             tesSUCCESS, XRP(  0) + (2 * f), USD(10),      1,      2,   USD(10), XRP(10)},
            {"ova",           reserve(env, 1) + 2 * f, USD(10), USD(10), XRP(10), XRP(10), USD(10),             tesSUCCESS, XRP(-10) + (2 * f), USD( 0),      0,      1},
            {"pam",           reserve(env, 1) + 2 * f, USD(10), USD(10), XRP(20), XRP(10), USD(10),             tesSUCCESS, XRP(-20) + (2 * f), USD( 0),      0,      1},
            {"qui",           reserve(env, 1) + 2 * f, USD(10), USD(20), XRP(40), XRP(10), USD(10),             tesSUCCESS, XRP(-20) + (2 * f), USD( 0),      0,      1},
            {"rae",           reserve(env, 2) + 2 * f, USD(10), USD( 5), XRP( 5), XRP(10), USD(10),             tesSUCCESS, XRP( -5) + (2 * f), USD( 5),      1,      2,   USD( 5), XRP( 5)},
            {"sue",           reserve(env, 2) + 2 * f, USD(10), USD( 5), XRP(10), XRP(10), USD(10),             tesSUCCESS, XRP(-10) + (2 * f), USD( 5),      1,      2,   USD( 5), XRP( 5)},
        };
        // clang-format on

        auto const zeroUsd = USD(0);
        for (auto const& t : tests)
        {
            // Make sure gateway has no current offers.
            env.require(offers(gw, 0));

            auto const acct = Account(t.account);

            env.fund(t.fundXrp, acct);
            env.close();

            // Optionally give acct some USD.  This is not part of the test,
            // so we assume that acct has sufficient USD to cover the reserve
            // on the trust line.
            if (t.fundUSD != zeroUsd)
            {
                env(trust(acct, t.fundUSD));
                env.close();
                env(pay(gw, acct, t.fundUSD));
                env.close();
            }

            env(offer(gw, t.gwGets, t.gwPays));
            env.close();
            std::uint32_t const gwOfferSeq = env.seq(gw) - 1;

            // Acct creates a tfSell offer.  This is the heart of the test.
            env(offer(acct, t.acctGets, t.acctPays, tfSell), ter(t.tec));
            env.close();
            std::uint32_t const acctOfferSeq = env.seq(acct) - 1;

            // Check results
            BEAST_EXPECT(env.balance(acct, USD.issue()) == t.finalUsd);
            BEAST_EXPECT(env.balance(acct, xrpIssue()) == t.fundXrp - t.spentXrp);
            env.require(offers(acct, t.offers));
            env.require(owners(acct, t.owners));

            if (t.offers != 0)
            {
                auto const acctOffers = offersOnAccount(env, acct);
                if (!acctOffers.empty())
                {
                    BEAST_EXPECT(acctOffers.size() == 1);
                    auto const& acctOffer = *(acctOffers.front());

                    BEAST_EXPECT(acctOffer[sfLedgerEntryType] == ltOFFER);
                    BEAST_EXPECT(acctOffer[sfTakerGets] == t.takerGets);
                    BEAST_EXPECT(acctOffer[sfTakerPays] == t.takerPays);
                }
            }

            // Give the next loop a clean slate by canceling any left-overs
            // in the offers.
            env(offer_cancel(acct, acctOfferSeq));
            env(offer_cancel(gw, gwOfferSeq));
            env.close();
        }
    }

    void
    testSellWithFillOrKill(FeatureBitset features)
    {
        // Test a number of different corner cases regarding offer crossing
        // when both the tfSell flag and tfFillOrKill flags are set.
        testcase("Combine tfSell with tfFillOrKill");

        using namespace jtx;

        auto const gw = Account("gateway");
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const USD = gw["USD"];

        Env env{*this, features};

        env.fund(XRP(10000000), gw, alice, bob);
        env.close();

        // Code returned if an offer is killed.
        TER const killedCode{TER{tecKILLED}};

        // bob offers XRP for USD.
        env(trust(bob, USD(200)));
        env.close();
        env(pay(gw, bob, USD(100)));
        env.close();
        env(offer(bob, XRP(2000), USD(20)));
        env.close();
        {
            // alice submits a tfSell | tfFillOrKill offer that does not cross.
            env(offer(alice, USD(21), XRP(2100), tfSell | tfFillOrKill), ter(killedCode));
            env.close();
            env.require(balance(alice, USD(none)));
            env.require(offers(alice, 0));
            env.require(balance(bob, USD(100)));
        }
        {
            // alice submits a tfSell | tfFillOrKill offer that crosses.
            // Even though tfSell is present it doesn't matter this time.
            env(offer(alice, USD(20), XRP(2000), tfSell | tfFillOrKill));
            env.close();
            env.require(balance(alice, USD(20)));
            env.require(offers(alice, 0));
            env.require(balance(bob, USD(80)));
        }
        {
            // alice submits a tfSell | tfFillOrKill offer that crosses and
            // returns more than was asked for (because of the tfSell flag).
            env(offer(bob, XRP(2000), USD(20)));
            env.close();
            env(offer(alice, USD(10), XRP(1500), tfSell | tfFillOrKill));
            env.close();
            env.require(balance(alice, USD(35)));
            env.require(offers(alice, 0));
            env.require(balance(bob, USD(65)));
        }
        {
            // alice submits a tfSell | tfFillOrKill offer that doesn't cross.
            // This would have succeeded with a regular tfSell, but the
            // fillOrKill prevents the transaction from crossing since not
            // all of the offer is consumed.

            // We're using bob's left-over offer for XRP(500), USD(5)
            env(offer(alice, USD(1), XRP(501), tfSell | tfFillOrKill), ter(killedCode));
            env.close();
            env.require(balance(alice, USD(35)));
            env.require(offers(alice, 0));
            env.require(balance(bob, USD(65)));
        }
        {
            // Alice submits a tfSell | tfFillOrKill offer that finishes
            // off the remainder of bob's offer.

            // We're using bob's left-over offer for XRP(500), USD(5)
            env(offer(alice, USD(1), XRP(500), tfSell | tfFillOrKill));
            env.close();
            env.require(balance(alice, USD(40)));
            env.require(offers(alice, 0));
            env.require(balance(bob, USD(60)));
        }
    }

    void
    testTransferRateOffer(FeatureBitset features)
    {
        testcase("Transfer Rate Offer");

        using namespace jtx;

        auto const gw1 = Account("gateway1");
        auto const USD = gw1["USD"];

        Env env{*this, features};

        // The fee that's charged for transactions.
        auto const fee = env.current()->fees().base;

        env.fund(XRP(100000), gw1);
        env.close();

        env(rate(gw1, 1.25));
        {
            auto const ann = Account("ann");
            auto const bob = Account("bob");
            env.fund(XRP(100) + reserve(env, 2) + (fee * 2), ann, bob);
            env.close();

            env(trust(ann, USD(200)));
            env(trust(bob, USD(200)));
            env.close();

            env(pay(gw1, bob, USD(125)));
            env.close();

            // bob offers to sell USD(100) for XRP.  alice takes bob's offer.
            // Notice that although bob only offered USD(100), USD(125) was
            // removed from his account due to the gateway fee.
            //
            // A comparable payment would look like this:
            //   env (pay (bob, alice, USD(100)), sendmax(USD(125)))
            env(offer(bob, XRP(1), USD(100)));
            env.close();

            env(offer(ann, USD(100), XRP(1)));
            env.close();

            env.require(balance(ann, USD(100)));
            env.require(balance(ann, XRP(99) + reserve(env, 2)));
            env.require(offers(ann, 0));

            env.require(balance(bob, USD(0)));
            env.require(balance(bob, XRP(101) + reserve(env, 2)));
            env.require(offers(bob, 0));
        }
        {
            // Reverse the order, so the offer in the books is to sell XRP
            // in return for USD.  Gateway rate should still apply identically.
            auto const che = Account("che");
            auto const deb = Account("deb");
            env.fund(XRP(100) + reserve(env, 2) + (fee * 2), che, deb);
            env.close();

            env(trust(che, USD(200)));
            env(trust(deb, USD(200)));
            env.close();

            env(pay(gw1, deb, USD(125)));
            env.close();

            env(offer(che, USD(100), XRP(1)));
            env.close();

            env(offer(deb, XRP(1), USD(100)));
            env.close();

            env.require(balance(che, USD(100)));
            env.require(balance(che, XRP(99) + reserve(env, 2)));
            env.require(offers(che, 0));

            env.require(balance(deb, USD(0)));
            env.require(balance(deb, XRP(101) + reserve(env, 2)));
            env.require(offers(deb, 0));
        }
        {
            auto const eve = Account("eve");
            auto const fyn = Account("fyn");

            env.fund(XRP(20000) + (fee * 2), eve, fyn);
            env.close();

            env(trust(eve, USD(1000)));
            env(trust(fyn, USD(1000)));
            env.close();

            env(pay(gw1, eve, USD(100)));
            env(pay(gw1, fyn, USD(100)));
            env.close();

            // This test verifies that the amount removed from an offer
            // accounts for the transfer fee that is removed from the
            // account but not from the remaining offer.
            env(offer(eve, USD(10), XRP(4000)));
            env.close();
            std::uint32_t const eveOfferSeq = env.seq(eve) - 1;

            env(offer(fyn, XRP(2000), USD(5)));
            env.close();

            env.require(balance(eve, USD(105)));
            env.require(balance(eve, XRP(18000)));
            auto const evesOffers = offersOnAccount(env, eve);
            BEAST_EXPECT(evesOffers.size() == 1);
            if (!evesOffers.empty())
            {
                auto const& evesOffer = *(evesOffers.front());
                BEAST_EXPECT(evesOffer[sfLedgerEntryType] == ltOFFER);
                BEAST_EXPECT(evesOffer[sfTakerGets] == XRP(2000));
                BEAST_EXPECT(evesOffer[sfTakerPays] == USD(5));
            }
            env(offer_cancel(eve, eveOfferSeq));  // For later tests

            env.require(balance(fyn, USD(93.75)));
            env.require(balance(fyn, XRP(22000)));
            env.require(offers(fyn, 0));
        }
        // Start messing with two non-native currencies.
        auto const gw2 = Account("gateway2");
        auto const EUR = gw2["EUR"];

        env.fund(XRP(100000), gw2);
        env.close();

        env(rate(gw2, 1.5));
        {
            // Remove XRP from the equation.  Give the two currencies two
            // different transfer rates so we can see both transfer rates
            // apply in the same transaction.
            auto const gay = Account("gay");
            auto const hal = Account("hal");
            env.fund(reserve(env, 3) + (fee * 3), gay, hal);
            env.close();

            env(trust(gay, USD(200)));
            env(trust(gay, EUR(200)));
            env(trust(hal, USD(200)));
            env(trust(hal, EUR(200)));
            env.close();

            env(pay(gw1, gay, USD(125)));
            env(pay(gw2, hal, EUR(150)));
            env.close();

            env(offer(gay, EUR(100), USD(100)));
            env.close();

            env(offer(hal, USD(100), EUR(100)));
            env.close();

            env.require(balance(gay, USD(0)));
            env.require(balance(gay, EUR(100)));
            env.require(balance(gay, reserve(env, 3)));
            env.require(offers(gay, 0));

            env.require(balance(hal, USD(100)));
            env.require(balance(hal, EUR(0)));
            env.require(balance(hal, reserve(env, 3)));
            env.require(offers(hal, 0));
        }
        {
            // A trust line's QualityIn should not affect offer crossing.
            auto const ivy = Account("ivy");
            auto const joe = Account("joe");
            env.fund(reserve(env, 3) + (fee * 3), ivy, joe);
            env.close();

            env(trust(ivy, USD(400)), qualityInPercent(90));
            env(trust(ivy, EUR(400)), qualityInPercent(80));
            env(trust(joe, USD(400)), qualityInPercent(70));
            env(trust(joe, EUR(400)), qualityInPercent(60));
            env.close();

            env(pay(gw1, ivy, USD(270)), sendmax(USD(500)));
            env(pay(gw2, joe, EUR(150)), sendmax(EUR(300)));
            env.close();
            env.require(balance(ivy, USD(300)));
            env.require(balance(joe, EUR(250)));

            env(offer(ivy, EUR(100), USD(200)));
            env.close();

            env(offer(joe, USD(200), EUR(100)));
            env.close();

            env.require(balance(ivy, USD(50)));
            env.require(balance(ivy, EUR(100)));
            env.require(balance(ivy, reserve(env, 3)));
            env.require(offers(ivy, 0));

            env.require(balance(joe, USD(200)));
            env.require(balance(joe, EUR(100)));
            env.require(balance(joe, reserve(env, 3)));
            env.require(offers(joe, 0));
        }
        {
            // A trust line's QualityOut should not affect offer crossing.
            auto const kim = Account("kim");
            auto const K_BUX = kim["BUX"];
            auto const lex = Account("lex");
            auto const meg = Account("meg");
            auto const ned = Account("ned");
            auto const N_BUX = ned["BUX"];

            // Verify trust line QualityOut affects payments.
            env.fund(reserve(env, 4) + (fee * 4), kim, lex, meg, ned);
            env.close();

            env(trust(lex, K_BUX(400)));
            env(trust(lex, N_BUX(200)), qualityOutPercent(120));
            env(trust(meg, N_BUX(100)));
            env.close();
            env(pay(ned, lex, N_BUX(100)));
            env.close();
            env.require(balance(lex, N_BUX(100)));

            env(pay(kim, meg, N_BUX(60)), path(lex, ned), sendmax(K_BUX(200)));
            env.close();

            env.require(balance(kim, K_BUX(none)));
            env.require(balance(kim, N_BUX(none)));
            env.require(balance(lex, K_BUX(72)));
            env.require(balance(lex, N_BUX(40)));
            env.require(balance(meg, K_BUX(none)));
            env.require(balance(meg, N_BUX(60)));
            env.require(balance(ned, K_BUX(none)));
            env.require(balance(ned, N_BUX(none)));

            // Now verify that offer crossing is unaffected by QualityOut.
            env(offer(lex, K_BUX(30), N_BUX(30)));
            env.close();

            env(offer(kim, N_BUX(30), K_BUX(30)));
            env.close();

            env.require(balance(kim, K_BUX(none)));
            env.require(balance(kim, N_BUX(30)));
            env.require(balance(lex, K_BUX(102)));
            env.require(balance(lex, N_BUX(10)));
            env.require(balance(meg, K_BUX(none)));
            env.require(balance(meg, N_BUX(60)));
            env.require(balance(ned, K_BUX(-30)));
            env.require(balance(ned, N_BUX(none)));
        }
        {
            // Make sure things work right when we're auto-bridging as well.
            auto const ova = Account("ova");
            auto const pat = Account("pat");
            auto const qae = Account("qae");
            env.fund(XRP(2) + reserve(env, 3) + (fee * 3), ova, pat, qae);
            env.close();

            //   o ova has USD but wants XRP.
            //   o pat has XRP but wants EUR.
            //   o qae has EUR but wants USD.
            env(trust(ova, USD(200)));
            env(trust(ova, EUR(200)));
            env(trust(pat, USD(200)));
            env(trust(pat, EUR(200)));
            env(trust(qae, USD(200)));
            env(trust(qae, EUR(200)));
            env.close();

            env(pay(gw1, ova, USD(125)));
            env(pay(gw2, qae, EUR(150)));
            env.close();

            env(offer(ova, XRP(2), USD(100)));
            env(offer(pat, EUR(100), XRP(2)));
            env.close();

            env(offer(qae, USD(100), EUR(100)));
            env.close();

            env.require(balance(ova, USD(0)));
            env.require(balance(ova, EUR(0)));
            env.require(balance(ova, XRP(4) + reserve(env, 3)));

            // In pre-flow code ova's offer is left empty in the ledger.
            auto const ovasOffers = offersOnAccount(env, ova);
            if (!ovasOffers.empty())
            {
                BEAST_EXPECT(ovasOffers.size() == 1);
                auto const& ovasOffer = *(ovasOffers.front());

                BEAST_EXPECT(ovasOffer[sfLedgerEntryType] == ltOFFER);
                BEAST_EXPECT(ovasOffer[sfTakerGets] == USD(0));
                BEAST_EXPECT(ovasOffer[sfTakerPays] == XRP(0));
            }

            env.require(balance(pat, USD(0)));
            env.require(balance(pat, EUR(100)));
            env.require(balance(pat, XRP(0) + reserve(env, 3)));
            env.require(offers(pat, 0));

            env.require(balance(qae, USD(100)));
            env.require(balance(qae, EUR(0)));
            env.require(balance(qae, XRP(2) + reserve(env, 3)));
            env.require(offers(qae, 0));
        }
    }

    void
    testSelfCrossOffer1(FeatureBitset features)
    {
        // The following test verifies some correct but slightly surprising
        // behavior in offer crossing.  The scenario:
        //
        //  o An entity has created one or more offers.
        //  o The entity creates another offer that can be directly crossed
        //    (not autobridged) by the previously created offer(s).
        //  o Rather than self crossing the offers, delete the old offer(s).
        //
        // See a more complete explanation in the comments for
        // BookOfferCrossingStep::limitSelfCrossQuality().
        //
        // Note that, in this particular example, one offer causes several
        // crossable offers (worth considerably more than the new offer)
        // to be removed from the book.
        using namespace jtx;

        auto const gw = Account("gateway");
        auto const USD = gw["USD"];

        Env env{*this, features};

        // The fee that's charged for transactions.
        auto const fee = env.current()->fees().base;
        auto const startBalance = XRP(1000000);

        env.fund(startBalance + (fee * 4), gw);
        env.close();

        env(offer(gw, USD(60), XRP(600)));
        env.close();
        env(offer(gw, USD(60), XRP(600)));
        env.close();
        env(offer(gw, USD(60), XRP(600)));
        env.close();

        env.require(owners(gw, 3));
        env.require(balance(gw, startBalance + fee));

        auto gwOffers = offersOnAccount(env, gw);
        BEAST_EXPECT(gwOffers.size() == 3);
        for (auto const& offerPtr : gwOffers)
        {
            auto const& offer = *offerPtr;
            BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
            BEAST_EXPECT(offer[sfTakerGets] == XRP(600));
            BEAST_EXPECT(offer[sfTakerPays] == USD(60));
        }

        // Since this offer crosses the first offers, the previous offers
        // will be deleted and this offer will be put on the order book.
        env(offer(gw, XRP(1000), USD(100)));
        env.close();
        env.require(owners(gw, 1));
        env.require(offers(gw, 1));
        env.require(balance(gw, startBalance));

        gwOffers = offersOnAccount(env, gw);
        BEAST_EXPECT(gwOffers.size() == 1);
        for (auto const& offerPtr : gwOffers)
        {
            auto const& offer = *offerPtr;
            BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
            BEAST_EXPECT(offer[sfTakerGets] == USD(100));
            BEAST_EXPECT(offer[sfTakerPays] == XRP(1000));
        }
    }

    void
    testSelfCrossOffer2(FeatureBitset features)
    {
        using namespace jtx;

        auto const gw1 = Account("gateway1");
        auto const gw2 = Account("gateway2");
        auto const alice = Account("alice");
        auto const USD = gw1["USD"];
        auto const EUR = gw2["EUR"];

        Env env{*this, features};

        env.fund(XRP(1000000), gw1, gw2);
        env.close();

        // The fee that's charged for transactions.
        auto const f = env.current()->fees().base;

        // Test cases
        struct TestData
        {
            std::string acct;    // Account operated on
            STAmount fundXRP;    // XRP acct funded with
            STAmount fundUSD;    // USD acct funded with
            STAmount fundEUR;    // EUR acct funded with
            TER firstOfferTec;   // tec code on first offer
            TER secondOfferTec;  // tec code on second offer
        };

        // clang-format off
        TestData const tests[]{
            // acct                 fundXRP   fundUSD    fundEUR            firstOfferTec           secondOfferTec
            {"ann", reserve(env, 3) + f * 4, USD(1000), EUR(1000),             tesSUCCESS,             tesSUCCESS},
            {"bev", reserve(env, 3) + f * 4, USD(   1), EUR(1000),             tesSUCCESS,             tesSUCCESS},
            {"cam", reserve(env, 3) + f * 4, USD(1000), EUR(   1),             tesSUCCESS,             tesSUCCESS},
            {"deb", reserve(env, 3) + f * 4, USD(   0), EUR(   1),             tesSUCCESS,      tecUNFUNDED_OFFER},
            {"eve", reserve(env, 3) + f * 4, USD(   1), EUR(   0),      tecUNFUNDED_OFFER,             tesSUCCESS},
            {"flo", reserve(env, 3) +     0, USD(1000), EUR(1000), tecINSUF_RESERVE_OFFER, tecINSUF_RESERVE_OFFER},
        };
        //clang-format on

        for (auto const& t : tests)
        {
            auto const acct = Account{t.acct};
            env.fund(t.fundXRP, acct);
            env.close();

            env(trust(acct, USD(1000)));
            env(trust(acct, EUR(1000)));
            env.close();

            if (t.fundUSD > USD(0))
                env(pay(gw1, acct, t.fundUSD));
            if (t.fundEUR > EUR(0))
                env(pay(gw2, acct, t.fundEUR));
            env.close();

            env(offer(acct, USD(500), EUR(600)), ter(t.firstOfferTec));
            env.close();
            std::uint32_t const firstOfferSeq = env.seq(acct) - 1;

            int offerCount = isTesSuccess(t.firstOfferTec) ? 1 : 0;
            env.require(owners(acct, 2 + offerCount));
            env.require(balance(acct, t.fundUSD));
            env.require(balance(acct, t.fundEUR));

            auto acctOffers = offersOnAccount(env, acct);
            BEAST_EXPECT(acctOffers.size() == offerCount);
            for (auto const& offerPtr : acctOffers)
            {
                auto const& offer = *offerPtr;
                BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
                BEAST_EXPECT(offer[sfTakerGets] == EUR(600));
                BEAST_EXPECT(offer[sfTakerPays] == USD(500));
            }

            env(offer(acct, EUR(600), USD(500)), ter(t.secondOfferTec));
            env.close();
            std::uint32_t const secondOfferSeq = env.seq(acct) - 1;

            offerCount = isTesSuccess(t.secondOfferTec) ? 1 : offerCount;
            env.require(owners(acct, 2 + offerCount));
            env.require(balance(acct, t.fundUSD));
            env.require(balance(acct, t.fundEUR));

            acctOffers = offersOnAccount(env, acct);
            BEAST_EXPECT(acctOffers.size() == offerCount);
            for (auto const& offerPtr : acctOffers)
            {
                auto const& offer = *offerPtr;
                BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
                if (offer[sfSequence] == firstOfferSeq)
                {
                    BEAST_EXPECT(offer[sfTakerGets] == EUR(600));
                    BEAST_EXPECT(offer[sfTakerPays] == USD(500));
                }
                else
                {
                    BEAST_EXPECT(offer[sfTakerGets] == USD(500));
                    BEAST_EXPECT(offer[sfTakerPays] == EUR(600));
                }
            }

            // Remove any offers from acct for the next pass.
            env(offer_cancel(acct, firstOfferSeq));
            env.close();
            env(offer_cancel(acct, secondOfferSeq));
            env.close();
        }
    }

    void
    testSelfCrossOffer(FeatureBitset features)
    {
        testcase("Self Cross Offer");
        testSelfCrossOffer1(features);
        testSelfCrossOffer2(features);
    }

    void
    testSelfIssueOffer(FeatureBitset features)
    {
        // Folks who issue their own currency have, in effect, as many
        // funds as they are trusted for.  This test used to fail because
        // self-issuing was not properly checked.  Verify that it works
        // correctly now.
        using namespace jtx;

        Env env{*this, features};

        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const USD = bob["USD"];
        auto const f = env.current()->fees().base;

        env.fund(XRP(50000) + f, alice, bob);
        env.close();

        env(offer(alice, USD(5000), XRP(50000)));
        env.close();

        // This offer should take alice's offer up to Alice's reserve.
        env(offer(bob, XRP(50000), USD(5000)));
        env.close();

        // alice's offer should have been removed, since she's down to her
        // XRP reserve.
        env.require(balance(alice, XRP(250)));
        env.require(owners(alice, 1));
        env.require(lines(alice, 1));

        // However bob's offer should be in the ledger, since it was not
        // fully crossed.
        auto const bobOffers = offersOnAccount(env, bob);
        BEAST_EXPECT(bobOffers.size() == 1);
        for (auto const& offerPtr : bobOffers)
        {
            auto const& offer = *offerPtr;
            BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
            BEAST_EXPECT(offer[sfTakerGets] == USD(25));
            BEAST_EXPECT(offer[sfTakerPays] == XRP(250));
        }
    }

    void
    testBadPathAssert(FeatureBitset features)
    {
        // At one point in the past this invalid path caused an assert.  It
        // should not be possible for user-supplied data to cause an assert.
        // Make sure the assert is gone.
        testcase("Bad path assert");

        using namespace jtx;

        Env env{*this, features};

        // The fee that's charged for transactions.
        auto const fee = env.current()->fees().base;
        {
            // A trust line's QualityOut should not affect offer crossing.
            auto const ann = Account("ann");
            auto const A_BUX = ann["BUX"];
            auto const bob = Account("bob");
            auto const cam = Account("cam");
            auto const dan = Account("dan");
            auto const D_BUX = dan["BUX"];

            // Verify trust line QualityOut affects payments.
            env.fund(reserve(env, 4) + (fee * 4), ann, bob, cam, dan);
            env.close();

            env(trust(bob, A_BUX(400)));
            env(trust(bob, D_BUX(200)), qualityOutPercent(120));
            env(trust(cam, D_BUX(100)));
            env.close();
            env(pay(dan, bob, D_BUX(100)));
            env.close();
            env.require(balance(bob, D_BUX(100)));

            env(pay(ann, cam, D_BUX(60)), path(bob, dan), sendmax(A_BUX(200)));
            env.close();

            env.require(balance(ann, A_BUX(none)));
            env.require(balance(ann, D_BUX(none)));
            env.require(balance(bob, A_BUX(72)));
            env.require(balance(bob, D_BUX(40)));
            env.require(balance(cam, A_BUX(none)));
            env.require(balance(cam, D_BUX(60)));
            env.require(balance(dan, A_BUX(none)));
            env.require(balance(dan, D_BUX(none)));

            env(offer(bob, A_BUX(30), D_BUX(30)));
            env.close();

            env(trust(ann, D_BUX(100)));
            env.close();

            // This payment caused the assert.
            env(pay(ann, ann, D_BUX(30)),
                path(A_BUX, D_BUX),
                sendmax(A_BUX(30)),
                ter(temBAD_PATH));
            env.close();

            env.require(balance(ann, A_BUX(none)));
            env.require(balance(ann, D_BUX(0)));
            env.require(balance(bob, A_BUX(72)));
            env.require(balance(bob, D_BUX(40)));
            env.require(balance(cam, A_BUX(none)));
            env.require(balance(cam, D_BUX(60)));
            env.require(balance(dan, A_BUX(0)));
            env.require(balance(dan, D_BUX(none)));
        }
    }

    void
    testDirectToDirectPath(FeatureBitset features)
    {
        // The offer crossing code expects that a DirectStep is always
        // preceded by a BookStep.  In one instance the default path
        // was not matching that assumption.  Here we recreate that case
        // so we can prove the bug stays fixed.
        testcase("Direct to Direct path");

        using namespace jtx;

        Env env{*this, features};

        auto const ann = Account("ann");
        auto const bob = Account("bob");
        auto const cam = Account("cam");
        auto const A_BUX = ann["BUX"];
        auto const B_BUX = bob["BUX"];

        auto const fee = env.current()->fees().base;
        env.fund(reserve(env, 4) + (fee * 5), ann, bob, cam);
        env.close();

        env(trust(ann, B_BUX(40)));
        env(trust(cam, A_BUX(40)));
        env(trust(cam, B_BUX(40)));
        env.close();

        env(pay(ann, cam, A_BUX(35)));
        env(pay(bob, cam, B_BUX(35)));

        env(offer(bob, A_BUX(30), B_BUX(30)));
        env.close();

        // cam puts an offer on the books that her upcoming offer could cross.
        // But this offer should be deleted, not crossed, by her upcoming
        // offer.
        env(offer(cam, A_BUX(29), B_BUX(30), tfPassive));
        env.close();
        env.require(balance(cam, A_BUX(35)));
        env.require(balance(cam, B_BUX(35)));
        env.require(offers(cam, 1));

        // This offer caused the assert.
        env(offer(cam, B_BUX(30), A_BUX(30)));
        env.close();

        env.require(balance(bob, A_BUX(30)));
        env.require(balance(cam, A_BUX(5)));
        env.require(balance(cam, B_BUX(65)));
        env.require(offers(cam, 0));
    }

    void
    testSelfCrossLowQualityOffer(FeatureBitset features)
    {
        // The Flow offer crossing code used to assert if an offer was made
        // for more XRP than the offering account held.  This unit test
        // reproduces that failing case.
        testcase("Self crossing low quality offer");

        using namespace jtx;

        Env env{*this, features};

        auto const ann = Account("ann");
        auto const gw = Account("gateway");
        auto const BTC = gw["BTC"];

        auto const fee = env.current()->fees().base;
        env.fund(reserve(env, 2) + drops(9999640) + (fee), ann);
        env.fund(reserve(env, 2) + (fee * 4), gw);
        env.close();

        env(rate(gw, 1.002));
        env(trust(ann, BTC(10)));
        env.close();

        env(pay(gw, ann, BTC(2.856)));
        env.close();

        env(offer(ann, drops(365611702030), BTC(5.713)));
        env.close();

        // This offer caused the assert.
        env(offer(ann, BTC(0.687), drops(20000000000)),
            ter(tecINSUF_RESERVE_OFFER));
    }

    void
    testOfferInScaling(FeatureBitset features)
    {
        // The Flow offer crossing code had a case where it was not rounding
        // the offer crossing correctly after a partial crossing.  The
        // failing case was found on the network.  Here we add the case to
        // the unit tests.
        testcase("Offer In Scaling");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account("gateway");
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const CNY = gw["CNY"];

        auto const fee = env.current()->fees().base;
        env.fund(reserve(env, 2) + drops(400000000000) + (fee), alice, bob);
        env.fund(reserve(env, 2) + (fee * 4), gw);
        env.close();

        env(trust(bob, CNY(500)));
        env.close();

        env(pay(gw, bob, CNY(300)));
        env.close();

        env(offer(bob, drops(5400000000), CNY(216.054)));
        env.close();

        // This offer did not round result of partial crossing correctly.
        env(offer(alice, CNY(13562.0001), drops(339000000000)));
        env.close();

        auto const aliceOffers = offersOnAccount(env, alice);
        BEAST_EXPECT(aliceOffers.size() == 1);
        for (auto const& offerPtr : aliceOffers)
        {
            auto const& offer = *offerPtr;
            BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
            BEAST_EXPECT(offer[sfTakerGets] == drops(333599446582));
            BEAST_EXPECT(offer[sfTakerPays] == CNY(13345.9461));
        }
    }

    void
    testOfferInScalingWithXferRate(FeatureBitset features)
    {
        // After adding the previous case, there were still failing rounding
        // cases in Flow offer crossing.  This one was because the gateway
        // transfer rate was not being correctly handled.
        testcase("Offer In Scaling With Xfer Rate");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account("gateway");
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const BTC = gw["BTC"];
        auto const JPY = gw["JPY"];

        auto const fee = env.current()->fees().base;
        env.fund(reserve(env, 2) + drops(400000000000) + (fee), alice, bob);
        env.fund(reserve(env, 2) + (fee * 4), gw);
        env.close();

        env(rate(gw, 1.002));
        env(trust(alice, JPY(4000)));
        env(trust(bob, BTC(2)));
        env.close();

        env(pay(gw, alice, JPY(3699.034802280317)));
        env(pay(gw, bob, BTC(1.156722559140311)));
        env.close();

        env(offer(bob, JPY(1241.913390770747), BTC(0.01969825690469254)));
        env.close();

        // This offer did not round result of partial crossing correctly.
        env(offer(alice, BTC(0.05507568706427876), JPY(3472.696773391072)));
        env.close();

        auto const aliceOffers = offersOnAccount(env, alice);
        BEAST_EXPECT(aliceOffers.size() == 1);
        for (auto const& offerPtr : aliceOffers)
        {
            auto const& offer = *offerPtr;
            BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
            BEAST_EXPECT(
                offer[sfTakerGets] ==
                STAmount(JPY.issue(), std::uint64_t(2230682446713524ul), -12));
            BEAST_EXPECT(offer[sfTakerPays] == BTC(0.035378));
        }
    }

    void
    testOfferThresholdWithReducedFunds(FeatureBitset features)
    {
        // Another instance where Flow offer crossing was not always
        // working right was if the Taker had fewer funds than the Offer
        // was offering.  The basis for this test came off the network.
        testcase("Offer Threshold With Reduced Funds");

        using namespace jtx;

        Env env{*this, features};

        auto const gw1 = Account("gw1");
        auto const gw2 = Account("gw2");
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const USD = gw1["USD"];
        auto const JPY = gw2["JPY"];

        auto const fee = env.current()->fees().base;
        env.fund(reserve(env, 2) + drops(400000000000) + (fee), alice, bob);
        env.fund(reserve(env, 2) + (fee * 4), gw1, gw2);
        env.close();

        env(rate(gw1, 1.002));
        env(trust(alice, USD(1000)));
        env(trust(bob, JPY(100000)));
        env.close();

        env(
            pay(gw1,
                alice,
                STAmount{USD.issue(), std::uint64_t(2185410179555600), -14}));
        env(
            pay(gw2,
                bob,
                STAmount{JPY.issue(), std::uint64_t(6351823459548956), -12}));
        env.close();

        env(offer(
            bob,
            STAmount{USD.issue(), std::uint64_t(4371257532306000), -17},
            STAmount{JPY.issue(), std::uint64_t(4573216636606000), -15}));
        env.close();

        // This offer did not partially cross correctly.
        env(offer(
            alice,
            STAmount{JPY.issue(), std::uint64_t(2291181510070762), -12},
            STAmount{USD.issue(), std::uint64_t(2190218999914694), -14}));
        env.close();

        auto const aliceOffers = offersOnAccount(env, alice);
        BEAST_EXPECT(aliceOffers.size() == 1);
        for (auto const& offerPtr : aliceOffers)
        {
            auto const& offer = *offerPtr;
            BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
            BEAST_EXPECT(
                offer[sfTakerGets] ==
                STAmount(USD.issue(), std::uint64_t(2185847305256635), -14));
            BEAST_EXPECT(
                offer[sfTakerPays] ==
                STAmount(JPY.issue(), std::uint64_t(2286608293434156), -12));
        }
    }

    void
    testTinyOffer(FeatureBitset features)
    {
        testcase("Tiny Offer");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account("gw");
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const CNY = gw["CNY"];
        auto const fee = env.current()->fees().base;
        auto const startXrpBalance = drops(400000000000) + (fee * 2);

        env.fund(startXrpBalance, gw, alice, bob);
        env.close();

        env(trust(bob, CNY(100000)));
        env.close();

        // Place alice's tiny offer in the book first.  Let's see what happens
        // when a reasonable offer crosses it.
        STAmount const aliceCnyOffer{
            CNY.issue(), std::uint64_t(4926000000000000), -23};

        env(offer(alice, aliceCnyOffer, drops(1), tfPassive));
        env.close();

        // bob places an ordinary offer
        STAmount const bobCnyStartBalance{
            CNY.issue(), std::uint64_t(3767479960090235), -15};
        env(pay(gw, bob, bobCnyStartBalance));
        env.close();

        env(offer(
            bob,
            drops(203),
            STAmount{CNY.issue(), std::uint64_t(1000000000000000), -20}));
        env.close();

        env.require(balance(alice, aliceCnyOffer));
        env.require(balance(alice, startXrpBalance - fee - drops(1)));
        env.require(balance(bob, bobCnyStartBalance - aliceCnyOffer));
        env.require(balance(bob, startXrpBalance - (fee * 2) + drops(1)));
    }

    void
    testSelfPayXferFeeOffer(FeatureBitset features)
    {
        testcase("Self Pay Xfer Fee");
        // The old offer crossing code does not charge a transfer fee
        // if alice pays alice.  That's different from how payments work.
        // Payments always charge a transfer fee even if the money is staying
        // in the same hands.
        //
        // What's an example where alice pays alice?  There are three actors:
        // gw, alice, and bob.
        //
        //  1. gw issues BTC and USD.  qw charges a 0.2% transfer fee.
        //
        //  2. alice makes an offer to buy XRP and sell USD.
        //  3. bob makes an offer to buy BTC and sell XRP.
        //
        //  4. alice now makes an offer to sell BTC and buy USD.
        //
        // This last offer crosses using auto-bridging.
        //  o alice's last offer sells BTC to...
        //  o bob' offer which takes alice's BTC and sells XRP to...
        //  o alice's first offer which takes bob's XRP and sells USD to...
        //  o alice's last offer.
        //
        // So alice sells USD to herself.
        //
        // There are six cases that we need to test:
        //  o alice crosses her own offer on the first leg (BTC).
        //  o alice crosses her own offer on the second leg (USD).
        //  o alice crosses her own offers on both legs.
        // All three cases need to be tested:
        //  o In reverse (alice has enough BTC to cover her offer) and
        //  o Forward (alice owns less BTC than is in her final offer.
        //
        // It turns out that two of the forward cases fail for a different
        // reason.  They are therefore commented out here, But they are
        // revisited in the testSelfPayUnlimitedFunds() unit test.

        using namespace jtx;

        Env env{*this, features};
        auto const baseFee = env.current()->fees().base.drops();

        auto const gw = Account("gw");
        auto const BTC = gw["BTC"];
        auto const USD = gw["USD"];
        auto const startXrpBalance = XRP(4000000);

        env.fund(startXrpBalance, gw);
        env.close();

        env(rate(gw, 1.25));
        env.close();

        // Test cases
        struct Actor
        {
            Account acct;
            int offers;        // offers on account after crossing
            PrettyAmount xrp;  // final expected after crossing
            PrettyAmount btc;  // final expected after crossing
            PrettyAmount usd;  // final expected after crossing
        };
        struct TestData
        {
            // The first three three integers give the *index* in actors
            // to assign each of the three roles.  By using indices it is
            // easy for alice to own the offer in the first leg, the second
            // leg, or both.
            std::size_t self;
            std::size_t leg0;
            std::size_t leg1;
            PrettyAmount btcStart;
            std::vector<Actor> actors;
        };

        // clang-format off
        TestData const tests[]{
            //        btcStart   --------------------- actor[0] ---------------------    -------------------- actor[1] -------------------
            {0, 0, 1, BTC(20), {{"ann", 0, drops(3900000'000000 - (4 * baseFee)), BTC(20.0), USD(3000)}, {"abe", 0, drops(4100000'000000 - (3 * baseFee)), BTC( 0), USD(750)}}},  // no BTC xfer fee
            {0, 1, 0, BTC(20), {{"bev", 0, drops(4100000'000000 - (4 * baseFee)), BTC( 7.5), USD(2000)}, {"bob", 0, drops(3900000'000000 - (3 * baseFee)), BTC(10), USD(  0)}}},  // no USD xfer fee
            {0, 0, 0, BTC(20), {{"cam", 0, drops(4000000'000000 - (5 * baseFee)), BTC(20.0), USD(2000)}                                                     }},  // no xfer fee
            {0, 1, 0, BTC( 5), {{"deb", 1, drops(4040000'000000 - (4 * baseFee)), BTC( 0.0), USD(2000)}, {"dan", 1, drops(3960000'000000 - (3 * baseFee)), BTC( 4), USD(  0)}}},  // no USD xfer fee
        };
        // clang-format on

        for (auto const& t : tests)
        {
            Account const& self = t.actors[t.self].acct;
            Account const& leg0 = t.actors[t.leg0].acct;
            Account const& leg1 = t.actors[t.leg1].acct;

            for (auto const& actor : t.actors)
            {
                env.fund(XRP(4000000), actor.acct);
                env.close();

                env(trust(actor.acct, BTC(40)));
                env(trust(actor.acct, USD(8000)));
                env.close();
            }

            env(pay(gw, self, t.btcStart));
            env(pay(gw, self, USD(2000)));
            if (self.id() != leg1.id())
                env(pay(gw, leg1, USD(2000)));
            env.close();

            // Get the initial offers in place.  Remember their sequences
            // so we can delete them later.
            env(offer(leg0, BTC(10), XRP(100000), tfPassive));
            env.close();
            std::uint32_t const leg0OfferSeq = env.seq(leg0) - 1;

            env(offer(leg1, XRP(100000), USD(1000), tfPassive));
            env.close();
            std::uint32_t const leg1OfferSeq = env.seq(leg1) - 1;

            // This is the offer that matters.
            env(offer(self, USD(1000), BTC(10)));
            env.close();
            std::uint32_t const selfOfferSeq = env.seq(self) - 1;

            // Verify results.
            for (auto const& actor : t.actors)
            {
                // Sometimes Taker crossing gets lazy about deleting offers.
                // Treat an empty offer as though it is deleted.
                auto actorOffers = offersOnAccount(env, actor.acct);
                auto const offerCount = std::distance(
                    actorOffers.begin(),
                    std::remove_if(
                        actorOffers.begin(),
                        actorOffers.end(),
                        [](std::shared_ptr<SLE const>& offer) {
                            return (*offer)[sfTakerGets].signum() == 0;
                        }));
                BEAST_EXPECT(offerCount == actor.offers);

                env.require(balance(actor.acct, actor.xrp));
                env.require(balance(actor.acct, actor.btc));
                env.require(balance(actor.acct, actor.usd));
            }
            // Remove any offers that might be left hanging around.  They
            // could bollix up later loops.
            env(offer_cancel(leg0, leg0OfferSeq));
            env.close();
            env(offer_cancel(leg1, leg1OfferSeq));
            env.close();
            env(offer_cancel(self, selfOfferSeq));
            env.close();
        }
    }

    void
    testSelfPayUnlimitedFunds(FeatureBitset features)
    {
        testcase("Self Pay Unlimited Funds");
        // The Taker offer crossing code recognized when Alice was paying
        // Alice the same denomination.  In this case, as long as Alice
        // has a little bit of that denomination, it treats Alice as though
        // she has unlimited funds in that denomination.
        //
        // Huh?  What kind of sense does that make?
        //
        // One way to think about it is to break a single payment into a
        // series of very small payments executed sequentially but very
        // quickly.  Alice needs to pay herself 1 USD, but she only has
        // 0.01 USD.  Alice says, "Hey Alice, let me pay you a penny."
        // Alice does this, taking the penny out of her pocket and then
        // putting it back in her pocket.  Then she says, "Hey Alice,
        // I found another penny.  I can pay you another penny."  Repeat
        // these steps 100 times and Alice has paid herself 1 USD even though
        // she only owns 0.01 USD.
        //
        // That's all very nice, but the payment code does not support this
        // optimization.  In part that's because the payment code can
        // operate on a whole batch of offers.  As a matter of fact, it can
        // deal in two consecutive batches of offers.  It would take a great
        // deal of sorting out to figure out which offers in the two batches
        // had the same owner and give them special processing.  And,
        // honestly, it's a weird little corner case.
        //
        // So, since Flow offer crossing uses the payments engine, Flow
        // offer crossing no longer supports this optimization.
        //
        // The following test shows the difference in the behaviors between
        // Taker offer crossing and Flow offer crossing.

        using namespace jtx;

        Env env{*this, features};
        auto const baseFee = env.current()->fees().base.drops();

        auto const gw = Account("gw");
        auto const BTC = gw["BTC"];
        auto const USD = gw["USD"];
        auto const startXrpBalance = XRP(4000000);

        env.fund(startXrpBalance, gw);
        env.close();

        env(rate(gw, 1.25));
        env.close();

        // Test cases
        struct Actor
        {
            Account acct;
            int offers;        // offers on account after crossing
            PrettyAmount xrp;  // final expected after crossing
            PrettyAmount btc;  // final expected after crossing
            PrettyAmount usd;  // final expected after crossing
        };
        struct TestData
        {
            // The first three three integers give the *index* in actors
            // to assign each of the three roles.  By using indices it is
            // easy for alice to own the offer in the first leg, the second
            // leg, or both.
            std::size_t self;
            std::size_t leg0;
            std::size_t leg1;
            PrettyAmount btcStart;
            std::vector<Actor> actors;
        };

        // clang-format off
        TestData const tests[]{
            //         btcStart    ------------------- actor[0] --------------------    ------------------- actor[1] --------------------
            {0, 0, 1, BTC(5), {{"gay", 1, drops(3950000'000000 - (4 * baseFee)), BTC(5), USD(2500)}, {"gar", 1, drops(4050000'000000 - (3 * baseFee)), BTC(0), USD(1375)}}}, // no BTC xfer fee
            {0, 0, 0, BTC(5), {{"hye", 2, drops(4000000'000000 - (5 * baseFee)), BTC(5), USD(2000)}                                                     }}  // no xfer fee
        };
        // clang-format on

        for (auto const& t : tests)
        {
            Account const& self = t.actors[t.self].acct;
            Account const& leg0 = t.actors[t.leg0].acct;
            Account const& leg1 = t.actors[t.leg1].acct;

            for (auto const& actor : t.actors)
            {
                env.fund(XRP(4000000), actor.acct);
                env.close();

                env(trust(actor.acct, BTC(40)));
                env(trust(actor.acct, USD(8000)));
                env.close();
            }

            env(pay(gw, self, t.btcStart));
            env(pay(gw, self, USD(2000)));
            if (self.id() != leg1.id())
                env(pay(gw, leg1, USD(2000)));
            env.close();

            // Get the initial offers in place.  Remember their sequences
            // so we can delete them later.
            env(offer(leg0, BTC(10), XRP(100000), tfPassive));
            env.close();
            std::uint32_t const leg0OfferSeq = env.seq(leg0) - 1;

            env(offer(leg1, XRP(100000), USD(1000), tfPassive));
            env.close();
            std::uint32_t const leg1OfferSeq = env.seq(leg1) - 1;

            // This is the offer that matters.
            env(offer(self, USD(1000), BTC(10)));
            env.close();
            std::uint32_t const selfOfferSeq = env.seq(self) - 1;

            // Verify results.
            for (auto const& actor : t.actors)
            {
                // Sometimes Taker offer crossing gets lazy about deleting
                // offers.  Treat an empty offer as though it is deleted.
                auto actorOffers = offersOnAccount(env, actor.acct);
                auto const offerCount = std::distance(
                    actorOffers.begin(),
                    std::remove_if(
                        actorOffers.begin(),
                        actorOffers.end(),
                        [](std::shared_ptr<SLE const>& offer) {
                            return (*offer)[sfTakerGets].signum() == 0;
                        }));
                BEAST_EXPECT(offerCount == actor.offers);

                env.require(balance(actor.acct, actor.xrp));
                env.require(balance(actor.acct, actor.btc));
                env.require(balance(actor.acct, actor.usd));
            }
            // Remove any offers that might be left hanging around.  They
            // could bollix up later loops.
            env(offer_cancel(leg0, leg0OfferSeq));
            env.close();
            env(offer_cancel(leg1, leg1OfferSeq));
            env.close();
            env(offer_cancel(self, selfOfferSeq));
            env.close();
        }
    }

    void
    testRequireAuth(FeatureBitset features)
    {
        testcase("lsfRequireAuth");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account("gw");
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const gwUSD = gw["USD"];
        auto const aliceUSD = alice["USD"];
        auto const bobUSD = bob["USD"];

        env.fund(XRP(400000), gw, alice, bob);
        env.close();

        // GW requires authorization for holders of its IOUs
        env(fset(gw, asfRequireAuth));
        env.close();

        // Properly set trust and have gw authorize bob and alice
        env(trust(gw, bobUSD(100)), txflags(tfSetfAuth));
        env(trust(bob, gwUSD(100)));
        env(trust(gw, aliceUSD(100)), txflags(tfSetfAuth));
        env(trust(alice, gwUSD(100)));
        // Alice is able to place the offer since the GW has authorized her
        env(offer(alice, gwUSD(40), XRP(4000)));
        env.close();

        env.require(offers(alice, 1));
        env.require(balance(alice, gwUSD(0)));

        env(pay(gw, bob, gwUSD(50)));
        env.close();

        env.require(balance(bob, gwUSD(50)));

        // Bob's offer should cross Alice's
        env(offer(bob, XRP(4000), gwUSD(40)));
        env.close();

        env.require(offers(alice, 0));
        env.require(balance(alice, gwUSD(40)));

        env.require(offers(bob, 0));
        env.require(balance(bob, gwUSD(10)));
    }

    void
    testMissingAuth(FeatureBitset features)
    {
        testcase("Missing Auth");
        // 1. alice creates an offer to acquire USD/gw, an asset for which
        //    she does not have a trust line.  At some point in the future,
        //    gw adds lsfRequireAuth.  Then, later, alice's offer is crossed.
        //     Alice's offer is deleted, not consumed, since alice is not
        //     authorized to hold USD/gw.
        //
        // 2. alice tries to create an offer for USD/gw, now that gw has
        //    lsfRequireAuth set.  This time the offer create fails because
        //    alice is not authorized to hold USD/gw.
        //
        // 3. Next, gw creates a trust line to alice, but does not set
        //    tfSetfAuth on that trust line.  alice attempts to create an
        //    offer and again fails.
        //
        // 4. Finally, gw sets tsfSetAuth on the trust line authorizing
        //    alice to own USD/gw.  At this point alice successfully
        //    creates and crosses an offer for USD/gw.

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account("gw");
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const gwUSD = gw["USD"];
        auto const aliceUSD = alice["USD"];
        auto const bobUSD = bob["USD"];

        env.fund(XRP(400000), gw, alice, bob);
        env.close();

        env(offer(alice, gwUSD(40), XRP(4000)));
        env.close();

        env.require(offers(alice, 1));
        env.require(balance(alice, gwUSD(none)));
        env(fset(gw, asfRequireAuth));
        env.close();

        env(trust(gw, bobUSD(100)), txflags(tfSetfAuth));
        env.close();
        env(trust(bob, gwUSD(100)));
        env.close();

        env(pay(gw, bob, gwUSD(50)));
        env.close();
        env.require(balance(bob, gwUSD(50)));

        // gw now requires authorization and bob has gwUSD(50).  Let's see if
        // bob can cross alice's offer.
        //
        // Bob's offer shouldn't cross and alice's unauthorized offer should be
        // deleted.
        env(offer(bob, XRP(4000), gwUSD(40)));
        env.close();
        std::uint32_t const bobOfferSeq = env.seq(bob) - 1;

        env.require(offers(alice, 0));
        // alice's unauthorized offer is deleted & bob's offer not crossed.
        env.require(balance(alice, gwUSD(none)));
        env.require(offers(bob, 1));
        env.require(balance(bob, gwUSD(50)));

        // See if alice can create an offer without authorization.  alice
        // should not be able to create the offer and bob's offer should be
        // untouched.
        env(offer(alice, gwUSD(40), XRP(4000)), ter(tecNO_LINE));
        env.close();

        env.require(offers(alice, 0));
        env.require(balance(alice, gwUSD(none)));

        env.require(offers(bob, 1));
        env.require(balance(bob, gwUSD(50)));

        // Set up a trust line for alice, but don't authorize it.  alice
        // should still not be able to create an offer for USD/gw.
        env(trust(gw, aliceUSD(100)));
        env.close();

        env(offer(alice, gwUSD(40), XRP(4000)), ter(tecNO_AUTH));
        env.close();

        env.require(offers(alice, 0));
        env.require(balance(alice, gwUSD(0)));

        env.require(offers(bob, 1));
        env.require(balance(bob, gwUSD(50)));

        // Delete bob's offer so alice can create an offer without crossing.
        env(offer_cancel(bob, bobOfferSeq));
        env.close();
        env.require(offers(bob, 0));

        // Finally, set up an authorized trust line for alice.  Now alice's
        // offer should succeed.  Note that, since this is an offer rather
        // than a payment, alice does not need to set a trust line limit.
        env(trust(gw, aliceUSD(100)), txflags(tfSetfAuth));
        env.close();

        env(offer(alice, gwUSD(40), XRP(4000)));
        env.close();

        env.require(offers(alice, 1));

        // Now bob creates his offer again.  alice's offer should cross.
        env(offer(bob, XRP(4000), gwUSD(40)));
        env.close();

        env.require(offers(alice, 0));
        env.require(balance(alice, gwUSD(40)));

        env.require(offers(bob, 0));
        env.require(balance(bob, gwUSD(10)));
    }

    void
    testRCSmoketest(FeatureBitset features)
    {
        testcase("RippleConnect Smoketest payment flow");
        using namespace jtx;

        Env env{*this, features};

        // This test mimics the payment flow used in the Ripple Connect
        // smoke test.  The players:
        //   A USD gateway with hot and cold wallets
        //   A EUR gateway with hot and cold walllets
        //   A MM gateway that will provide offers from USD->EUR and EUR->USD
        // A path from hot US to cold EUR is found and then used to send
        // USD for EUR that goes through the market maker

        auto const hotUS = Account("hotUS");
        auto const coldUS = Account("coldUS");
        auto const hotEU = Account("hotEU");
        auto const coldEU = Account("coldEU");
        auto const mm = Account("mm");

        auto const USD = coldUS["USD"];
        auto const EUR = coldEU["EUR"];

        env.fund(XRP(100000), hotUS, coldUS, hotEU, coldEU, mm);
        env.close();

        // Cold wallets require trust but will ripple by default
        for (auto const& cold : {coldUS, coldEU})
        {
            env(fset(cold, asfRequireAuth));
            env(fset(cold, asfDefaultRipple));
        }
        env.close();

        // Each hot wallet trusts the related cold wallet for a large amount
        env(trust(hotUS, USD(10000000)), txflags(tfSetNoRipple));
        env(trust(hotEU, EUR(10000000)), txflags(tfSetNoRipple));
        // Market maker trusts both cold wallets for a large amount
        env(trust(mm, USD(10000000)), txflags(tfSetNoRipple));
        env(trust(mm, EUR(10000000)), txflags(tfSetNoRipple));
        env.close();

        // Gateways authorize the trustlines of hot and market maker
        env(trust(coldUS, USD(0), hotUS, tfSetfAuth));
        env(trust(coldEU, EUR(0), hotEU, tfSetfAuth));
        env(trust(coldUS, USD(0), mm, tfSetfAuth));
        env(trust(coldEU, EUR(0), mm, tfSetfAuth));
        env.close();

        // Issue currency from cold wallets to hot and market maker
        env(pay(coldUS, hotUS, USD(5000000)));
        env(pay(coldEU, hotEU, EUR(5000000)));
        env(pay(coldUS, mm, USD(5000000)));
        env(pay(coldEU, mm, EUR(5000000)));
        env.close();

        // MM places offers
        float const rate = 0.9f;  // 0.9 USD = 1 EUR
        env(offer(mm, EUR(4000000 * rate), USD(4000000)), json(jss::Flags, tfSell));

        float const reverseRate = 1.0f / rate * 1.00101f;
        env(offer(mm, USD(4000000 * reverseRate), EUR(4000000)), json(jss::Flags, tfSell));
        env.close();

        // There should be a path available from hot US to cold EUR
        {
            Json::Value jvParams;
            jvParams[jss::destination_account] = coldEU.human();
            jvParams[jss::destination_amount][jss::issuer] = coldEU.human();
            jvParams[jss::destination_amount][jss::currency] = "EUR";
            jvParams[jss::destination_amount][jss::value] = 10;
            jvParams[jss::source_account] = hotUS.human();

            Json::Value const jrr{
                env.rpc("json", "ripple_path_find", to_string(jvParams))[jss::result]};

            BEAST_EXPECT(jrr[jss::status] == "success");
            BEAST_EXPECT(jrr[jss::alternatives].isArray() && jrr[jss::alternatives].size() > 0);
        }
        // Send the payment using the found path.
        env(pay(hotUS, coldEU, EUR(10)), sendmax(USD(11.1223326)));
    }

    void
    testSelfAuth(FeatureBitset features)
    {
        testcase("Self Auth");

        using namespace jtx;

        Env env{*this, features};

        auto const gw = Account("gw");
        auto const alice = Account("alice");
        auto const gwUSD = gw["USD"];
        auto const aliceUSD = alice["USD"];

        env.fund(XRP(400000), gw, alice);
        env.close();

        // Test that gw can create an offer to buy gw's currency.
        env(offer(gw, gwUSD(40), XRP(4000)));
        env.close();
        std::uint32_t const gwOfferSeq = env.seq(gw) - 1;
        env.require(offers(gw, 1));

        // Since gw has an offer out, gw should not be able to set RequireAuth.
        env(fset(gw, asfRequireAuth), ter(tecOWNERS));
        env.close();

        // Cancel gw's offer so we can set RequireAuth.
        env(offer_cancel(gw, gwOfferSeq));
        env.close();
        env.require(offers(gw, 0));

        // gw now requires authorization for holders of its IOUs
        env(fset(gw, asfRequireAuth));
        env.close();

        // Before DepositPreauth an account with lsfRequireAuth set could not
        // create an offer to buy their own currency.  After DepositPreauth
        // they can.
        env(offer(gw, gwUSD(40), XRP(4000)), ter(tesSUCCESS));
        env.close();

        env.require(offers(gw, 1));

        // Set up an authorized trust line and pay alice gwUSD 50.
        env(trust(gw, aliceUSD(100)), txflags(tfSetfAuth));
        env(trust(alice, gwUSD(100)));
        env.close();

        env(pay(gw, alice, gwUSD(50)));
        env.close();

        env.require(balance(alice, gwUSD(50)));

        // alice's offer should cross gw's
        env(offer(alice, XRP(4000), gwUSD(40)));
        env.close();

        env.require(offers(alice, 0));
        env.require(balance(alice, gwUSD(10)));

        env.require(offers(gw, 0));
    }

    void
    testDeletedOfferIssuer(FeatureBitset features)
    {
        // Show that an offer who's issuer has been deleted cannot be crossed.
        using namespace jtx;

        testcase("Deleted offer issuer");

        auto trustLineExists = [](jtx::Env const& env,
                                  jtx::Account const& src,
                                  jtx::Account const& dst,
                                  Currency const& cur) -> bool {
            return bool(env.le(keylet::line(src, dst, cur)));
        };

        Account const alice("alice");
        Account const becky("becky");
        Account const carol("carol");
        Account const gw("gateway");
        auto const USD = gw["USD"];
        auto const BUX = alice["BUX"];

        Env env{*this, features};

        env.fund(XRP(10000), alice, becky, carol, noripple(gw));
        env.close();
        env.trust(USD(1000), becky);
        env(pay(gw, becky, USD(5)));
        env.close();
        BEAST_EXPECT(trustLineExists(env, gw, becky, USD.currency));

        // Make offers that produce USD and can be crossed two ways:
        // direct XRP -> USD
        // direct BUX -> USD
        env(offer(becky, XRP(2), USD(2)), txflags(tfPassive));
        std::uint32_t const beckyBuxUsdSeq{env.seq(becky)};
        env(offer(becky, BUX(3), USD(3)), txflags(tfPassive));
        env.close();

        // becky keeps the offers, but removes the trustline.
        env(pay(becky, gw, USD(5)));
        env.trust(USD(0), becky);
        env.close();
        BEAST_EXPECT(!trustLineExists(env, gw, becky, USD.currency));
        BEAST_EXPECT(isOffer(env, becky, XRP(2), USD(2)));
        BEAST_EXPECT(isOffer(env, becky, BUX(3), USD(3)));

        // Delete gw's account.
        {
            // The ledger sequence needs to far enough ahead of the account
            // sequence before the account can be deleted.
            int const delta = [&env, &gw, openLedgerSeq = env.current()->seq()]() -> int {
                std::uint32_t const gwSeq{env.seq(gw)};
                if (gwSeq + 255 > openLedgerSeq)
                    return gwSeq - openLedgerSeq + 255;
                return 0;
            }();

            for (int i = 0; i < delta; ++i)
                env.close();

            // Account deletion has a high fee.  Account for that.
            env(acctdelete(gw, alice), fee(drops(env.current()->fees().increment)));
            env.close();

            // Verify that gw's account root is gone from the ledger.
            BEAST_EXPECT(!env.closed()->exists(keylet::account(gw.id())));
        }

        // alice crosses becky's first offer.  The offer create fails because
        // the USD issuer is not in the ledger.
        env(offer(alice, USD(2), XRP(2)), ter(tecNO_ISSUER));
        env.close();
        env.require(offers(alice, 0));
        BEAST_EXPECT(isOffer(env, becky, XRP(2), USD(2)));
        BEAST_EXPECT(isOffer(env, becky, BUX(3), USD(3)));

        // alice crosses becky's second offer.  Again, the offer create fails
        // because the USD issuer is not in the ledger.
        env(offer(alice, USD(3), BUX(3)), ter(tecNO_ISSUER));
        env.require(offers(alice, 0));
        BEAST_EXPECT(isOffer(env, becky, XRP(2), USD(2)));
        BEAST_EXPECT(isOffer(env, becky, BUX(3), USD(3)));

        // Cancel becky's BUX -> USD offer so we can try auto-bridging.
        env(offer_cancel(becky, beckyBuxUsdSeq));
        env.close();
        BEAST_EXPECT(!isOffer(env, becky, BUX(3), USD(3)));

        // alice creates an offer that can be auto-bridged with becky's
        // remaining offer.
        env.trust(BUX(1000), carol);
        env(pay(alice, carol, BUX(2)));

        env(offer(alice, BUX(2), XRP(2)));
        env.close();

        // carol attempts the auto-bridge.  Again, the offer create fails
        // because the USD issuer is not in the ledger.
        env(offer(carol, USD(2), BUX(2)), ter(tecNO_ISSUER));
        env.close();
        BEAST_EXPECT(isOffer(env, alice, BUX(2), XRP(2)));
        BEAST_EXPECT(isOffer(env, becky, XRP(2), USD(2)));
    }

    void
    testTickSize(FeatureBitset features)
    {
        testcase("Tick Size");

        using namespace jtx;

        // Try to set tick size out of range
        {
            Env env{*this, features};
            auto const gw = Account{"gateway"};
            env.fund(XRP(10000), gw);
            env.close();

            auto txn = noop(gw);
            txn[sfTickSize.fieldName] = Quality::minTickSize - 1;
            env(txn, ter(temBAD_TICK_SIZE));

            txn[sfTickSize.fieldName] = Quality::minTickSize;
            env(txn);
            BEAST_EXPECT((*env.le(gw))[sfTickSize] == Quality::minTickSize);

            txn = noop(gw);
            txn[sfTickSize.fieldName] = Quality::maxTickSize;
            env(txn);
            BEAST_EXPECT(!env.le(gw)->isFieldPresent(sfTickSize));

            txn = noop(gw);
            txn[sfTickSize.fieldName] = Quality::maxTickSize - 1;
            env(txn);
            BEAST_EXPECT((*env.le(gw))[sfTickSize] == Quality::maxTickSize - 1);

            txn = noop(gw);
            txn[sfTickSize.fieldName] = Quality::maxTickSize + 1;
            env(txn, ter(temBAD_TICK_SIZE));

            txn[sfTickSize.fieldName] = 0;
            env(txn);
            BEAST_EXPECT(!env.le(gw)->isFieldPresent(sfTickSize));
        }

        Env env{*this, features};
        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const XTS = gw["XTS"];
        auto const XXX = gw["XXX"];

        env.fund(XRP(10000), gw, alice);
        env.close();

        {
            // Gateway sets its tick size to 5
            auto txn = noop(gw);
            txn[sfTickSize.fieldName] = 5;
            env(txn);
            BEAST_EXPECT((*env.le(gw))[sfTickSize] == 5);
        }

        env(trust(alice, XTS(1000)));
        env(trust(alice, XXX(1000)));

        env(pay(gw, alice, alice["XTS"](100)));
        env(pay(gw, alice, alice["XXX"](100)));

        env(offer(alice, XTS(10), XXX(30)));
        env(offer(alice, XTS(30), XXX(10)));
        env(offer(alice, XTS(10), XXX(30)), json(jss::Flags, tfSell));
        env(offer(alice, XTS(30), XXX(10)), json(jss::Flags, tfSell));

        std::map<std::uint32_t, std::pair<STAmount, STAmount>> offers;
        forEachItem(*env.current(), alice, [&](std::shared_ptr<SLE const> const& sle) {
            if (sle->getType() == ltOFFER)
            {
                offers.emplace(
                    (*sle)[sfSequence], std::make_pair((*sle)[sfTakerPays], (*sle)[sfTakerGets]));
            }
        });

        // first offer
        auto it = offers.begin();
        BEAST_EXPECT(it != offers.end());
        BEAST_EXPECT(
            it->second.first == XTS(10) && it->second.second < XXX(30) &&
            it->second.second > XXX(29.9994));

        // second offer
        ++it;
        BEAST_EXPECT(it != offers.end());
        BEAST_EXPECT(it->second.first == XTS(30) && it->second.second == XXX(10));

        // third offer
        ++it;
        BEAST_EXPECT(it != offers.end());
        BEAST_EXPECT(it->second.first == XTS(10.0002) && it->second.second == XXX(30));

        // fourth offer
        // exact TakerPays is XTS(1/.033333)
        ++it;
        BEAST_EXPECT(it != offers.end());
        BEAST_EXPECT(it->second.first == XTS(30) && it->second.second == XXX(10));

        BEAST_EXPECT(++it == offers.end());
    }

    // Helper function that returns offers on an account sorted by sequence.
    static std::vector<std::shared_ptr<SLE const>>
    sortedOffersOnAccount(jtx::Env& env, jtx::Account const& acct)
    {
        std::vector<std::shared_ptr<SLE const>> offers{offersOnAccount(env, acct)};
        std::sort(
            offers.begin(),
            offers.end(),
            [](std::shared_ptr<SLE const> const& rhs, std::shared_ptr<SLE const> const& lhs) {
                return (*rhs)[sfSequence] < (*lhs)[sfSequence];
            });
        return offers;
    }

    void
    testTicketOffer(FeatureBitset features)
    {
        testcase("Ticket Offers");

        using namespace jtx;

        // Two goals for this test.
        //
        //  o Verify that offers can be created using tickets.
        //
        //  o Show that offers in the _same_ order book remain in
        //    chronological order regardless of sequence/ticket numbers.
        Env env{*this, features};
        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const USD = gw["USD"];

        env.fund(XRP(10000), gw, alice, bob);
        env.close();

        env(trust(alice, USD(1000)));
        env(trust(bob, USD(1000)));
        env.close();

        env(pay(gw, alice, USD(200)));
        env.close();

        // Create four offers from the same account with identical quality
        // so they go in the same order book.  Each offer goes in a different
        // ledger so the chronology is clear.
        std::uint32_t const offerId_0{env.seq(alice)};
        env(offer(alice, XRP(50), USD(50)));
        env.close();

        // Create two tickets.
        std::uint32_t const ticketSeq{env.seq(alice) + 1};
        env(ticket::create(alice, 2));
        env.close();

        // Create another sequence-based offer.
        std::uint32_t const offerId_1{env.seq(alice)};
        BEAST_EXPECT(offerId_1 == offerId_0 + 4);
        env(offer(alice, XRP(50), USD(50)));
        env.close();

        // Create two ticket based offers in reverse order.
        std::uint32_t const offerId_2{ticketSeq + 1};
        env(offer(alice, XRP(50), USD(50)), ticket::use(offerId_2));
        env.close();

        // Create the last offer.
        std::uint32_t const offerId_3{ticketSeq};
        env(offer(alice, XRP(50), USD(50)), ticket::use(offerId_3));
        env.close();

        // Verify that all of alice's offers are present.
        {
            auto offers = sortedOffersOnAccount(env, alice);
            BEAST_EXPECT(offers.size() == 4);
            BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId_0);
            BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerId_3);
            BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerId_2);
            BEAST_EXPECT(offers[3]->getFieldU32(sfSequence) == offerId_1);
            env.require(balance(alice, USD(200)));
            env.require(owners(alice, 5));
        }

        // Cross alice's first offer.
        env(offer(bob, USD(50), XRP(50)));
        env.close();

        // Verify that the first offer alice created was consumed.
        {
            auto offers = sortedOffersOnAccount(env, alice);
            BEAST_EXPECT(offers.size() == 3);
            BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId_3);
            BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerId_2);
            BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerId_1);
        }

        // Cross alice's second offer.
        env(offer(bob, USD(50), XRP(50)));
        env.close();

        // Verify that the second offer alice created was consumed.
        {
            auto offers = sortedOffersOnAccount(env, alice);
            BEAST_EXPECT(offers.size() == 2);
            BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId_3);
            BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerId_2);
        }

        // Cross alice's third offer.
        env(offer(bob, USD(50), XRP(50)));
        env.close();

        // Verify that the third offer alice created was consumed.
        {
            auto offers = sortedOffersOnAccount(env, alice);
            BEAST_EXPECT(offers.size() == 1);
            BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId_3);
        }

        // Cross alice's last offer.
        env(offer(bob, USD(50), XRP(50)));
        env.close();

        // Verify that the third offer alice created was consumed.
        {
            auto offers = sortedOffersOnAccount(env, alice);
            BEAST_EXPECT(offers.empty());
        }
        env.require(balance(alice, USD(0)));
        env.require(owners(alice, 1));
        env.require(balance(bob, USD(200)));
        env.require(owners(bob, 1));
    }

    void
    testTicketCancelOffer(FeatureBitset features)
    {
        testcase("Ticket Cancel Offers");

        using namespace jtx;

        // Verify that offers created with or without tickets can be canceled
        // by transactions with or without tickets.
        Env env{*this, features};
        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const USD = gw["USD"];

        env.fund(XRP(10000), gw, alice);
        env.close();

        env(trust(alice, USD(1000)));
        env.close();
        env.require(owners(alice, 1), tickets(alice, 0));

        env(pay(gw, alice, USD(200)));
        env.close();

        // Create the first of four offers using a sequence.
        std::uint32_t const offerSeqId_0{env.seq(alice)};
        env(offer(alice, XRP(50), USD(50)));
        env.close();
        env.require(owners(alice, 2), tickets(alice, 0));

        // Create four tickets.
        std::uint32_t const ticketSeq{env.seq(alice) + 1};
        env(ticket::create(alice, 4));
        env.close();
        env.require(owners(alice, 6), tickets(alice, 4));

        // Create the second (also sequence-based) offer.
        std::uint32_t const offerSeqId_1{env.seq(alice)};
        BEAST_EXPECT(offerSeqId_1 == offerSeqId_0 + 6);
        env(offer(alice, XRP(50), USD(50)));
        env.close();

        // Create the third (ticket-based) offer.
        std::uint32_t const offerTixId_0{ticketSeq + 1};
        env(offer(alice, XRP(50), USD(50)), ticket::use(offerTixId_0));
        env.close();

        // Create the last offer.
        std::uint32_t const offerTixId_1{ticketSeq};
        env(offer(alice, XRP(50), USD(50)), ticket::use(offerTixId_1));
        env.close();

        // Verify that all of alice's offers are present.
        {
            auto offers = sortedOffersOnAccount(env, alice);
            BEAST_EXPECT(offers.size() == 4);
            BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerSeqId_0);
            BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerTixId_1);
            BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerTixId_0);
            BEAST_EXPECT(offers[3]->getFieldU32(sfSequence) == offerSeqId_1);
            env.require(balance(alice, USD(200)));
            env.require(owners(alice, 7));
        }

        // Use a ticket to cancel an offer created with a sequence.
        env(offer_cancel(alice, offerSeqId_0), ticket::use(ticketSeq + 2));
        env.close();

        // Verify that offerSeqId_0 was canceled.
        {
            auto offers = sortedOffersOnAccount(env, alice);
            BEAST_EXPECT(offers.size() == 3);
            BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerTixId_1);
            BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerTixId_0);
            BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerSeqId_1);
        }

        // Use a ticket to cancel an offer created with a ticket.
        env(offer_cancel(alice, offerTixId_0), ticket::use(ticketSeq + 3));
        env.close();

        // Verify that offerTixId_0 was canceled.
        {
            auto offers = sortedOffersOnAccount(env, alice);
            BEAST_EXPECT(offers.size() == 2);
            BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerTixId_1);
            BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerSeqId_1);
        }

        // All of alice's tickets should now be used up.
        env.require(owners(alice, 3), tickets(alice, 0));

        // Use a sequence to cancel an offer created with a ticket.
        env(offer_cancel(alice, offerTixId_1));
        env.close();

        // Verify that offerTixId_1 was canceled.
        {
            auto offers = sortedOffersOnAccount(env, alice);
            BEAST_EXPECT(offers.size() == 1);
            BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerSeqId_1);
        }

        // Use a sequence to cancel an offer created with a sequence.
        env(offer_cancel(alice, offerSeqId_1));
        env.close();

        // Verify that offerSeqId_1 was canceled.
        // All of alice's tickets should now be used up.
        env.require(owners(alice, 1), tickets(alice, 0), offers(alice, 0));
    }

    void
    testFalseAssert()
    {
        // An assert was falsely triggering when computing rates for offers.
        // This unit test would trigger that assert (which has been removed).
        testcase("incorrect assert fixed");
        using namespace jtx;

        Env env{*this};
        auto const alice = Account("alice");
        auto const USD = alice["USD"];

        env.fund(XRP(10000), alice);
        env.close();
        env(offer(alice, XRP(100000000000), USD(100000000)));
        pass();
    }

    void
    testFillOrKill(FeatureBitset features)
    {
        testcase("fixFillOrKill");
        using namespace jtx;
        Env env(*this, features);
        Account const issuer("issuer");
        Account const maker("maker");
        Account const taker("taker");
        auto const USD = issuer["USD"];
        auto const EUR = issuer["EUR"];

        env.fund(XRP(1'000), issuer);
        env.fund(XRP(1'000), maker, taker);
        env.close();

        env.trust(USD(1'000), maker, taker);
        env.trust(EUR(1'000), maker, taker);
        env.close();

        env(pay(issuer, maker, USD(1'000)));
        env(pay(issuer, taker, USD(1'000)));
        env(pay(issuer, maker, EUR(1'000)));
        env.close();

        auto makerUSDBalance = env.balance(maker, USD).value();
        auto takerUSDBalance = env.balance(taker, USD).value();
        auto makerEURBalance = env.balance(maker, EUR).value();
        auto takerEURBalance = env.balance(taker, EUR).value();
        auto makerXRPBalance = env.balance(maker, XRP).value();
        auto takerXRPBalance = env.balance(taker, XRP).value();

        // tfFillOrKill, TakerPays must be filled
        {
            TER const err = features[fixFillOrKill] ? TER(tesSUCCESS) : tecKILLED;

            env(offer(maker, XRP(100), USD(100)));
            env.close();

            env(offer(taker, USD(100), XRP(101)), txflags(tfFillOrKill), ter(err));
            env.close();

            makerXRPBalance -= txfee(env, 1);
            takerXRPBalance -= txfee(env, 1);
            if (isTesSuccess(err))
            {
                makerUSDBalance -= USD(100);
                takerUSDBalance += USD(100);
                makerXRPBalance += XRP(100).value();
                takerXRPBalance -= XRP(100).value();
            }
            BEAST_EXPECT(expectOffers(env, taker, 0));

            env(offer(maker, USD(100), XRP(100)));
            env.close();

            env(offer(taker, XRP(100), USD(101)), txflags(tfFillOrKill), ter(err));
            env.close();

            makerXRPBalance -= txfee(env, 1);
            takerXRPBalance -= txfee(env, 1);
            if (isTesSuccess(err))
            {
                makerUSDBalance += USD(100);
                takerUSDBalance -= USD(100);
                makerXRPBalance -= XRP(100).value();
                takerXRPBalance += XRP(100).value();
            }
            BEAST_EXPECT(expectOffers(env, taker, 0));

            env(offer(maker, USD(100), EUR(100)));
            env.close();

            env(offer(taker, EUR(100), USD(101)), txflags(tfFillOrKill), ter(err));
            env.close();

            makerXRPBalance -= txfee(env, 1);
            takerXRPBalance -= txfee(env, 1);
            if (isTesSuccess(err))
            {
                makerUSDBalance += USD(100);
                takerUSDBalance -= USD(100);
                makerEURBalance -= EUR(100);
                takerEURBalance += EUR(100);
            }
            BEAST_EXPECT(expectOffers(env, taker, 0));
        }

        // tfFillOrKill + tfSell, TakerGets must be filled
        {
            env(offer(maker, XRP(101), USD(101)));
            env.close();

            env(offer(taker, USD(100), XRP(101)), txflags(tfFillOrKill | tfSell));
            env.close();

            makerUSDBalance -= USD(101);
            takerUSDBalance += USD(101);
            makerXRPBalance += XRP(101).value() - txfee(env, 1);
            takerXRPBalance -= XRP(101).value() + txfee(env, 1);
            BEAST_EXPECT(expectOffers(env, taker, 0));

            env(offer(maker, USD(101), XRP(101)));
            env.close();

            env(offer(taker, XRP(100), USD(101)), txflags(tfFillOrKill | tfSell));
            env.close();

            makerUSDBalance += USD(101);
            takerUSDBalance -= USD(101);
            makerXRPBalance -= XRP(101).value() + txfee(env, 1);
            takerXRPBalance += XRP(101).value() - txfee(env, 1);
            BEAST_EXPECT(expectOffers(env, taker, 0));

            env(offer(maker, USD(101), EUR(101)));
            env.close();

            env(offer(taker, EUR(100), USD(101)), txflags(tfFillOrKill | tfSell));
            env.close();

            makerUSDBalance += USD(101);
            takerUSDBalance -= USD(101);
            makerEURBalance -= EUR(101);
            takerEURBalance += EUR(101);
            makerXRPBalance -= txfee(env, 1);
            takerXRPBalance -= txfee(env, 1);
            BEAST_EXPECT(expectOffers(env, taker, 0));
        }

        // Fail regardless of fixFillOrKill amendment
        for (auto const flags : {tfFillOrKill, tfFillOrKill + tfSell})
        {
            env(offer(maker, XRP(100), USD(100)));
            env.close();

            env(offer(taker, USD(100), XRP(99)), txflags(flags), ter(tecKILLED));
            env.close();

            makerXRPBalance -= txfee(env, 1);
            takerXRPBalance -= txfee(env, 1);
            BEAST_EXPECT(expectOffers(env, taker, 0));

            env(offer(maker, USD(100), XRP(100)));
            env.close();

            env(offer(taker, XRP(100), USD(99)), txflags(flags), ter(tecKILLED));
            env.close();

            makerXRPBalance -= txfee(env, 1);
            takerXRPBalance -= txfee(env, 1);
            BEAST_EXPECT(expectOffers(env, taker, 0));

            env(offer(maker, USD(100), EUR(100)));
            env.close();

            env(offer(taker, EUR(100), USD(99)), txflags(flags), ter(tecKILLED));
            env.close();

            makerXRPBalance -= txfee(env, 1);
            takerXRPBalance -= txfee(env, 1);
            BEAST_EXPECT(expectOffers(env, taker, 0));
        }

        BEAST_EXPECT(
            env.balance(maker, USD) == makerUSDBalance &&
            env.balance(taker, USD) == takerUSDBalance &&
            env.balance(maker, EUR) == makerEURBalance &&
            env.balance(taker, EUR) == takerEURBalance &&
            env.balance(maker, XRP) == makerXRPBalance &&
            env.balance(taker, XRP) == takerXRPBalance);
    }

    void
    testAll(FeatureBitset features)
    {
        testCanceledOffer(features);
        testRmFundedOffer(features);
        testTinyPayment(features);
        testXRPTinyPayment(features);
        testEnforceNoRipple(features);
        testInsufficientReserve(features);
        testFillModes(features);
        testMalformed(features);
        testExpiration(features);
        testUnfundedCross(features);
        testSelfCross(false, features);
        testSelfCross(true, features);
        testNegativeBalance(features);
        testOfferCrossWithXRP(true, features);
        testOfferCrossWithXRP(false, features);
        testOfferCrossWithLimitOverride(features);
        testOfferAcceptThenCancel(features);
        testOfferCancelPastAndFuture(features);
        testCurrencyConversionEntire(features);
        testCurrencyConversionIntoDebt(features);
        testCurrencyConversionInParts(features);
        testCrossCurrencyStartXRP(features);
        testCrossCurrencyEndXRP(features);
        testCrossCurrencyBridged(features);
        testBridgedSecondLegDry(features);
        testOfferFeesConsumeFunds(features);
        testOfferCreateThenCross(features);
        testSellFlagBasic(features);
        testSellFlagExceedLimit(features);
        testGatewayCrossCurrency(features);
        testPartialCross(features);
        testXRPDirectCross(features);
        testDirectCross(features);
        testBridgedCross(features);
        testSellOffer(features);
        testSellWithFillOrKill(features);
        testTransferRateOffer(features);
        testSelfCrossOffer(features);
        testSelfIssueOffer(features);
        testBadPathAssert(features);
        testDirectToDirectPath(features);
        testSelfCrossLowQualityOffer(features);
        testOfferInScaling(features);
        testOfferInScalingWithXferRate(features);
        testOfferThresholdWithReducedFunds(features);
        testTinyOffer(features);
        testSelfPayXferFeeOffer(features);
        testSelfPayUnlimitedFunds(features);
        testRequireAuth(features);
        testMissingAuth(features);
        testRCSmoketest(features);
        testSelfAuth(features);
        testDeletedOfferIssuer(features);
        testTickSize(features);
        testTicketOffer(features);
        testTicketCancelOffer(features);
        testRmSmallIncreasedQOffersXRP(features);
        testRmSmallIncreasedQOffersIOU(features);
        testFillOrKill(features);
    }

    FeatureBitset const allFeatures{jtx::testable_amendments()};

    void
    run() override
    {
        testAll(allFeatures - featurePermissionedDEX);
        testFalseAssert();
    }
};

class OfferWOSmallQOffers_test : public OfferBaseUtil_test
{
    void
    run() override
    {
        testAll(allFeatures - fixFillOrKill - featurePermissionedDEX);
    }
};

class OfferAllFeatures_test : public OfferBaseUtil_test
{
    void
    run() override
    {
        testAll(allFeatures);
    }
};

class Offer_manual_test : public OfferBaseUtil_test
{
    void
    run() override
    {
        using namespace jtx;
        FeatureBitset const all{testable_amendments()};
        FeatureBitset const fillOrKill{fixFillOrKill};
        FeatureBitset const permDEX{featurePermissionedDEX};

        testAll(all - fillOrKill - permDEX);
        testAll(all - permDEX);
        testAll(all);
    }
};

BEAST_DEFINE_TESTSUITE_PRIO(OfferBaseUtil, app, xrpl, 2);
BEAST_DEFINE_TESTSUITE_PRIO(OfferWOSmallQOffers, app, xrpl, 2);
BEAST_DEFINE_TESTSUITE_PRIO(OfferAllFeatures, app, xrpl, 2);
BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(Offer_manual, app, xrpl, 20);

}  // namespace test
}  // namespace xrpl
