#include <test/jtx.h>

#include <xrpl/ledger/Dir.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/tx/applySteps.h>

#include <algorithm>
#include <iterator>

namespace xrpl {
namespace test {

struct EscrowToken_test : public beast::unit_test::suite
{
    static uint64_t
    mptEscrowed(jtx::Env const& env, jtx::Account const& account, jtx::MPT const& mpt)
    {
        auto const sle = env.le(keylet::mptoken(mpt.mpt(), account));
        if (sle && sle->isFieldPresent(sfLockedAmount))
            return (*sle)[sfLockedAmount];
        return 0;
    }

    static uint64_t
    issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt)
    {
        auto const sle = env.le(keylet::mptIssuance(mpt.mpt()));
        if (sle && sle->isFieldPresent(sfLockedAmount))
            return (*sle)[sfLockedAmount];
        return 0;
    }

    static jtx::PrettyAmount
    issuerBalance(jtx::Env& env, jtx::Account const& account, Issue const& issue)
    {
        Json::Value params;
        params[jss::account] = account.human();
        auto jrr = env.rpc("json", "gateway_balances", to_string(params));
        auto const result = jrr[jss::result];
        auto const obligations = result[jss::obligations][to_string(issue.currency)];
        if (obligations.isNull())
            return {STAmount(issue, 0), account.name()};
        STAmount const amount = amountFromString(issue, obligations.asString());
        return {amount, account.name()};
    }

    static jtx::PrettyAmount
    issuerEscrowed(jtx::Env& env, jtx::Account const& account, Issue const& issue)
    {
        Json::Value params;
        params[jss::account] = account.human();
        auto jrr = env.rpc("json", "gateway_balances", to_string(params));
        auto const result = jrr[jss::result];
        auto const locked = result[jss::locked][to_string(issue.currency)];
        if (locked.isNull())
            return {STAmount(issue, 0), account.name()};
        STAmount const amount = amountFromString(issue, locked.asString());
        return {amount, account.name()};
    }

    void
    testIOUEnablement(FeatureBitset features)
    {
        testcase("IOU Enablement");

        using namespace jtx;
        using namespace std::chrono;

        for (bool const withTokenEscrow : {false, true})
        {
            auto const amend = withTokenEscrow ? features : features - featureTokenEscrow;
            Env env{*this, amend};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(10'000), alice, bob);
            env.close();
            env(pay(gw, alice, USD(5000)));
            env(pay(gw, bob, USD(5000)));
            env.close();

            auto const createResult = withTokenEscrow ? ter(tesSUCCESS) : ter(temBAD_AMOUNT);
            auto const finishResult = withTokenEscrow ? ter(tesSUCCESS) : ter(tecNO_TARGET);

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, USD(1'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                createResult);
            env.close();
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                finishResult);
            env.close();

            auto const seq2 = env.seq(alice);
            env(escrow::create(alice, bob, USD(1'000)),
                escrow::condition(escrow::cb2),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 2s),
                fee(baseFee * 150),
                createResult);
            env.close();
            env(escrow::cancel(bob, alice, seq2), finishResult);
            env.close();
        }

        for (bool const withTokenEscrow : {false, true})
        {
            auto const amend = withTokenEscrow ? features : features - featureTokenEscrow;
            Env env{*this, amend};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(10'000), alice, bob);
            env.close();
            env(pay(gw, alice, USD(5000)));
            env(pay(gw, bob, USD(5000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tecNO_TARGET));
            env.close();

            env(escrow::cancel(bob, alice, seq1), ter(tecNO_TARGET));
            env.close();
        }
    }

    void
    testIOUAllowLockingFlag(FeatureBitset features)
    {
        testcase("IOU Allow Locking Flag");

        using namespace jtx;
        using namespace std::chrono;

        Env env{*this, features};
        auto const baseFee = env.current()->fees().base;
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const gw = Account{"gateway"};
        auto const USD = gw["USD"];
        env.fund(XRP(5000), alice, bob, gw);
        env(fset(gw, asfAllowTrustLineLocking));
        env.close();
        env.trust(USD(10'000), alice, bob);
        env.close();
        env(pay(gw, alice, USD(5000)));
        env(pay(gw, bob, USD(5000)));
        env.close();

        // Create Escrow #1 & #2
        auto const seq1 = env.seq(alice);
        env(escrow::create(alice, bob, USD(1'000)),
            escrow::condition(escrow::cb1),
            escrow::finish_time(env.now() + 1s),
            fee(baseFee * 150),
            ter(tesSUCCESS));
        env.close();

        auto const seq2 = env.seq(alice);
        env(escrow::create(alice, bob, USD(1'000)),
            escrow::finish_time(env.now() + 1s),
            escrow::cancel_time(env.now() + 3s),
            fee(baseFee),
            ter(tesSUCCESS));
        env.close();

        // Clear the asfAllowTrustLineLocking flag
        env(fclear(gw, asfAllowTrustLineLocking));
        env.close();
        env.require(nflags(gw, asfAllowTrustLineLocking));

        // Cannot Create Escrow without asfAllowTrustLineLocking
        env(escrow::create(alice, bob, USD(1'000)),
            escrow::condition(escrow::cb1),
            escrow::finish_time(env.now() + 1s),
            fee(baseFee * 150),
            ter(tecNO_PERMISSION));
        env.close();

        // Can finish the escrow created before the flag was cleared
        env(escrow::finish(bob, alice, seq1),
            escrow::condition(escrow::cb1),
            escrow::fulfillment(escrow::fb1),
            fee(baseFee * 150),
            ter(tesSUCCESS));
        env.close();

        // Can cancel the escrow created before the flag was cleared
        env(escrow::cancel(bob, alice, seq2), ter(tesSUCCESS));
        env.close();
    }

    void
    testIOUCreatePreflight(FeatureBitset features)
    {
        testcase("IOU Create Preflight");
        using namespace test::jtx;
        using namespace std::literals;

        // temBAD_FEE: Exercises invalid preflight1.
        {
            Env env{*this, features};
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, bob, gw);

            env(escrow::create(alice, bob, USD(1)),
                escrow::finish_time(env.now() + 1s),
                fee(XRP(-1)),
                ter(temBAD_FEE));
            env.close();
        }

        // temBAD_AMOUNT: amount <= 0
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, bob, gw);

            env(escrow::create(alice, bob, USD(-1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(temBAD_AMOUNT));
            env.close();
        }

        // temBAD_CURRENCY: badCurrency() == amount.getCurrency()
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const BAD = IOU(gw, badCurrency());
            env.fund(XRP(5000), alice, bob, gw);

            env(escrow::create(alice, bob, BAD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(temBAD_CURRENCY));
            env.close();
        }
    }

    void
    testIOUCreatePreclaim(FeatureBitset features)
    {
        testcase("IOU Create Preclaim");
        using namespace test::jtx;
        using namespace std::literals;

        // tecNO_PERMISSION: issuer is the same as the account
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, bob, gw);

            env(escrow::create(gw, alice, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecNO_PERMISSION));
            env.close();
        }

        // tecNO_ISSUER: Issuer does not exist
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, bob);
            env.close();
            env.memoize(gw);

            env(escrow::create(alice, bob, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecNO_ISSUER));
            env.close();
        }

        // tecNO_PERMISSION: asfAllowTrustLineLocking is not set
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, bob, gw);
            env.close();
            env.trust(USD(10'000), alice, bob);
            env.close();
            env(pay(gw, alice, USD(5000)));
            env(pay(gw, bob, USD(5000)));
            env.close();

            env(escrow::create(gw, alice, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecNO_PERMISSION));
            env.close();
        }

        // tecNO_LINE: account does not have a trustline to the issuer
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env(escrow::create(alice, bob, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecNO_LINE));
            env.close();
        }

        // tecNO_PERMISSION: Not testable
        // tecNO_PERMISSION: Not testable
        // tecNO_AUTH: requireAuth
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env(fset(gw, asfRequireAuth));
            env.close();
            env.trust(USD(10'000), alice, bob);
            env.close();

            env(escrow::create(alice, bob, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecNO_AUTH));
            env.close();
        }

        // tecNO_AUTH: requireAuth
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            auto const aliceUSD = alice["USD"];
            env.fund(XRP(5000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env(fset(gw, asfRequireAuth));
            env.close();
            env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth));
            env.trust(USD(10'000), alice, bob);
            env.close();

            env(escrow::create(alice, bob, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecNO_AUTH));
            env.close();
        }

        // tecFROZEN: account is frozen
        {
            // Env Setup
            Env env{*this, features};
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env(trust(alice, USD(100'000)));
            env(trust(bob, USD(100'000)));
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            // set freeze on alice trustline
            env(trust(gw, USD(10'000), alice, tfSetFreeze));
            env.close();

            env(escrow::create(alice, bob, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecFROZEN));
            env.close();
        }

        // tecFROZEN: dest is frozen
        {
            // Env Setup
            Env env{*this, features};
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env(trust(alice, USD(100'000)));
            env(trust(bob, USD(100'000)));
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            // set freeze on bob trustline
            env(trust(gw, USD(10'000), bob, tfSetFreeze));
            env.close();

            env(escrow::create(alice, bob, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecFROZEN));
            env.close();
        }

        // tecINSUFFICIENT_FUNDS
        {
            // Env Setup
            Env env{*this, features};
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env(trust(alice, USD(100'000)));
            env(trust(bob, USD(100'000)));
            env.close();

            env(escrow::create(alice, bob, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecINSUFFICIENT_FUNDS));
            env.close();
        }

        // tecINSUFFICIENT_FUNDS
        {
            // Env Setup
            Env env{*this, features};
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env(trust(alice, USD(100'000)));
            env(trust(bob, USD(100'000)));
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            env(escrow::create(alice, bob, USD(10'001)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecINSUFFICIENT_FUNDS));
            env.close();
        }

        // tecPRECISION_LOSS
        {
            Env env{*this, features};
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(100000000000000000), alice);
            env.trust(USD(100000000000000000), bob);
            env.close();
            env(pay(gw, alice, USD(10000000000000000)));
            env(pay(gw, bob, USD(1)));
            env.close();

            bool const largeMantissa =
                features[featureSingleAssetVault] || features[featureLendingProtocol];

            // alice cannot create escrow for 1/10 iou - precision loss
            env(escrow::create(alice, bob, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(largeMantissa ? (TER)tesSUCCESS : (TER)tecPRECISION_LOSS));
            env.close();
        }
    }

    void
    testIOUFinishPreclaim(FeatureBitset features)
    {
        testcase("IOU Finish Preclaim");
        using namespace test::jtx;
        using namespace std::literals;

        // tecNO_AUTH: requireAuth set: dest not authorized
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            auto const aliceUSD = alice["USD"];
            auto const bobUSD = bob["USD"];
            env.fund(XRP(5000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env(fset(gw, asfRequireAuth));
            env.close();
            env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth));
            env(trust(gw, bobUSD(10'000)), txflags(tfSetfAuth));
            env.trust(USD(10'000), alice, bob);
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            env(pay(bob, gw, USD(10'000)));
            env(trust(gw, bobUSD(0)), txflags(tfSetfAuth));
            env(trust(bob, USD(0)));
            env.close();

            env.trust(USD(10'000), bob);
            env.close();

            // bob cannot finish because he is not authorized
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tecNO_AUTH));
            env.close();
        }

        // tecFROZEN: issuer has deep frozen the dest
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(10'000), alice, bob);
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            // set freeze on bob trustline
            env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze));

            // bob cannot finish because of deep freeze
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tecFROZEN));
            env.close();
        }
    }

    void
    testIOUFinishDoApply(FeatureBitset features)
    {
        testcase("IOU Finish Do Apply");
        using namespace test::jtx;
        using namespace std::literals;

        // tecNO_LINE_INSUF_RESERVE: insufficient reserve to create line
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const acctReserve = env.current()->fees().reserve;
            auto const incReserve = env.current()->fees().increment;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, gw);
            env.fund(acctReserve + (incReserve - 1), bob);
            env.close();
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(10'000), alice);
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            // bob cannot finish because insufficient reserve to create line
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tecNO_LINE_INSUF_RESERVE));
            env.close();
        }

        // tecNO_LINE: alice submits; finish IOU not created
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, bob, gw);
            env.close();
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(10'000), alice);
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            // alice cannot finish because bob does not have a trustline
            env(escrow::finish(alice, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tecNO_LINE));
            env.close();
        }

        // tecLIMIT_EXCEEDED: alice submits; IOU Limit < balance + amount
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, bob, gw);
            env.close();
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(1000), alice, bob);
            env.close();
            env(pay(gw, alice, USD(1000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, USD(5)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

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

            // alice cannot finish because bob's limit is too low
            env(escrow::finish(alice, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tecLIMIT_EXCEEDED));
            env.close();
        }

        // tesSUCCESS: bob submits; IOU Limit < balance + amount
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, bob, gw);
            env.close();
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(1000), alice, bob);
            env.close();
            env(pay(gw, alice, USD(1000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, USD(5)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

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

            // bob can finish even if bob's limit is too low
            auto const bobPreLimit = env.limit(bob, USD);

            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            // bob's limit is not changed
            BEAST_EXPECT(env.limit(bob, USD) == bobPreLimit);
        }
    }

    void
    testIOUCancelPreclaim(FeatureBitset features)
    {
        testcase("IOU Cancel Preclaim");
        using namespace test::jtx;
        using namespace std::literals;

        // tecNO_AUTH: requireAuth set: account not authorized
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            auto const aliceUSD = alice["USD"];
            auto const bobUSD = bob["USD"];
            env.fund(XRP(5000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env(fset(gw, asfRequireAuth));
            env.close();
            env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth));
            env(trust(gw, bobUSD(10'000)), txflags(tfSetfAuth));
            env.trust(USD(10'000), alice, bob);
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, USD(1)),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 2s),
                fee(baseFee),
                ter(tesSUCCESS));
            env.close();

            env(pay(alice, gw, USD(9'999)));
            env(trust(gw, aliceUSD(0)), txflags(tfSetfAuth));
            env(trust(alice, USD(0)));
            env.close();

            env.trust(USD(10'000), alice);
            env.close();

            // alice cannot cancel because she is not authorized
            env(escrow::cancel(bob, alice, seq1), fee(baseFee), ter(tecNO_AUTH));
            env.close();
        }
    }

    void
    testIOUBalances(FeatureBitset features)
    {
        testcase("IOU Balances");

        using namespace jtx;
        using namespace std::chrono;

        Env env{*this, features};
        auto const baseFee = env.current()->fees().base;
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const gw = Account{"gateway"};
        auto const USD = gw["USD"];
        env.fund(XRP(5000), alice, bob, gw);
        env(fset(gw, asfAllowTrustLineLocking));
        env.close();
        env.trust(USD(10'000), alice, bob);
        env.close();
        env(pay(gw, alice, USD(5'000)));
        env(pay(gw, bob, USD(5'000)));
        env.close();

        auto const outstandingUSD = USD(10'000);

        // Create & Finish Escrow
        auto const seq1 = env.seq(alice);
        {
            auto const preAliceUSD = env.balance(alice, USD);
            auto const preBobUSD = env.balance(bob, USD);
            env(escrow::create(alice, bob, USD(1'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, USD) == preAliceUSD - USD(1'000));
            BEAST_EXPECT(env.balance(bob, USD) == preBobUSD);
            BEAST_EXPECT(issuerBalance(env, gw, USD) == outstandingUSD - USD(1'000));
            BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(1'000));
        }
        {
            auto const preAliceUSD = env.balance(alice, USD);
            auto const preBobUSD = env.balance(bob, USD);
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, USD) == preAliceUSD);
            BEAST_EXPECT(env.balance(bob, USD) == preBobUSD + USD(1'000));
            BEAST_EXPECT(issuerBalance(env, gw, USD) == outstandingUSD);
            BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(0));
        }

        // Create & Cancel Escrow
        auto const seq2 = env.seq(alice);
        {
            auto const preAliceUSD = env.balance(alice, USD);
            auto const preBobUSD = env.balance(bob, USD);
            env(escrow::create(alice, bob, USD(1'000)),
                escrow::condition(escrow::cb2),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 2s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, USD) == preAliceUSD - USD(1'000));
            BEAST_EXPECT(env.balance(bob, USD) == preBobUSD);
            BEAST_EXPECT(issuerBalance(env, gw, USD) == outstandingUSD - USD(1'000));
            BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(1'000));
        }
        {
            auto const preAliceUSD = env.balance(alice, USD);
            auto const preBobUSD = env.balance(bob, USD);
            env(escrow::cancel(bob, alice, seq2), ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, USD) == preAliceUSD + USD(1'000));
            BEAST_EXPECT(env.balance(bob, USD) == preBobUSD);
            BEAST_EXPECT(issuerBalance(env, gw, USD) == outstandingUSD);
            BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(0));
        }
    }

    void
    testIOUMetaAndOwnership(FeatureBitset features)
    {
        using namespace jtx;
        using namespace std::chrono;

        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const carol = Account("carol");
        auto const gw = Account{"gateway"};
        auto const USD = gw["USD"];
        {
            testcase("IOU Metadata to self");

            Env env{*this, features};
            env.fund(XRP(5000), alice, bob, carol, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(10'000), alice, bob, carol);
            env.close();
            env(pay(gw, alice, USD(5000)));
            env(pay(gw, bob, USD(5000)));
            env(pay(gw, carol, USD(5000)));
            env.close();
            auto const aseq = env.seq(alice);
            auto const bseq = env.seq(bob);

            env(escrow::create(alice, alice, USD(1'000)),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 500s));
            BEAST_EXPECT(
                (*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
            env.close(5s);
            auto const aa = env.le(keylet::escrow(alice.id(), aseq));
            BEAST_EXPECT(aa);
            {
                xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id()));
                BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2);
                BEAST_EXPECT(std::find(aod.begin(), aod.end(), aa) != aod.end());
            }

            {
                xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id()));
                BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4);
                BEAST_EXPECT(std::find(iod.begin(), iod.end(), aa) != iod.end());
            }

            env(escrow::create(bob, bob, USD(1'000)),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 2s));
            BEAST_EXPECT(
                (*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
            env.close(5s);
            auto const bb = env.le(keylet::escrow(bob.id(), bseq));
            BEAST_EXPECT(bb);

            {
                xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id()));
                BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2);
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), bb) != bod.end());
            }

            {
                xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id()));
                BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 5);
                BEAST_EXPECT(std::find(iod.begin(), iod.end(), bb) != iod.end());
            }

            env.close(5s);
            env(escrow::finish(alice, alice, aseq));
            {
                BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq)));
                BEAST_EXPECT(
                    (*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));

                xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id()));
                BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
                BEAST_EXPECT(std::find(aod.begin(), aod.end(), aa) == aod.end());

                xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id()));
                BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2);
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), bb) != bod.end());

                xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id()));
                BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4);
                BEAST_EXPECT(std::find(iod.begin(), iod.end(), bb) != iod.end());
            }

            env.close(5s);
            env(escrow::cancel(bob, bob, bseq));
            {
                BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq)));
                BEAST_EXPECT(
                    (*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));

                xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id()));
                BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1);
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), bb) == bod.end());

                xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id()));
                BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3);
                BEAST_EXPECT(std::find(iod.begin(), iod.end(), bb) == iod.end());
            }
        }
        {
            testcase("IOU Metadata to other");

            Env env{*this, features};
            env.fund(XRP(5000), alice, bob, carol, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(10'000), alice, bob, carol);
            env.close();
            env(pay(gw, alice, USD(5000)));
            env(pay(gw, bob, USD(5000)));
            env(pay(gw, carol, USD(5000)));
            env.close();
            auto const aseq = env.seq(alice);
            auto const bseq = env.seq(bob);

            env(escrow::create(alice, bob, USD(1'000)), escrow::finish_time(env.now() + 1s));
            BEAST_EXPECT(
                (*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
            env.close(5s);
            env(escrow::create(bob, carol, USD(1'000)),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 2s));
            BEAST_EXPECT(
                (*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
            env.close(5s);

            auto const ab = env.le(keylet::escrow(alice.id(), aseq));
            BEAST_EXPECT(ab);

            auto const bc = env.le(keylet::escrow(bob.id(), bseq));
            BEAST_EXPECT(bc);

            {
                xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id()));
                BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2);
                BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) != aod.end());

                xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id()));
                BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3);
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) != bod.end());
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) != bod.end());

                xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id()));
                BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2);
                BEAST_EXPECT(std::find(cod.begin(), cod.end(), bc) != cod.end());

                xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id()));
                BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 5);
                BEAST_EXPECT(std::find(iod.begin(), iod.end(), ab) != iod.end());
                BEAST_EXPECT(std::find(iod.begin(), iod.end(), bc) != iod.end());
            }

            env.close(5s);
            env(escrow::finish(alice, alice, aseq));
            {
                BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq)));
                BEAST_EXPECT(env.le(keylet::escrow(bob.id(), bseq)));

                xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id()));
                BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
                BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) == aod.end());

                xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id()));
                BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2);
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end());
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) != bod.end());

                xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id()));
                BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2);

                xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id()));
                BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4);
                BEAST_EXPECT(std::find(iod.begin(), iod.end(), ab) == iod.end());
                BEAST_EXPECT(std::find(iod.begin(), iod.end(), bc) != iod.end());
            }

            env.close(5s);
            env(escrow::cancel(bob, bob, bseq));
            {
                BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq)));
                BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq)));

                xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id()));
                BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
                BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) == aod.end());

                xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id()));
                BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1);
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end());
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) == bod.end());

                xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id()));
                BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1);

                xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id()));
                BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3);
                BEAST_EXPECT(std::find(iod.begin(), iod.end(), ab) == iod.end());
                BEAST_EXPECT(std::find(iod.begin(), iod.end(), bc) == iod.end());
            }
        }

        {
            testcase("IOU Metadata to issuer");

            Env env{*this, features};
            env.fund(XRP(5000), alice, carol, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(10'000), alice, carol);
            env.close();
            env(pay(gw, alice, USD(5000)));
            env(pay(gw, carol, USD(5000)));
            env.close();
            auto const aseq = env.seq(alice);

            env(escrow::create(alice, gw, USD(1'000)), escrow::finish_time(env.now() + 1s));

            BEAST_EXPECT(
                (*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
            env.close(5s);
            env(escrow::create(gw, carol, USD(1'000)),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 2s),
                ter(tecNO_PERMISSION));
            env.close(5s);

            auto const ag = env.le(keylet::escrow(alice.id(), aseq));
            BEAST_EXPECT(ag);

            {
                xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id()));
                BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2);
                BEAST_EXPECT(std::find(aod.begin(), aod.end(), ag) != aod.end());

                xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id()));
                BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1);

                xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id()));
                BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3);
                BEAST_EXPECT(std::find(iod.begin(), iod.end(), ag) != iod.end());
            }

            env.close(5s);
            env(escrow::finish(alice, alice, aseq));
            {
                BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq)));

                xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id()));
                BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
                BEAST_EXPECT(std::find(aod.begin(), aod.end(), ag) == aod.end());

                xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id()));
                BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1);

                xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id()));
                BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 2);
                BEAST_EXPECT(std::find(iod.begin(), iod.end(), ag) == iod.end());
            }
        }
    }

    void
    testIOURippleState(FeatureBitset features)
    {
        testcase("IOU RippleState");
        using namespace test::jtx;
        using namespace std::literals;

        struct TestAccountData
        {
            Account src;
            Account dst;
            Account gw;
            bool hasTrustline;
            bool negative;
        };

        std::array<TestAccountData, 8> tests = {{
            // src > dst && src > issuer && dst no trustline
            {Account("alice2"), Account("bob0"), Account{"gw0"}, false, true},
            // src < dst && src < issuer && dst no trustline
            {Account("carol0"), Account("dan1"), Account{"gw1"}, false, false},
            // dst > src && dst > issuer && dst no trustline
            {Account("dan1"), Account("alice2"), Account{"gw0"}, false, true},
            // dst < src && dst < issuer && dst no trustline
            {Account("bob0"), Account("carol0"), Account{"gw1"}, false, false},
            // src > dst && src > issuer && dst has trustline
            {Account("alice2"), Account("bob0"), Account{"gw0"}, true, true},
            // src < dst && src < issuer && dst has trustline
            {Account("carol0"), Account("dan1"), Account{"gw1"}, true, false},
            // dst > src && dst > issuer && dst has trustline
            {Account("dan1"), Account("alice2"), Account{"gw0"}, true, true},
            // dst < src && dst < issuer && dst has trustline
            {Account("bob0"), Account("carol0"), Account{"gw1"}, true, false},
        }};

        for (auto const& t : tests)
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const USD = t.gw["USD"];
            env.fund(XRP(5000), t.src, t.dst, t.gw);
            env(fset(t.gw, asfAllowTrustLineLocking));
            env.close();

            if (t.hasTrustline)
            {
                env.trust(USD(100'000), t.src, t.dst);
            }
            else
            {
                env.trust(USD(100'000), t.src);
            }
            env.close();

            env(pay(t.gw, t.src, USD(10'000)));
            if (t.hasTrustline)
                env(pay(t.gw, t.dst, USD(10'000)));
            env.close();

            // src can create escrow
            auto const seq1 = env.seq(t.src);
            auto const delta = USD(1'000);
            env(escrow::create(t.src, t.dst, delta),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            // dst can finish escrow
            auto const preSrc = env.balance(t.src, USD);
            auto const preDst = env.balance(t.dst, USD);

            env(escrow::finish(t.dst, t.src, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150));
            env.close();

            BEAST_EXPECT(env.balance(t.src, USD) == preSrc);
            BEAST_EXPECT(env.balance(t.dst, USD) == preDst + delta);
        }
    }

    void
    testIOUGateway(FeatureBitset features)
    {
        testcase("IOU Gateway");
        using namespace test::jtx;
        using namespace std::literals;

        struct TestAccountData
        {
            Account src;
            Account dst;
            bool hasTrustline;
        };

        // issuer is source
        {
            auto const gw = Account{"gateway"};
            auto const alice = Account{"alice"};
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const USD = gw["USD"];
            env.fund(XRP(5000), alice, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(100'000), alice);
            env.close();

            env(pay(gw, alice, USD(10'000)));
            env.close();

            // issuer cannot create escrow
            env(escrow::create(gw, alice, USD(1'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecNO_PERMISSION));
            env.close();
        }

        std::array<TestAccountData, 4> gwDstTests = {{
            // src > dst && src > issuer && dst has trustline
            {Account("alice2"), Account{"gw0"}, true},
            // src < dst && src < issuer && dst has trustline
            {Account("carol0"), Account{"gw1"}, true},
            // dst > src && dst > issuer && dst has trustline
            {Account("dan1"), Account{"gw0"}, true},
            // dst < src && dst < issuer && dst has trustline
            {Account("bob0"), Account{"gw1"}, true},
        }};

        // issuer is destination
        for (auto const& t : gwDstTests)
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const USD = t.dst["USD"];
            env.fund(XRP(5000), t.dst, t.src);
            env(fset(t.dst, asfAllowTrustLineLocking));
            env.close();

            env.trust(USD(100'000), t.src);
            env.close();

            env(pay(t.dst, t.src, USD(10'000)));
            env.close();

            // issuer can receive escrow
            auto const seq1 = env.seq(t.src);
            auto const preSrc = env.balance(t.src, USD);
            env(escrow::create(t.src, t.dst, USD(1'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            // issuer can finish escrow, no dest trustline
            env(escrow::finish(t.dst, t.src, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150));
            env.close();
            auto const preAmount = 10'000;
            BEAST_EXPECT(preSrc == USD(preAmount));
            auto const postAmount = 9000;
            BEAST_EXPECT(env.balance(t.src, USD) == USD(postAmount));
            BEAST_EXPECT(env.balance(t.dst, USD) == USD(0));
        }

        // issuer is source and destination
        {
            auto const gw = Account{"gateway"};
            auto const USD = gw["USD"];
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(5000), gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();

            // issuer cannot receive escrow
            env(escrow::create(gw, gw, USD(1'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecNO_PERMISSION));
            env.close();
        }
    }

    void
    testIOULockedRate(FeatureBitset features)
    {
        testcase("IOU Locked Rate");
        using namespace test::jtx;
        using namespace std::literals;

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

        // test locked rate
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env(rate(gw, 1.25));
            env.close();
            env.trust(USD(100'000), alice);
            env.trust(USD(100'000), bob);
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            // alice can create escrow w/ xfer rate
            auto const preAlice = env.balance(alice, USD);
            auto const seq1 = env.seq(alice);
            auto const delta = USD(125);
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();
            auto const transferRate = escrow::rate(env, alice, seq1);
            BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));

            // bob can finish escrow
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150));
            env.close();

            BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta);
            BEAST_EXPECT(env.balance(bob, USD) == USD(10'100));
        }
        // test rate change - higher
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env(rate(gw, 1.25));
            env.close();
            env.trust(USD(100'000), alice);
            env.trust(USD(100'000), bob);
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            // alice can create escrow w/ xfer rate
            auto const preAlice = env.balance(alice, USD);
            auto const seq1 = env.seq(alice);
            auto const delta = USD(125);
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();
            auto transferRate = escrow::rate(env, alice, seq1);
            BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));

            // issuer changes rate higher
            env(rate(gw, 1.26));
            env.close();

            // bob can finish escrow - rate unchanged
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150));
            env.close();

            BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta);
            BEAST_EXPECT(env.balance(bob, USD) == USD(10'100));
        }

        // test rate change - lower
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env(rate(gw, 1.25));
            env.close();
            env.trust(USD(100'000), alice);
            env.trust(USD(100'000), bob);
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            // alice can create escrow w/ xfer rate
            auto const preAlice = env.balance(alice, USD);
            auto const seq1 = env.seq(alice);
            auto const delta = USD(125);
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();
            auto transferRate = escrow::rate(env, alice, seq1);
            BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));

            // issuer changes rate lower
            env(rate(gw, 1.00));
            env.close();

            // bob can finish escrow - rate changed
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150));
            env.close();

            BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta);
            BEAST_EXPECT(env.balance(bob, USD) == USD(10125));
        }

        // test cancel doesn't charge rate
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env(rate(gw, 1.25));
            env.close();
            env.trust(USD(100'000), alice);
            env.trust(USD(100'000), bob);
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            // alice can create escrow w/ xfer rate
            auto const preAlice = env.balance(alice, USD);
            auto const seq1 = env.seq(alice);
            auto const delta = USD(125);
            env(escrow::create(alice, bob, delta),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 3s),
                fee(baseFee));
            env.close();
            auto transferRate = escrow::rate(env, alice, seq1);
            BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));

            // issuer changes rate lower
            env(rate(gw, 1.00));
            env.close();

            // alice can cancel escrow - rate is not charged
            env(escrow::cancel(alice, alice, seq1), fee(baseFee));
            env.close();

            BEAST_EXPECT(env.balance(alice, USD) == preAlice);
            BEAST_EXPECT(env.balance(bob, USD) == USD(10000));
        }
    }

    void
    testIOULimitAmount(FeatureBitset features)
    {
        testcase("IOU Limit");
        using namespace test::jtx;
        using namespace std::literals;

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

        // test LimitAmount
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(1'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(10'000), alice, bob);
            env.close();
            env(pay(gw, alice, USD(1'000)));
            env(pay(gw, bob, USD(1'000)));
            env.close();

            // alice can create escrow
            auto seq1 = env.seq(alice);
            auto const delta = USD(125);
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            // bob can finish
            auto const preBobLimit = env.limit(bob, USD);
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150));
            env.close();
            auto const postBobLimit = env.limit(bob, USD);
            // bob's limit is NOT changed
            BEAST_EXPECT(postBobLimit == preBobLimit);
        }
    }

    void
    testIOURequireAuth(FeatureBitset features)
    {
        testcase("IOU Require Auth");
        using namespace test::jtx;
        using namespace std::literals;

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

        auto const aliceUSD = alice["USD"];
        auto const bobUSD = bob["USD"];

        Env env{*this, features};
        auto const baseFee = env.current()->fees().base;
        env.fund(XRP(1'000), alice, bob, gw);
        env(fset(gw, asfAllowTrustLineLocking));
        env(fset(gw, asfRequireAuth));
        env.close();
        env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth));
        env(trust(alice, USD(10'000)));
        env(trust(bob, USD(10'000)));
        env.close();
        env(pay(gw, alice, USD(1'000)));
        env.close();

        // alice cannot create escrow - fails without auth
        auto seq1 = env.seq(alice);
        auto const delta = USD(125);
        env(escrow::create(alice, bob, delta),
            escrow::condition(escrow::cb1),
            escrow::finish_time(env.now() + 1s),
            fee(baseFee * 150),
            ter(tecNO_AUTH));
        env.close();

        // set auth on bob
        env(trust(gw, bobUSD(10'000)), txflags(tfSetfAuth));
        env(trust(bob, USD(10'000)));
        env.close();
        env(pay(gw, bob, USD(1'000)));
        env.close();

        // alice can create escrow - bob has auth
        seq1 = env.seq(alice);
        env(escrow::create(alice, bob, delta),
            escrow::condition(escrow::cb1),
            escrow::finish_time(env.now() + 1s),
            fee(baseFee * 150));
        env.close();

        // bob can finish
        env(escrow::finish(bob, alice, seq1),
            escrow::condition(escrow::cb1),
            escrow::fulfillment(escrow::fb1),
            fee(baseFee * 150));
        env.close();
    }

    void
    testIOUFreeze(FeatureBitset features)
    {
        testcase("IOU Freeze");
        using namespace test::jtx;
        using namespace std::literals;

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

        // test Global Freeze
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(100'000), alice);
            env.trust(USD(100'000), bob);
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();
            env(fset(gw, asfGlobalFreeze));
            env.close();

            // setup transaction
            auto seq1 = env.seq(alice);
            auto const delta = USD(125);

            // create escrow fails - frozen trustline
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecFROZEN));
            env.close();

            // clear global freeze
            env(fclear(gw, asfGlobalFreeze));
            env.close();

            // create escrow success
            seq1 = env.seq(alice);
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            // set global freeze
            env(fset(gw, asfGlobalFreeze));
            env.close();

            // bob finish escrow success regardless of frozen assets
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150));
            env.close();

            // clear global freeze
            env(fclear(gw, asfGlobalFreeze));
            env.close();

            // create escrow success
            seq1 = env.seq(alice);
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::cancel_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            // set global freeze
            env(fset(gw, asfGlobalFreeze));
            env.close();

            // bob cancel escrow success regardless of frozen assets
            env(escrow::cancel(bob, alice, seq1), fee(baseFee));
            env.close();
        }

        // test Individual Freeze
        {
            // Env Setup
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env(trust(alice, USD(100'000)));
            env(trust(bob, USD(100'000)));
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            // set freeze on alice trustline
            env(trust(gw, USD(10'000), alice, tfSetFreeze));
            env.close();

            // setup transaction
            auto seq1 = env.seq(alice);
            auto const delta = USD(125);

            // create escrow fails - frozen trustline
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecFROZEN));
            env.close();

            // clear freeze on alice trustline
            env(trust(gw, USD(10'000), alice, tfClearFreeze));
            env.close();

            // create escrow success
            seq1 = env.seq(alice);
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            // set freeze on bob trustline
            env(trust(gw, USD(10'000), bob, tfSetFreeze));
            env.close();

            // bob finish escrow success regardless of frozen assets
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150));
            env.close();

            // reset freeze on bob and alice trustline
            env(trust(gw, USD(10'000), alice, tfClearFreeze));
            env(trust(gw, USD(10'000), bob, tfClearFreeze));
            env.close();

            // create escrow success
            seq1 = env.seq(alice);
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::cancel_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            // set freeze on bob trustline
            env(trust(gw, USD(10'000), bob, tfSetFreeze));
            env.close();

            // bob cancel escrow success regardless of frozen assets
            env(escrow::cancel(bob, alice, seq1), fee(baseFee));
            env.close();
        }

        // test Deep Freeze
        {
            // Env Setup
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env(trust(alice, USD(100'000)));
            env(trust(bob, USD(100'000)));
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            // set freeze on alice trustline
            env(trust(gw, USD(10'000), alice, tfSetFreeze | tfSetDeepFreeze));
            env.close();

            // setup transaction
            auto seq1 = env.seq(alice);
            auto const delta = USD(125);

            // create escrow fails - frozen trustline
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecFROZEN));
            env.close();

            // clear freeze on alice trustline
            env(trust(gw, USD(10'000), alice, tfClearFreeze | tfClearDeepFreeze));
            env.close();

            // create escrow success
            seq1 = env.seq(alice);
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            // set freeze on bob trustline
            env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze));
            env.close();

            // bob finish escrow fails because of deep frozen assets
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tecFROZEN));
            env.close();

            // reset freeze on alice and bob trustline
            env(trust(gw, USD(10'000), alice, tfClearFreeze | tfClearDeepFreeze));
            env(trust(gw, USD(10'000), bob, tfClearFreeze | tfClearDeepFreeze));
            env.close();

            // create escrow success
            seq1 = env.seq(alice);
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::cancel_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            // set freeze on bob trustline
            env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze));
            env.close();

            // bob cancel escrow fails because of deep frozen assets
            env(escrow::cancel(bob, alice, seq1), fee(baseFee), ter(tesSUCCESS));
            env.close();
        }
    }
    void
    testIOUInsufficientFunds(FeatureBitset features)
    {
        testcase("IOU Insufficient Funds");
        using namespace test::jtx;
        using namespace std::literals;

        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const carol = Account("carol");
        auto const gw = Account{"gateway"};
        auto const USD = gw["USD"];
        {
            // test tecPATH_PARTIAL
            // ie. has 10'000, escrow 1'000 then try to pay 10'000
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(100'000), alice);
            env.trust(USD(100'000), bob);
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            // create escrow success
            auto const delta = USD(1'000);
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();
            env(pay(alice, gw, USD(10'000)), ter(tecPATH_PARTIAL));
        }
        {
            // test tecINSUFFICIENT_FUNDS
            // ie. has 10'000 escrow 1'000 then try to escrow 10'000
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(100'000), alice);
            env.trust(USD(100'000), bob);
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env(pay(gw, bob, USD(10'000)));
            env.close();

            auto const delta = USD(1'000);
            env(escrow::create(alice, bob, delta),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            env(escrow::create(alice, bob, USD(10'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecINSUFFICIENT_FUNDS));
            env.close();
        }
    }

    void
    testIOUPrecisionLoss(FeatureBitset features)
    {
        testcase("IOU Precision Loss");
        using namespace test::jtx;
        using namespace std::literals;

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

        // test min create precision loss
        {
            Env env(*this, features);
            auto const baseFee = env.current()->fees().base;
            env.fund(XRP(10'000), alice, bob, gw);
            env(fset(gw, asfAllowTrustLineLocking));
            env.close();
            env.trust(USD(100000000000000000), alice);
            env.trust(USD(100000000000000000), bob);
            env.close();
            env(pay(gw, alice, USD(10000000000000000)));
            env(pay(gw, bob, USD(1)));
            env.close();

            bool const largeMantissa =
                features[featureSingleAssetVault] || features[featureLendingProtocol];

            // alice cannot create escrow for 1/10 iou - precision loss
            env(escrow::create(alice, bob, USD(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(largeMantissa ? (TER)tesSUCCESS : (TER)tecPRECISION_LOSS));
            env.close();

            auto const seq1 = env.seq(alice);
            // alice can create escrow for 1'000 iou
            env(escrow::create(alice, bob, USD(1'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            // bob finish escrow success
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150));
            env.close();
        }
    }

    void
    testMPTEnablement(FeatureBitset features)
    {
        testcase("MPT Enablement");

        using namespace jtx;
        using namespace std::chrono;

        for (bool const withTokenEscrow : {false, true})
        {
            auto const amend = withTokenEscrow ? features : features - featureTokenEscrow;
            Env env{*this, amend};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");
            env.fund(XRP(5000), bob);

            MPTTester mptGw(env, gw, {.holders = {alice}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env.close();

            auto const createResult = withTokenEscrow ? ter(tesSUCCESS) : ter(temBAD_AMOUNT);
            auto const finishResult = withTokenEscrow ? ter(tesSUCCESS) : ter(tecNO_TARGET);

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, MPT(1'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                createResult);
            env.close();
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                finishResult);
            env.close();
            auto const seq2 = env.seq(alice);
            env(escrow::create(alice, bob, MPT(1'000)),
                escrow::condition(escrow::cb2),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 2s),
                fee(baseFee * 150),
                createResult);
            env.close();
            env(escrow::cancel(bob, alice, seq2), finishResult);
            env.close();
        }
    }

    void
    testMPTCreatePreflight(FeatureBitset features)
    {
        testcase("MPT Create Preflight");
        using namespace test::jtx;
        using namespace std::literals;

        for (bool const withMPT : {true, false})
        {
            auto const amend = withMPT ? features : features - featureMPTokensV1;
            Env env{*this, amend};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");
            env.fund(XRP(1'000), alice, bob, gw);

            Json::Value jv = escrow::create(alice, bob, XRP(1));
            jv.removeMember(jss::Amount);
            jv[jss::Amount][jss::mpt_issuance_id] =
                "00000004A407AF5856CCF3C42619DAA925813FC955C72983";
            jv[jss::Amount][jss::value] = "-1";

            auto const result = withMPT ? ter(temBAD_AMOUNT) : ter(temDISABLED);
            env(jv,
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                result);
            env.close();
        }

        // temBAD_AMOUNT: amount < 0
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();

            env(escrow::create(alice, bob, MPT(-1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(temBAD_AMOUNT));
            env.close();
        }
    }

    void
    testMPTCreatePreclaim(FeatureBitset features)
    {
        testcase("MPT Create Preclaim");
        using namespace test::jtx;
        using namespace std::literals;

        // tecNO_PERMISSION: issuer is the same as the account
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env.close();

            env(escrow::create(gw, alice, MPT(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecNO_PERMISSION));
            env.close();
        }

        // tecOBJECT_NOT_FOUND: mpt does not exist
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");
            env.fund(XRP(10'000), alice, bob, gw);
            env.close();

            auto const mpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
            Json::Value jv = escrow::create(alice, bob, mpt(2));
            jv[jss::Amount][jss::mpt_issuance_id] =
                "00000004A407AF5856CCF3C42619DAA925813FC955C72983";
            env(jv,
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecOBJECT_NOT_FOUND));
            env.close();
        }

        // tecNO_PERMISSION: tfMPTCanEscrow is not enabled
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();

            env(escrow::create(alice, bob, MPT(3)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecNO_PERMISSION));
            env.close();
        }

        // tecOBJECT_NOT_FOUND: account does not have the mpt
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            auto const MPT = mptGw["MPT"];

            env(escrow::create(alice, bob, MPT(4)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecOBJECT_NOT_FOUND));
            env.close();
        }

        // tecNO_AUTH: requireAuth set: account not authorized
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = gw, .holder = alice});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env.close();

            // unauthorize account
            mptGw.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize});

            env(escrow::create(alice, bob, MPT(5)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecNO_AUTH));
            env.close();
        }

        // tecNO_AUTH: requireAuth set: dest not authorized
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = gw, .holder = alice});
            mptGw.authorize({.account = bob});
            mptGw.authorize({.account = gw, .holder = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();

            // unauthorize dest
            mptGw.authorize({.account = gw, .holder = bob, .flags = tfMPTUnauthorize});

            env(escrow::create(alice, bob, MPT(6)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecNO_AUTH));
            env.close();
        }

        // tecLOCKED: issuer has locked the account
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();

            // lock account
            mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock});

            env(escrow::create(alice, bob, MPT(7)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecLOCKED));
            env.close();
        }

        // tecLOCKED: issuer has locked the dest
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();

            // lock dest
            mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock});

            env(escrow::create(alice, bob, MPT(8)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecLOCKED));
            env.close();
        }

        // tecNO_AUTH: mpt cannot be transferred
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();

            env(escrow::create(alice, bob, MPT(9)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecNO_AUTH));
            env.close();
        }

        // tecINSUFFICIENT_FUNDS: spendable amount is zero
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, bob, MPT(10)));
            env.close();

            env(escrow::create(alice, bob, MPT(11)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecINSUFFICIENT_FUNDS));
            env.close();
        }

        // tecINSUFFICIENT_FUNDS: spendable amount is less than the amount
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10)));
            env(pay(gw, bob, MPT(10)));
            env.close();

            env(escrow::create(alice, bob, MPT(11)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tecINSUFFICIENT_FUNDS));
            env.close();
        }
    }

    void
    testMPTFinishPreclaim(FeatureBitset features)
    {
        testcase("MPT Finish Preclaim");
        using namespace test::jtx;
        using namespace std::literals;

        // tecNO_AUTH: requireAuth set: dest not authorized
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = gw, .holder = alice});
            mptGw.authorize({.account = bob});
            mptGw.authorize({.account = gw, .holder = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, MPT(10)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            // unauthorize dest
            mptGw.authorize({.account = gw, .holder = bob, .flags = tfMPTUnauthorize});

            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tecNO_AUTH));
            env.close();
        }

        // tecOBJECT_NOT_FOUND: MPT issuance does not exist
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            env.fund(XRP(10'000), alice, bob);
            env.close();

            auto const seq1 = env.seq(alice);
            env.app().openLedger().modify([&](OpenView& view, beast::Journal j) {
                Sandbox sb(&view, tapNONE);
                auto sleNew = std::make_shared<SLE>(keylet::escrow(alice, seq1));
                MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}};
                STAmount amt(mpt, 10);
                sleNew->setAccountID(sfDestination, bob);
                sleNew->setFieldAmount(sfAmount, amt);
                sb.insert(sleNew);
                sb.apply(view);
                return true;
            });

            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tecOBJECT_NOT_FOUND));
            env.close();
        }

        // tecLOCKED: issuer has locked the dest
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, MPT(8)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            // lock dest
            mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock});

            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tecLOCKED));
            env.close();
        }
    }

    void
    testMPTFinishDoApply(FeatureBitset features)
    {
        testcase("MPT Finish Do Apply");
        using namespace test::jtx;
        using namespace std::literals;

        // tecINSUFFICIENT_RESERVE: insufficient reserve to create MPT
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const acctReserve = env.current()->fees().reserve;
            auto const incReserve = env.current()->fees().increment;

            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");
            env.fund(acctReserve + (incReserve - 1), bob);
            env.close();

            MPTTester mptGw(env, gw, {.holders = {alice}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, MPT(10)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tecINSUFFICIENT_RESERVE));
            env.close();
        }

        // tesSUCCESS: bob submits; finish MPT created
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");
            env.fund(XRP(10'000), bob);
            env.close();

            MPTTester mptGw(env, gw, {.holders = {alice}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, MPT(10)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();
        }

        // tecNO_PERMISSION: carol submits; finish MPT not created
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const carol = Account("carol");
            auto const gw = Account("gw");
            env.fund(XRP(10'000), bob, carol);
            env.close();

            MPTTester mptGw(env, gw, {.holders = {alice}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, MPT(10)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            env(escrow::finish(carol, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tecNO_PERMISSION));
            env.close();
        }
    }

    void
    testMPTCancelPreclaim(FeatureBitset features)
    {
        testcase("MPT Cancel Preclaim");
        using namespace test::jtx;
        using namespace std::literals;

        // tecNO_AUTH: requireAuth set: account not authorized
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = gw, .holder = alice});
            mptGw.authorize({.account = bob});
            mptGw.authorize({.account = gw, .holder = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, MPT(10)),
                escrow::cancel_time(env.now() + 2s),
                escrow::condition(escrow::cb1),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            // unauthorize account
            mptGw.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize});

            env(escrow::cancel(bob, alice, seq1), ter(tecNO_AUTH));
            env.close();
        }

        // tecOBJECT_NOT_FOUND: MPT issuance does not exist
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            env.fund(XRP(10'000), alice, bob);

            auto const seq1 = env.seq(alice);
            env.app().openLedger().modify([&](OpenView& view, beast::Journal j) {
                Sandbox sb(&view, tapNONE);
                auto sleNew = std::make_shared<SLE>(keylet::escrow(alice, seq1));
                MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}};
                STAmount amt(mpt, 10);
                sleNew->setAccountID(sfDestination, bob);
                sleNew->setFieldAmount(sfAmount, amt);
                sb.insert(sleNew);
                sb.apply(view);
                return true;
            });

            env(escrow::cancel(bob, alice, seq1), fee(baseFee), ter(tecOBJECT_NOT_FOUND));
            env.close();
        }
    }

    void
    testMPTBalances(FeatureBitset features)
    {
        testcase("MPT Balances");

        using namespace jtx;
        using namespace std::chrono;

        Env env{*this, features};
        auto const baseFee = env.current()->fees().base;
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const carol = Account("carol");
        auto const gw = Account("gw");
        env.fund(XRP(5000), bob);

        MPTTester mptGw(env, gw, {.holders = {alice, carol}});
        mptGw.create(
            {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
        mptGw.authorize({.account = alice});
        mptGw.authorize({.account = carol});
        auto const MPT = mptGw["MPT"];
        env(pay(gw, alice, MPT(10'000)));
        env(pay(gw, carol, MPT(10'000)));
        env.close();

        auto outstandingMPT = env.balance(gw, MPT);

        // Create & Finish Escrow
        auto const seq1 = env.seq(alice);
        {
            auto const preAliceMPT = env.balance(alice, MPT);
            auto const preBobMPT = env.balance(bob, MPT);
            env(escrow::create(alice, bob, MPT(1'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000);
            BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT);
            BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0);
            BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000);
        }
        {
            auto const preAliceMPT = env.balance(alice, MPT);
            auto const preBobMPT = env.balance(bob, MPT);
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT);
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
            BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT + MPT(1'000));
            BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0);
            BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0);
        }

        // Create & Cancel Escrow
        auto const seq2 = env.seq(alice);
        {
            auto const preAliceMPT = env.balance(alice, MPT);
            auto const preBobMPT = env.balance(bob, MPT);
            env(escrow::create(alice, bob, MPT(1'000)),
                escrow::condition(escrow::cb2),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 2s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000);
            BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT);
            BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0);
            BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000);
        }
        {
            auto const preAliceMPT = env.balance(alice, MPT);
            auto const preBobMPT = env.balance(bob, MPT);
            env(escrow::cancel(bob, alice, seq2), ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT + MPT(1'000));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
            BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT);
            BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0);
            BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0);
        }

        // Self Escrow Create & Finish
        {
            auto const seq = env.seq(alice);
            auto const preAliceMPT = env.balance(alice, MPT);
            env(escrow::create(alice, alice, MPT(1'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000);
            BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000);

            env(escrow::finish(alice, alice, seq),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT);
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
            BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0);
        }

        // Self Escrow Create & Cancel
        {
            auto const seq = env.seq(alice);
            auto const preAliceMPT = env.balance(alice, MPT);
            env(escrow::create(alice, alice, MPT(1'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 2s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000);
            BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000);

            env(escrow::cancel(alice, alice, seq), ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT);
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
            BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0);
        }

        // Multiple Escrows
        {
            auto const preAliceMPT = env.balance(alice, MPT);
            auto const preBobMPT = env.balance(bob, MPT);
            auto const preCarolMPT = env.balance(carol, MPT);
            env(escrow::create(alice, bob, MPT(1'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            env(escrow::create(carol, bob, MPT(1'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000);
            BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT);
            BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0);
            BEAST_EXPECT(env.balance(carol, MPT) == preCarolMPT - MPT(1'000));
            BEAST_EXPECT(mptEscrowed(env, carol, MPT) == 1'000);
            BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 2'000);
        }

        // Max MPT Amount Issued (Escrow 1 MPT)
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(maxMPTokenAmount)));
            env.close();

            auto const preAliceMPT = env.balance(alice, MPT);
            auto const preBobMPT = env.balance(bob, MPT);
            auto const outstandingMPT = env.balance(gw, MPT);

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, MPT(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1);
            BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT);
            BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0);
            BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1);

            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
            BEAST_EXPECT(
                !env.le(keylet::mptoken(MPT.mpt(), alice))->isFieldPresent(sfLockedAmount));
            BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT + MPT(1));
            BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0);
            BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0);
            BEAST_EXPECT(!env.le(keylet::mptIssuance(MPT.mpt()))->isFieldPresent(sfLockedAmount));
        }

        // Max MPT Amount Issued (Escrow Max MPT)
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(maxMPTokenAmount)));
            env.close();

            auto const preAliceMPT = env.balance(alice, MPT);
            auto const preBobMPT = env.balance(bob, MPT);
            auto const outstandingMPT = env.balance(gw, MPT);

            // Escrow Max MPT - 10
            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, MPT(maxMPTokenAmount - 10)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            // Escrow 10 MPT
            auto const seq2 = env.seq(alice);
            env(escrow::create(alice, bob, MPT(10)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(maxMPTokenAmount));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == maxMPTokenAmount);
            BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT);
            BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0);
            BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == maxMPTokenAmount);

            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            env(escrow::finish(bob, alice, seq2),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(maxMPTokenAmount));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
            BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT + MPT(maxMPTokenAmount));
            BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0);
            BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0);
        }
    }

    void
    testMPTMetaAndOwnership(FeatureBitset features)
    {
        using namespace jtx;
        using namespace std::chrono;

        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const carol = Account("carol");
        auto const gw = Account{"gateway"};
        {
            testcase("MPT Metadata to self");

            Env env{*this, features};
            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();
            auto const aseq = env.seq(alice);
            auto const bseq = env.seq(bob);

            env(escrow::create(alice, alice, MPT(1'000)),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 500s));
            BEAST_EXPECT(
                (*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
            env.close(5s);
            auto const aa = env.le(keylet::escrow(alice.id(), aseq));
            BEAST_EXPECT(aa);
            {
                xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id()));
                BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2);
                BEAST_EXPECT(std::find(aod.begin(), aod.end(), aa) != aod.end());
            }

            {
                xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id()));
                BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 1);
                BEAST_EXPECT(std::find(iod.begin(), iod.end(), aa) == iod.end());
            }

            env(escrow::create(bob, bob, MPT(1'000)),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 2s));
            BEAST_EXPECT(
                (*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
            env.close(5s);
            auto const bb = env.le(keylet::escrow(bob.id(), bseq));
            BEAST_EXPECT(bb);

            {
                xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id()));
                BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2);
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), bb) != bod.end());
            }

            env.close(5s);
            env(escrow::finish(alice, alice, aseq));
            {
                BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq)));
                BEAST_EXPECT(
                    (*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));

                xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id()));
                BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
                BEAST_EXPECT(std::find(aod.begin(), aod.end(), aa) == aod.end());

                xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id()));
                BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2);
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), bb) != bod.end());
            }

            env.close(5s);
            env(escrow::cancel(bob, bob, bseq));
            {
                BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq)));
                BEAST_EXPECT(
                    (*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));

                xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id()));
                BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1);
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), bb) == bod.end());
            }
        }

        {
            testcase("MPT Metadata to other");

            Env env{*this, features};
            MPTTester mptGw(env, gw, {.holders = {alice, bob, carol}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            mptGw.authorize({.account = carol});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env(pay(gw, carol, MPT(10'000)));
            env.close();
            auto const aseq = env.seq(alice);
            auto const bseq = env.seq(bob);

            env(escrow::create(alice, bob, MPT(1'000)), escrow::finish_time(env.now() + 1s));
            BEAST_EXPECT(
                (*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
            env.close(5s);
            env(escrow::create(bob, carol, MPT(1'000)),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 2s));
            BEAST_EXPECT(
                (*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
            env.close(5s);

            auto const ab = env.le(keylet::escrow(alice.id(), aseq));
            BEAST_EXPECT(ab);

            auto const bc = env.le(keylet::escrow(bob.id(), bseq));
            BEAST_EXPECT(bc);

            {
                xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id()));
                BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2);
                BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) != aod.end());

                xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id()));
                BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3);
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) != bod.end());
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) != bod.end());

                xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id()));
                BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2);
                BEAST_EXPECT(std::find(cod.begin(), cod.end(), bc) != cod.end());
            }

            env.close(5s);
            env(escrow::finish(alice, alice, aseq));
            {
                BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq)));
                BEAST_EXPECT(env.le(keylet::escrow(bob.id(), bseq)));

                xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id()));
                BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
                BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) == aod.end());

                xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id()));
                BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2);
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end());
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) != bod.end());

                xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id()));
                BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2);
            }

            env.close(5s);
            env(escrow::cancel(bob, bob, bseq));
            {
                BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq)));
                BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq)));

                xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id()));
                BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
                BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) == aod.end());

                xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id()));
                BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1);
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end());
                BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) == bod.end());

                xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id()));
                BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1);
            }
        }
    }

    void
    testMPTGateway(FeatureBitset features)
    {
        testcase("MPT Gateway Balances");
        using namespace test::jtx;
        using namespace std::literals;

        // issuer is dest; alice w/ authorization
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env.close();

            // issuer can be destination
            auto const seq1 = env.seq(alice);
            auto const preAliceMPT = env.balance(alice, MPT);
            auto const preOutstanding = env.balance(gw, MPT);
            auto const preEscrowed = issuerMPTEscrowed(env, MPT);
            BEAST_EXPECT(preOutstanding == MPT(-10'000));
            BEAST_EXPECT(preEscrowed == 0);

            env(escrow::create(alice, gw, MPT(1'000)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000);
            BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == preEscrowed + 1'000);

            // issuer (dest) can finish escrow
            env(escrow::finish(gw, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
            BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding + MPT(1'000));
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == preEscrowed);
        }
    }

    void
    testMPTLockedRate(FeatureBitset features)
    {
        testcase("MPT Locked Rate");
        using namespace test::jtx;
        using namespace std::literals;

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

        // test locked rate: finish
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.transferFee = 25000,
                 .ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();

            // alice can create escrow w/ xfer rate
            auto const preAlice = env.balance(alice, MPT);
            auto const seq1 = env.seq(alice);
            auto const delta = MPT(125);
            env(escrow::create(alice, bob, MPT(125)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();
            auto const transferRate = escrow::rate(env, alice, seq1);
            BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));

            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125);
            BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000));

            // bob can finish escrow
            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta);
            BEAST_EXPECT(env.balance(bob, MPT) == MPT(10'100));

            auto const escrowedWithFix = env.current()->rules().enabled(fixTokenEscrowV1) ? 0 : 25;
            auto const outstandingWithFix =
                env.current()->rules().enabled(fixTokenEscrowV1) ? MPT(19'975) : MPT(20'000);
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == escrowedWithFix);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == escrowedWithFix);
            BEAST_EXPECT(env.balance(gw, MPT) == -outstandingWithFix);
        }

        // test locked rate: cancel
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.transferFee = 25000,
                 .ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();

            // alice can create escrow w/ xfer rate
            auto const preAlice = env.balance(alice, MPT);
            auto const preBob = env.balance(bob, MPT);
            auto const seq1 = env.seq(alice);
            auto const delta = MPT(125);
            env(escrow::create(alice, bob, MPT(125)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 3s),
                fee(baseFee * 150));
            env.close();
            auto const transferRate = escrow::rate(env, alice, seq1);
            BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));

            // alice can cancel escrow
            env(escrow::cancel(alice, alice, seq1), fee(baseFee));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAlice);
            BEAST_EXPECT(env.balance(bob, MPT) == preBob);
            BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0);
        }

        // test locked rate: issuer is destination
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.transferFee = 25000,
                 .ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();

            // alice can create escrow w/ xfer rate
            auto const preAlice = env.balance(alice, MPT);
            auto const seq1 = env.seq(alice);
            auto const delta = MPT(125);
            env(escrow::create(alice, gw, MPT(125)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();
            auto const transferRate = escrow::rate(env, alice, seq1);
            BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));

            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125);
            BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000));

            // bob can finish escrow
            env(escrow::finish(gw, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta);
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
            BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0);
            BEAST_EXPECT(env.balance(gw, MPT) == MPT(-19'875));
        }
    }

    void
    testMPTRequireAuth(FeatureBitset features)
    {
        testcase("MPT Require Auth");
        using namespace test::jtx;
        using namespace std::literals;

        Env env{*this, features};
        auto const baseFee = env.current()->fees().base;
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const gw = Account("gw");

        MPTTester mptGw(env, gw, {.holders = {alice, bob}});
        mptGw.create(
            {.ownerCount = 1,
             .holderCount = 0,
             .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth});
        mptGw.authorize({.account = alice});
        mptGw.authorize({.account = gw, .holder = alice});
        mptGw.authorize({.account = bob});
        mptGw.authorize({.account = gw, .holder = bob});
        auto const MPT = mptGw["MPT"];
        env(pay(gw, alice, MPT(10'000)));
        env.close();

        auto seq = env.seq(alice);
        auto const delta = MPT(125);
        // alice can create escrow - is authorized
        env(escrow::create(alice, bob, MPT(100)),
            escrow::condition(escrow::cb1),
            escrow::finish_time(env.now() + 1s),
            fee(baseFee * 150));
        env.close();

        // bob can finish escrow - is authorized
        env(escrow::finish(bob, alice, seq),
            escrow::condition(escrow::cb1),
            escrow::fulfillment(escrow::fb1),
            fee(baseFee * 150));
        env.close();
    }

    void
    testMPTLock(FeatureBitset features)
    {
        testcase("MPT Lock");
        using namespace test::jtx;
        using namespace std::literals;

        Env env{*this, features};
        auto const baseFee = env.current()->fees().base;
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const gw = Account("gw");

        MPTTester mptGw(env, gw, {.holders = {alice, bob}});
        mptGw.create(
            {.ownerCount = 1,
             .holderCount = 0,
             .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock});
        mptGw.authorize({.account = alice});
        mptGw.authorize({.account = bob});
        auto const MPT = mptGw["MPT"];
        env(pay(gw, alice, MPT(10'000)));
        env(pay(gw, bob, MPT(10'000)));
        env.close();

        // alice create escrow
        auto seq1 = env.seq(alice);
        env(escrow::create(alice, bob, MPT(100)),
            escrow::condition(escrow::cb1),
            escrow::finish_time(env.now() + 1s),
            escrow::cancel_time(env.now() + 2s),
            fee(baseFee * 150));
        env.close();

        // lock account & dest
        mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock});
        mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock});

        // bob cannot finish
        env(escrow::finish(bob, alice, seq1),
            escrow::condition(escrow::cb1),
            escrow::fulfillment(escrow::fb1),
            fee(baseFee * 150),
            ter(tecLOCKED));
        env.close();

        // bob can cancel
        env(escrow::cancel(bob, alice, seq1));
        env.close();
    }

    void
    testMPTCanTransfer(FeatureBitset features)
    {
        testcase("MPT Can Transfer");
        using namespace test::jtx;
        using namespace std::literals;

        Env env{*this, features};
        auto const baseFee = env.current()->fees().base;
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const gw = Account("gw");

        MPTTester mptGw(env, gw, {.holders = {alice, bob}});
        mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow});
        mptGw.authorize({.account = alice});
        mptGw.authorize({.account = bob});
        auto const MPT = mptGw["MPT"];
        env(pay(gw, alice, MPT(10'000)));
        env(pay(gw, bob, MPT(10'000)));
        env.close();

        // alice cannot create escrow to non issuer
        env(escrow::create(alice, bob, MPT(100)),
            escrow::condition(escrow::cb1),
            escrow::finish_time(env.now() + 1s),
            escrow::cancel_time(env.now() + 2s),
            fee(baseFee * 150),
            ter(tecNO_AUTH));
        env.close();

        // Escrow Create & Finish
        {
            // alice an create escrow to issuer
            auto seq = env.seq(alice);
            env(escrow::create(alice, gw, MPT(100)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            // gw can finish
            env(escrow::finish(gw, alice, seq),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150));
            env.close();
        }

        // Escrow Create & Cancel
        {
            // alice an create escrow to issuer
            auto seq = env.seq(alice);
            env(escrow::create(alice, gw, MPT(100)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 2s),
                fee(baseFee * 150));
            env.close();

            // alice can cancel
            env(escrow::cancel(alice, alice, seq));
            env.close();
        }
    }

    void
    testMPTDestroy(FeatureBitset features)
    {
        testcase("MPT Destroy");
        using namespace test::jtx;
        using namespace std::literals;

        // tecHAS_OBLIGATIONS: issuer cannot destroy issuance
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");

            MPTTester mptGw(env, gw, {.holders = {alice, bob}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            mptGw.authorize({.account = bob});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env(pay(gw, bob, MPT(10'000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, MPT(10)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150));
            env.close();

            env(pay(alice, gw, MPT(10'000)), ter(tecPATH_PARTIAL));
            env(pay(alice, gw, MPT(9'990)));
            env(pay(bob, gw, MPT(10'000)));
            BEAST_EXPECT(env.balance(alice, MPT) == MPT(0));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 10);
            BEAST_EXPECT(env.balance(bob, MPT) == MPT(0));
            BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0);
            BEAST_EXPECT(env.balance(gw, MPT) == MPT(-10));
            mptGw.authorize({.account = bob, .flags = tfMPTUnauthorize});
            mptGw.destroy({.id = mptGw.issuanceID(), .ownerCount = 1, .err = tecHAS_OBLIGATIONS});

            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            env(pay(bob, gw, MPT(10)));
            mptGw.destroy({.id = mptGw.issuanceID(), .ownerCount = 0});
        }

        // tecHAS_OBLIGATIONS: holder cannot destroy mptoken
        {
            Env env{*this, features};
            auto const baseFee = env.current()->fees().base;
            auto const alice = Account("alice");
            auto const bob = Account("bob");
            auto const gw = Account("gw");
            env.fund(XRP(10'000), bob);
            env.close();

            MPTTester mptGw(env, gw, {.holders = {alice}});
            mptGw.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
            mptGw.authorize({.account = alice});
            auto const MPT = mptGw["MPT"];
            env(pay(gw, alice, MPT(10'000)));
            env.close();

            auto const seq1 = env.seq(alice);
            env(escrow::create(alice, bob, MPT(10)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            env(pay(alice, gw, MPT(9'990)));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == MPT(0));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 10);
            mptGw.authorize(
                {.account = alice, .flags = tfMPTUnauthorize, .err = tecHAS_OBLIGATIONS});

            env(escrow::finish(bob, alice, seq1),
                escrow::condition(escrow::cb1),
                escrow::fulfillment(escrow::fb1),
                fee(baseFee * 150),
                ter(tesSUCCESS));
            env.close();

            BEAST_EXPECT(env.balance(alice, MPT) == MPT(0));
            BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
            mptGw.authorize({.account = alice, .flags = tfMPTUnauthorize});
            BEAST_EXPECT(!env.le(keylet::mptoken(MPT.mpt(), alice)));
        }
    }

    void
    testIOUWithFeats(FeatureBitset features)
    {
        testIOUEnablement(features);
        testIOUAllowLockingFlag(features);
        testIOUCreatePreflight(features);
        testIOUCreatePreclaim(features);
        testIOUFinishPreclaim(features);
        testIOUFinishDoApply(features);
        testIOUCancelPreclaim(features);
        testIOUBalances(features);
        testIOUMetaAndOwnership(features);
        testIOURippleState(features);
        testIOUGateway(features);
        testIOULockedRate(features);
        testIOULimitAmount(features);
        testIOURequireAuth(features);
        testIOUFreeze(features);
        testIOUInsufficientFunds(features);
        testIOUPrecisionLoss(features);
    }

    void
    testMPTWithFeats(FeatureBitset features)
    {
        testMPTEnablement(features);
        testMPTCreatePreflight(features);
        testMPTCreatePreclaim(features);
        testMPTFinishPreclaim(features);
        testMPTFinishDoApply(features);
        testMPTCancelPreclaim(features);
        testMPTBalances(features);
        testMPTMetaAndOwnership(features);
        testMPTGateway(features);
        testMPTLockedRate(features);
        testMPTRequireAuth(features);
        testMPTLock(features);
        testMPTCanTransfer(features);
        testMPTDestroy(features);
    }

public:
    void
    run() override
    {
        using namespace test::jtx;
        FeatureBitset const all{testable_amendments()};
        for (FeatureBitset const& feats :
             {all - featureSingleAssetVault - featureLendingProtocol, all})
        {
            testIOUWithFeats(feats);
            testMPTWithFeats(feats);
            testMPTWithFeats(feats - fixTokenEscrowV1);
        }
    }
};

BEAST_DEFINE_TESTSUITE(EscrowToken, app, xrpl);

}  // namespace test
}  // namespace xrpl
