#include <test/jtx.h>
#include <test/jtx/Env.h>
#include <test/jtx/attester.h>
#include <test/jtx/multisign.h>
#include <test/jtx/xchain_bridge.h>

#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XChainAttestations.h>

#include <functional>
#include <limits>
#include <optional>
#include <random>
#include <string>
#include <tuple>
#include <variant>
#include <vector>

namespace xrpl::test {

// SEnv class - encapsulate jtx::Env to make it more user-friendly,
// for example having APIs that return a *this reference so that calls can be
// chained (fluent interface) allowing to create an environment and use it
// without encapsulating it in a curly brace block.
// ---------------------------------------------------------------------------
template <class T>
struct SEnv
{
    jtx::Env env_;

    SEnv(
        T& s,
        std::unique_ptr<Config> config,
        FeatureBitset features,
        std::unique_ptr<Logs> logs = nullptr,
        beast::severities::Severity thresh = beast::severities::kError)
        : env_(s, std::move(config), features, std::move(logs), thresh)
    {
    }

    SEnv&
    close()
    {
        env_.close();
        return *this;
    }

    SEnv&
    enableFeature(uint256 const feature)
    {
        env_.enableFeature(feature);
        return *this;
    }

    SEnv&
    disableFeature(uint256 const feature)
    {
        env_.app().config().features.erase(feature);
        return *this;
    }

    template <class Arg, class... Args>
    SEnv&
    fund(STAmount const& amount, Arg const& arg, Args const&... args)
    {
        env_.fund(amount, arg, args...);
        return *this;
    }

    template <class JsonValue, class... FN>
    SEnv&
    tx(JsonValue&& jv, FN const&... fN)
    {
        env_(std::forward<JsonValue>(jv), fN...);
        return *this;
    }

    template <class... FN>
    SEnv&
    multiTx(jtx::JValueVec const& jvv, FN const&... fN)
    {
        for (auto const& jv : jvv)
            env_(jv, fN...);
        return *this;
    }

    TER
    ter() const
    {
        return env_.ter();
    }

    STAmount
    balance(jtx::Account const& account) const
    {
        return env_.balance(account).value();
    }

    STAmount
    balance(jtx::Account const& account, Issue const& issue) const
    {
        return env_.balance(account, issue).value();
    }

    XRPAmount
    reserve(std::uint32_t count)
    {
        return env_.current()->fees().accountReserve(count);
    }

    XRPAmount
    txFee()
    {
        return env_.current()->fees().base;
    }

    std::shared_ptr<SLE const>
    account(jtx::Account const& account)
    {
        return env_.le(account);
    }

    std::shared_ptr<SLE const>
    bridge(Json::Value const& jvb)
    {
        STXChainBridge b(jvb);

        auto tryGet = [&](STXChainBridge::ChainType ct) -> std::shared_ptr<SLE const> {
            if (auto r = env_.le(keylet::bridge(b, ct)))
            {
                if ((*r)[sfXChainBridge] == b)
                    return r;
            }
            return nullptr;
        };
        if (auto r = tryGet(STXChainBridge::ChainType::locking))
            return r;
        return tryGet(STXChainBridge::ChainType::issuing);
    }

    std::uint64_t
    claimCount(Json::Value const& jvb)
    {
        return (*bridge(jvb))[sfXChainAccountClaimCount];
    }

    std::uint64_t
    claimID(Json::Value const& jvb)
    {
        return (*bridge(jvb))[sfXChainClaimID];
    }

    std::shared_ptr<SLE const>
    claimID(Json::Value const& jvb, std::uint64_t seq)
    {
        return env_.le(keylet::xChainClaimID(STXChainBridge(jvb), seq));
    }

    std::shared_ptr<SLE const>
    caClaimID(Json::Value const& jvb, std::uint64_t seq)
    {
        return env_.le(keylet::xChainCreateAccountClaimID(STXChainBridge(jvb), seq));
    }
};

// XEnv class used for XChain tests. The only difference with SEnv<T> is that it
// funds some default accounts, and that it enables `testable_amendments() |
// FeatureBitset{featureXChainBridge}` by default.
// -----------------------------------------------------------------------------
template <class T>
struct XEnv : public jtx::XChainBridgeObjects, public SEnv<T>
{
    XEnv(T& s, bool side = false) : SEnv<T>(s, jtx::envconfig(), features)
    {
        using namespace jtx;
        STAmount xrp_funds{XRP(10000)};

        if (!side)
        {
            this->fund(xrp_funds, mcDoor, mcAlice, mcBob, mcCarol, mcGw);

            // Signer's list must match the attestation signers
            // env_(jtx::signers(mcDoor, quorum, signers));
            for (auto& s : signers)
                this->fund(xrp_funds, s.account);
        }
        else
        {
            this->fund(xrp_funds, scDoor, scAlice, scBob, scCarol, scGw, scAttester, scReward);

            for (auto& ra : payees)
                this->fund(xrp_funds, ra);

            for (auto& s : signers)
                this->fund(xrp_funds, s.account);

            // Signer's list must match the attestation signers
            // env_(jtx::signers(Account::master, quorum, signers));
        }
        this->close();
    }
};

// Tracks the xrp balance for one account
template <class T>
struct Balance
{
    jtx::Account const& account_;
    T& env_;
    STAmount startAmount;

    Balance(T& env, jtx::Account const& account) : account_(account), env_(env)
    {
        startAmount = env_.balance(account_);
    }

    STAmount
    diff() const
    {
        return env_.balance(account_) - startAmount;
    }
};

// Tracks the xrp balance for multiple accounts involved in a crosss-chain
// transfer
template <class T>
struct BalanceTransfer
{
    using balance = Balance<T>;

    balance from_;
    balance to_;
    balance payer_;                        // pays the rewards
    std::vector<balance> reward_accounts;  // receives the reward
    XRPAmount txFees_;

    BalanceTransfer(
        T& env,
        jtx::Account const& from_acct,
        jtx::Account const& to_acct,
        jtx::Account const& payer,
        jtx::Account const* payees,
        size_t num_payees,
        bool withClaim)
        : from_(env, from_acct)
        , to_(env, to_acct)
        , payer_(env, payer)
        , reward_accounts([&]() {
            std::vector<balance> r;
            r.reserve(num_payees);
            for (size_t i = 0; i < num_payees; ++i)
                r.emplace_back(env, payees[i]);
            return r;
        }())
        , txFees_(withClaim ? env.env_.current()->fees().base : XRPAmount(0))
    {
    }

    BalanceTransfer(
        T& env,
        jtx::Account const& from_acct,
        jtx::Account const& to_acct,
        jtx::Account const& payer,
        std::vector<jtx::Account> const& payees,
        bool withClaim)
        : BalanceTransfer(env, from_acct, to_acct, payer, &payees[0], payees.size(), withClaim)
    {
    }

    bool
    payees_received(STAmount const& reward) const
    {
        return std::all_of(reward_accounts.begin(), reward_accounts.end(), [&](balance const& b) {
            return b.diff() == reward;
        });
    }

    bool
    check_most_balances(STAmount const& amt, STAmount const& reward)
    {
        return from_.diff() == -amt && to_.diff() == amt && payees_received(reward);
    }

    bool
    has_happened(STAmount const& amt, STAmount const& reward, bool check_payer = true)
    {
        auto reward_cost = multiply(reward, STAmount(reward_accounts.size()), reward.issue());
        return check_most_balances(amt, reward) &&
            (!check_payer || payer_.diff() == -(reward_cost + txFees_));
    }

    bool
    has_not_happened()
    {
        return check_most_balances(STAmount(0), STAmount(0)) &&
            payer_.diff() <= txFees_;  // could have paid fee for failed claim
    }
};

struct BridgeDef
{
    jtx::Account doorA;
    Issue issueA;
    jtx::Account doorB;
    Issue issueB;
    STAmount reward;
    STAmount minAccountCreate;
    uint32_t quorum;
    std::vector<jtx::signer> const& signers;
    Json::Value jvb;

    template <class ENV>
    void
    initBridge(ENV& mcEnv, ENV& scEnv)
    {
        jvb = bridge(doorA, issueA, doorB, issueB);

        auto const optAccountCreate = [&]() -> std::optional<STAmount> {
            if (issueA != xrpIssue() || issueB != xrpIssue())
                return {};
            return minAccountCreate;
        }();
        mcEnv.tx(bridge_create(doorA, jvb, reward, optAccountCreate))
            .tx(jtx::signers(doorA, quorum, signers))
            .close();

        scEnv.tx(bridge_create(doorB, jvb, reward, optAccountCreate))
            .tx(jtx::signers(doorB, quorum, signers))
            .close();
    }
};

struct XChain_test : public beast::unit_test::suite, public jtx::XChainBridgeObjects
{
    XRPAmount
    reserve(std::uint32_t count)
    {
        return XEnv(*this).env_.current()->fees().accountReserve(count);
    }

    XRPAmount
    txFee()
    {
        return XEnv(*this).env_.current()->fees().base;
    }

    void
    testXChainBridgeExtraFields()
    {
        auto jBridge = create_bridge(mcDoor)[sfXChainBridge.jsonName];
        bool exceptionPresent = false;
        try
        {
            exceptionPresent = false;
            [[maybe_unused]] STXChainBridge testBridge1(jBridge);
        }
        catch (std::exception& ec)
        {
            exceptionPresent = true;
        }

        BEAST_EXPECT(!exceptionPresent);

        try
        {
            exceptionPresent = false;
            jBridge["Extra"] = 1;
            [[maybe_unused]] STXChainBridge testBridge2(jBridge);
        }
        catch ([[maybe_unused]] std::exception& ec)
        {
            exceptionPresent = true;
        }

        BEAST_EXPECT(exceptionPresent);
    }

    void
    testXChainCreateBridge()
    {
        XRPAmount res1 = reserve(1);

        using namespace jtx;
        testcase("Create Bridge");

        // Normal create_bridge => should succeed
        XEnv(*this).tx(create_bridge(mcDoor)).close();

        // Bridge not owned by one of the door account.
        XEnv(*this).tx(create_bridge(mcBob), ter(temXCHAIN_BRIDGE_NONDOOR_OWNER));

        // Create twice on the same account
        XEnv(*this).tx(create_bridge(mcDoor)).close().tx(create_bridge(mcDoor), ter(tecDUPLICATE));

        // Create USD bridge Alice -> Bob ... should succeed
        XEnv(*this).tx(
            create_bridge(mcAlice, bridge(mcAlice, mcGw["USD"], mcBob, mcBob["USD"])),
            ter(tesSUCCESS));

        // Create USD bridge, Alice is both the locking door and locking issue,
        // ... should fail.
        XEnv(*this).tx(
            create_bridge(mcAlice, bridge(mcAlice, mcAlice["USD"], mcBob, mcBob["USD"])),
            ter(temXCHAIN_BRIDGE_BAD_ISSUES));

        // Bridge where the two door accounts are equal.
        XEnv(*this).tx(
            create_bridge(mcBob, bridge(mcBob, mcGw["USD"], mcBob, mcGw["USD"])),
            ter(temXCHAIN_EQUAL_DOOR_ACCOUNTS));

        // Both door accounts are on the same chain. This is not allowed.
        // Although it doesn't violate any invariants, it's not a useful thing
        // to do and it complicates the "add claim" transactions.
        XEnv(*this)
            .tx(create_bridge(mcAlice, bridge(mcAlice, mcGw["USD"], mcBob, mcBob["USD"])))
            .close()
            .tx(create_bridge(mcBob, bridge(mcAlice, mcGw["USD"], mcBob, mcBob["USD"])),
                ter(tecDUPLICATE))
            .close();

        // Create a bridge on an account with exactly enough balance to
        // meet the new reserve should succeed
        XEnv(*this)
            .fund(res1, mcuDoor)  // exact reserve for account + 1 object
            .close()
            .tx(create_bridge(mcuDoor, jvub), ter(tesSUCCESS));

        // Create a bridge on an account with no enough balance to meet the
        // new reserve
        XEnv(*this)
            .fund(res1 - 1, mcuDoor)  // just short of required reserve
            .close()
            .tx(create_bridge(mcuDoor, jvub), ter(tecINSUFFICIENT_RESERVE));

        // Reward amount is non-xrp
        XEnv(*this).tx(
            create_bridge(mcDoor, jvb, mcUSD(1)), ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT));

        // Reward amount is XRP and negative
        XEnv(*this).tx(
            create_bridge(mcDoor, jvb, XRP(-1)), ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT));

        // Reward amount is 1 xrp => should succeed
        XEnv(*this).tx(create_bridge(mcDoor, jvb, XRP(1)), ter(tesSUCCESS));

        // Min create amount is 1 xrp, mincreate is 1 xrp => should succeed
        XEnv(*this).tx(create_bridge(mcDoor, jvb, XRP(1), XRP(1)), ter(tesSUCCESS));

        // Min create amount is non-xrp
        XEnv(*this).tx(
            create_bridge(mcDoor, jvb, XRP(1), mcUSD(100)),
            ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT));

        // Min create amount is zero (should fail, currently succeeds)
        XEnv(*this).tx(
            create_bridge(mcDoor, jvb, XRP(1), XRP(0)),
            ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT));

        // Min create amount is negative
        XEnv(*this).tx(
            create_bridge(mcDoor, jvb, XRP(1), XRP(-1)),
            ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT));

        // coverage test: BridgeCreate::preflight() - create bridge when feature
        // disabled.
        {
            Env env(*this, testable_amendments() - featureXChainBridge);
            env(create_bridge(Account::master, jvb), ter(temDISABLED));
        }

        // coverage test: BridgeCreate::preclaim() returns tecNO_ISSUER.
        XEnv(*this).tx(
            create_bridge(mcAlice, bridge(mcAlice, mcuAlice["USD"], mcBob, mcBob["USD"])),
            ter(tecNO_ISSUER));

        // coverage test: create_bridge transaction with incorrect flag
        XEnv(*this).tx(create_bridge(mcAlice, jvb), txflags(tfFillOrKill), ter(temINVALID_FLAG));

        // coverage test: create_bridge transaction with xchain feature disabled
        XEnv(*this)
            .disableFeature(featureXChainBridge)
            .tx(create_bridge(mcAlice, jvb), ter(temDISABLED));
    }

    void
    testXChainBridgeCreateConstraints()
    {
        /**
         * Bridge create constraints tests.
         *
         * Define the door's bridge asset collection as the collection of all
         * the issuing assets for which the door account is on the issuing chain
         * and all the locking assets for which the door account is on the
         * locking chain. (note: a door account can simultaneously be on an
         * issuing and locking chain). A new bridge is not a duplicate as long
         * as the new bridge asset collection does not contain any duplicate
         * currencies (even if the issuers differ).
         *
         * Create bridges:
         *
         *| Owner | Locking   | Issuing | Comment                           |
         *| a1    | a1 USD/GW | USD/B   |                                   |
         *| a2    | a2 USD/GW | USD/B   | Same locking & issuing assets     |
         *|       |           |         |                                   |
         *| a3    | a3 USD/GW | USD/a4  |                                   |
         *| a4    | a4 USD/GW | USD/a4  | Same bridge, different accounts   |
         *|       |           |         |                                   |
         *| B     | A USD/GW  | USD/B   |                                   |
         *| B     | A EUR/GW  | USD/B   | Fail: Same issuing asset          |
         *|       |           |         |                                   |
         *| A     | A USD/B   | USD/C   |                                   |
         *| A     | A USD/B   | EUR/B   | Fail: Same locking asset          |
         *| A     | A USD/C   | EUR/B   | Fail: Same locking asset currency |
         *|       |           |         |                                   |
         *| A     | A USD/GW  | USD/B   | Fail: Same bridge not allowed     |
         *| A     | B USD/GW  | USD/A   | Fail: "A" has USD already         |
         *| B     | A EUR/GW  | USD/B   | Fail:                             |
         *
         * Note that, now from sidechain's point of view, A is both
         * a local locking door and a foreign locking door on different
         * bridges. Txns such as commits specify bridge spec, but not the
         * local door account. So we test the transactors can figure out
         * the correct local door account from bridge spec.
         *
         * Commit to sidechain door accounts:
         *        | bridge spec | result
         * case 6 | A -> B      | B's balance increase
         * case 7 | C <- A      | A's balance increase
         *
         * We also test ModifyBridge txns modify correct bridges.
         */

        using namespace jtx;
        testcase("Bridge create constraints");
        XEnv env(*this, true);
        auto& A = scAlice;
        auto& B = scBob;
        auto& C = scCarol;
        auto AUSD = A["USD"];
        auto BUSD = B["USD"];
        auto CUSD = C["USD"];
        auto GUSD = scGw["USD"];
        auto AEUR = A["EUR"];
        auto BEUR = B["EUR"];
        auto CEUR = C["EUR"];
        auto GEUR = scGw["EUR"];

        // Accounts to own single bridges
        Account const a1("a1");
        Account const a2("a2");
        Account const a3("a3");
        Account const a4("a4");
        Account const a5("a5");
        Account const a6("a6");

        env.fund(XRP(10000), a1, a2, a3, a4, a5, a6);
        env.close();

        // Add a bridge on two different accounts with the same locking and
        // issuing assets
        env.tx(create_bridge(a1, bridge(a1, GUSD, B, BUSD))).close();
        env.tx(create_bridge(a2, bridge(a2, GUSD, B, BUSD))).close();

        // Add the exact same bridge to two different accounts (one locking
        // account and one issuing)
        env.tx(create_bridge(a3, bridge(a3, GUSD, a4, a4["USD"]))).close();
        env.tx(create_bridge(a4, bridge(a3, GUSD, a4, a4["USD"])), ter(tecDUPLICATE)).close();

        // Add the exact same bridge to two different accounts (one issuing
        // account and one locking - opposite order from the test above)
        env.tx(create_bridge(a5, bridge(a6, GUSD, a5, a5["USD"]))).close();
        env.tx(create_bridge(a6, bridge(a6, GUSD, a5, a5["USD"])), ter(tecDUPLICATE)).close();

        // Test case 1 ~ 5, create bridges
        auto const goodBridge1 = bridge(A, GUSD, B, BUSD);
        auto const goodBridge2 = bridge(A, BUSD, C, CUSD);
        env.tx(create_bridge(B, goodBridge1)).close();
        // Issuing asset is the same, this is a duplicate
        env.tx(create_bridge(B, bridge(A, GEUR, B, BUSD)), ter(tecDUPLICATE)).close();
        env.tx(create_bridge(A, goodBridge2), ter(tesSUCCESS)).close();
        // Locking asset is the same - this is a duplicate
        env.tx(create_bridge(A, bridge(A, BUSD, B, BEUR)), ter(tecDUPLICATE)).close();
        // Locking asset is USD - this is a duplicate even tho it has a
        // different issuer
        env.tx(create_bridge(A, bridge(A, CUSD, B, BEUR)), ter(tecDUPLICATE)).close();

        // Test case 6 and 7, commits
        env.tx(trust(C, BUSD(1000)))
            .tx(trust(A, BUSD(1000)))
            .close()
            .tx(pay(B, C, BUSD(1000)))
            .close();
        auto const aBalanceStart = env.balance(A, BUSD);
        auto const cBalanceStart = env.balance(C, BUSD);
        env.tx(xchain_commit(C, goodBridge1, 1, BUSD(50))).close();
        BEAST_EXPECT(env.balance(A, BUSD) - aBalanceStart == BUSD(0));
        BEAST_EXPECT(env.balance(C, BUSD) - cBalanceStart == BUSD(-50));
        env.tx(xchain_commit(C, goodBridge2, 1, BUSD(60))).close();
        BEAST_EXPECT(env.balance(A, BUSD) - aBalanceStart == BUSD(60));
        BEAST_EXPECT(env.balance(C, BUSD) - cBalanceStart == BUSD(-50 - 60));

        // bridge modify test cases
        env.tx(bridge_modify(B, goodBridge1, XRP(33), std::nullopt)).close();
        BEAST_EXPECT((*env.bridge(goodBridge1))[sfSignatureReward] == XRP(33));
        env.tx(bridge_modify(A, goodBridge2, XRP(44), std::nullopt)).close();
        BEAST_EXPECT((*env.bridge(goodBridge2))[sfSignatureReward] == XRP(44));
    }

    void
    testXChainCreateBridgeMatrix()
    {
        using namespace jtx;
        testcase("Create Bridge Matrix");

        // Test all combinations of the following:`
        // --------------------------------------
        // - Locking chain is IOU with locking chain door account as issuer
        // - Locking chain is IOU with issuing chain door account that
        //   exists on the locking chain as issuer
        // - Locking chain is IOU with issuing chain door account that does
        //   not exists on the locking chain as issuer
        // - Locking chain is IOU with non-door account (that exists on the
        //   locking chain ledger) as issuer
        // - Locking chain is IOU with non-door account (that does not exist
        //   exists on the locking chain ledger) as issuer
        // - Locking chain is XRP
        // ---------------------------------------------------------------------
        // - Issuing chain is IOU with issuing chain door account as the
        //   issuer
        // - Issuing chain is IOU with locking chain door account (that
        //   exists on the issuing chain ledger) as the issuer
        // - Issuing chain is IOU with locking chain door account (that does
        //   not exist on the issuing chain ledger) as the issuer
        // - Issuing chain is IOU with non-door account (that exists on the
        //   issuing chain ledger) as the issuer
        // - Issuing chain is IOU with non-door account (that does not
        //   exists on the issuing chain ledger) as the issuer
        // - Issuing chain is XRP and issuing chain door account is not the
        //   root account
        // - Issuing chain is XRP and issuing chain door account is the root
        //   account
        // ---------------------------------------------------------------------
        // That's 42 combinations. The only combinations that should succeed
        // are:
        // - Locking chain is any IOU,
        // - Issuing chain is IOU with issuing chain door account as the
        // issuer
        //   Locking chain is XRP,
        // - Issuing chain is XRP with issuing chain is the root account.
        // ---------------------------------------------------------------------
        Account a("a"), b("b");
        Issue ia, ib;

        std::tuple lcs{
            std::make_pair(
                "Locking chain is IOU(locking chain door)",
                [&](auto& env, bool) {
                    a = mcDoor;
                    ia = mcDoor["USD"];
                }),
            std::make_pair(
                "Locking chain is IOU(issuing chain door funded on locking "
                "chain)",
                [&](auto& env, bool shouldFund) {
                    a = mcDoor;
                    ia = scDoor["USD"];
                    if (shouldFund)
                        env.fund(XRP(10000), scDoor);
                }),
            std::make_pair(
                "Locking chain is IOU(issuing chain door account unfunded "
                "on locking chain)",
                [&](auto& env, bool) {
                    a = mcDoor;
                    ia = scDoor["USD"];
                }),
            std::make_pair(
                "Locking chain is IOU(bob funded on locking chain)",
                [&](auto& env, bool) {
                    a = mcDoor;
                    ia = mcGw["USD"];
                }),
            std::make_pair(
                "Locking chain is IOU(bob unfunded on locking chain)",
                [&](auto& env, bool) {
                    a = mcDoor;
                    ia = mcuGw["USD"];
                }),
            std::make_pair("Locking chain is XRP", [&](auto& env, bool) {
                a = mcDoor;
                ia = xrpIssue();
            })};

        std::tuple ics{
            std::make_pair(
                "Issuing chain is IOU(issuing chain door account)",
                [&](auto& env, bool) {
                    b = scDoor;
                    ib = scDoor["USD"];
                }),
            std::make_pair(
                "Issuing chain is IOU(locking chain door funded on issuing "
                "chain)",
                [&](auto& env, bool shouldFund) {
                    b = scDoor;
                    ib = mcDoor["USD"];
                    if (shouldFund)
                        env.fund(XRP(10000), mcDoor);
                }),
            std::make_pair(
                "Issuing chain is IOU(locking chain door unfunded on "
                "issuing chain)",
                [&](auto& env, bool) {
                    b = scDoor;
                    ib = mcDoor["USD"];
                }),
            std::make_pair(
                "Issuing chain is IOU(bob funded on issuing chain)",
                [&](auto& env, bool) {
                    b = scDoor;
                    ib = mcGw["USD"];
                }),
            std::make_pair(
                "Issuing chain is IOU(bob unfunded on issuing chain)",
                [&](auto& env, bool) {
                    b = scDoor;
                    ib = mcuGw["USD"];
                }),
            std::make_pair(
                "Issuing chain is XRP and issuing chain door account is "
                "not the root account",
                [&](auto& env, bool) {
                    b = scDoor;
                    ib = xrpIssue();
                }),
            std::make_pair(
                "Issuing chain is XRP and issuing chain door account is "
                "the root account ",
                [&](auto& env, bool) {
                    b = Account::master;
                    ib = xrpIssue();
                })};

        std::vector<std::pair<int, int>> expected_result{
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {tesSUCCESS, tesSUCCESS},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {tecNO_ISSUER, tesSUCCESS},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {tesSUCCESS, tesSUCCESS},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {tecNO_ISSUER, tesSUCCESS},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
            {tesSUCCESS, tesSUCCESS}};

        std::vector<std::tuple<TER, TER, bool>> test_result;

        auto testcase = [&](auto const& lc, auto const& ic) {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            lc.second(mcEnv, true);
            lc.second(scEnv, false);

            ic.second(mcEnv, false);
            ic.second(scEnv, true);

            auto const& expected = expected_result[test_result.size()];

            mcEnv.tx(create_bridge(a, bridge(a, ia, b, ib)), ter(TER::fromInt(expected.first)));
            TER mcTER = mcEnv.env_.ter();

            scEnv.tx(create_bridge(b, bridge(a, ia, b, ib)), ter(TER::fromInt(expected.second)));
            TER scTER = scEnv.env_.ter();

            bool pass = isTesSuccess(mcTER) && isTesSuccess(scTER);

            test_result.emplace_back(mcTER, scTER, pass);
        };

        auto apply_ics = [&](auto const& lc, auto const& ics) {
            std::apply([&](auto const&... ic) { (testcase(lc, ic), ...); }, ics);
        };

        std::apply([&](auto const&... lc) { (apply_ics(lc, ics), ...); }, lcs);

#if GENERATE_MTX_OUTPUT
        // optional output of matrix results in markdown format
        // ----------------------------------------------------
        std::string fname{std::tmpnam(nullptr)};
        fname += ".md";
        std::cout << "Markdown output for matrix test: " << fname << "\n";

        auto print_res = [](auto tup) -> std::string {
            std::string status =
                std::string(transToken(std::get<0>(tup))) + " / " + transToken(std::get<1>(tup));

            if (std::get<2>(tup))
                return status;
            else
            {
                // red
                return std::string("`") + status + "`";
            }
        };

        auto output_table = [&](auto print_res) {
            size_t test_idx = 0;
            std::string res;
            res.reserve(10000);  // should be enough :-)

            // first two header lines
            res += "|  `issuing ->` | ";
            std::apply([&](auto const&... ic) { ((res += ic.first, res += " | "), ...); }, ics);
            res += "\n";

            res += "| :--- | ";
            std::apply(
                [&](auto const&... ic) { (((void)ic.first, res += ":---: |  "), ...); }, ics);
            res += "\n";

            auto output = [&](auto const& lc, auto const& ic) {
                res += print_res(test_result[test_idx]);
                res += " | ";
                ++test_idx;
            };

            auto output_ics = [&](auto const& lc, auto const& ics) {
                res += "| ";
                res += lc.first;
                res += " | ";
                std::apply([&](auto const&... ic) { (output(lc, ic), ...); }, ics);
                res += "\n";
            };

            std::apply([&](auto const&... lc) { (output_ics(lc, ics), ...); }, lcs);

            return res;
        };

        std::ofstream(fname) << output_table(print_res);

        std::string ter_fname{std::tmpnam(nullptr)};
        std::cout << "ter output for matrix test: " << ter_fname << "\n";

        std::ofstream ofs(ter_fname);
        for (auto& t : test_result)
        {
            ofs << "{ " << std::string(transToken(std::get<0>(t))) << ", "
                << std::string(transToken(std::get<1>(t))) << "}\n,";
        }
#endif
    }

    void
    testXChainModifyBridge()
    {
        using namespace jtx;
        testcase("Modify Bridge");

        // Changing a non-existent bridge should fail
        XEnv(*this).tx(
            bridge_modify(
                mcAlice, bridge(mcAlice, mcGw["USD"], mcBob, mcBob["USD"]), XRP(2), std::nullopt),
            ter(tecNO_ENTRY));

        // must change something
        // XEnv(*this)
        //    .tx(create_bridge(mcDoor, jvb, XRP(1), XRP(1)))
        //    .tx(bridge_modify(mcDoor, jvb, XRP(1), XRP(1)),
        //    ter(temMALFORMED));

        // must change something
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb, XRP(1), XRP(1)))
            .close()
            .tx(bridge_modify(mcDoor, jvb, {}, {}), ter(temMALFORMED));

        // Reward amount is non-xrp
        XEnv(*this).tx(
            bridge_modify(mcDoor, jvb, mcUSD(2), XRP(10)), ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT));

        // Reward amount is XRP and negative
        XEnv(*this).tx(
            bridge_modify(mcDoor, jvb, XRP(-2), XRP(10)), ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT));

        // Min create amount is non-xrp
        XEnv(*this).tx(
            bridge_modify(mcDoor, jvb, XRP(2), mcUSD(10)),
            ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT));

        // Min create amount is zero
        XEnv(*this).tx(
            bridge_modify(mcDoor, jvb, XRP(2), XRP(0)),
            ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT));

        // Min create amount is negative
        XEnv(*this).tx(
            bridge_modify(mcDoor, jvb, XRP(2), XRP(-10)),
            ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT));

        // First check the regular claim process (without bridge_modify)
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM,
                withClaim);

            scEnv
                .multiTx(claim_attestations(
                    scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers))
                .close();

            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)).close();
            }

            BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));
        }

        // Check that the reward paid from a claim Id was the reward when
        // the claim id was created, not the reward since the bridge was
        // modified.
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            // Now modify the reward on the bridge
            mcEnv.tx(bridge_modify(mcDoor, jvb, XRP(2), XRP(10))).close();
            scEnv.tx(bridge_modify(Account::master, jvb, XRP(2), XRP(10))).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM,
                withClaim);

            scEnv
                .multiTx(claim_attestations(
                    scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers))
                .close();

            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)).close();
            }

            // make sure the reward accounts indeed received the original
            // split reward (1 split 5 ways) instead of the updated 2 XRP.
            BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));
        }

        // Check that the signatures used to verify attestations and decide
        // if there is a quorum are the current signer's list on the door
        // account, not the signer's list that was in effect when the claim
        // id was created.
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            // change signers - claim should not be processed is the batch
            // is signed by original signers
            scEnv.tx(jtx::signers(Account::master, quorum, alt_signers)).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM,
                withClaim);

            // submit claim using outdated signers - should fail
            scEnv
                .multiTx(
                    claim_attestations(
                        scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers),
                    ter(tecNO_PERMISSION))
                .close();
            if (withClaim)
            {
                // need to submit a claim transactions
                scEnv
                    .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
                        ter(tecXCHAIN_CLAIM_NO_QUORUM))
                    .close();
            }

            // make sure transfer has not happened as we sent attestations
            // using outdated signers
            BEAST_EXPECT(transfer.has_not_happened());

            // submit claim using current signers - should succeed
            scEnv
                .multiTx(claim_attestations(
                    scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, alt_signers))
                .close();
            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)).close();
            }

            // make sure the transfer went through as we sent attestations
            // using new signers
            BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum, false));
        }

        // coverage test: bridge_modify transaction with incorrect flag
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb))
            .close()
            .tx(bridge_modify(mcDoor, jvb, XRP(1), XRP(2)),
                txflags(tfFillOrKill),
                ter(temINVALID_FLAG));

        // coverage test: bridge_modify transaction with xchain feature
        // disabled
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb))
            .disableFeature(featureXChainBridge)
            .close()
            .tx(bridge_modify(mcDoor, jvb, XRP(1), XRP(2)), ter(temDISABLED));

        // coverage test: bridge_modify return temSIDECHAIN_NONDOOR_OWNER;
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb))
            .close()
            .tx(bridge_modify(mcAlice, jvb, XRP(1), XRP(2)), ter(temXCHAIN_BRIDGE_NONDOOR_OWNER));

        /**
         * test tfClearAccountCreateAmount flag in BridgeModify tx
         * -- tx has both minAccountCreateAmount and the flag, temMALFORMED
         * -- tx has the flag and also modifies signature reward, tesSUCCESS
         * -- XChainCreateAccountCommit tx fail after previous step
         */
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20)))
            .close()
            .tx(sidechain_xchain_account_create(mcAlice, jvb, scuAlice, XRP(100), reward))
            .close()
            .tx(bridge_modify(mcDoor, jvb, {}, XRP(2)),
                txflags(tfClearAccountCreateAmount),
                ter(temMALFORMED))
            .close()
            .tx(bridge_modify(mcDoor, jvb, XRP(3), {}), txflags(tfClearAccountCreateAmount))
            .close()
            .tx(sidechain_xchain_account_create(mcAlice, jvb, scuBob, XRP(100), XRP(3)),
                ter(tecXCHAIN_CREATE_ACCOUNT_DISABLED))
            .close();
    }

    void
    testXChainCreateClaimID()
    {
        using namespace jtx;
        XRPAmount res1 = reserve(1);
        XRPAmount tx_fee = txFee();

        testcase("Create ClaimID");

        // normal bridge create for sanity check with the exact necessary
        // account balance
        XEnv(*this, true)
            .tx(create_bridge(Account::master, jvb))
            .fund(res1, scuAlice)  // acct reserve + 1 object
            .close()
            .tx(xchain_create_claim_id(scuAlice, jvb, reward, mcAlice))
            .close();

        // check reward not deducted when claim id is created
        {
            XEnv xenv(*this, true);

            Balance scAlice_bal(xenv, scAlice);

            xenv.tx(create_bridge(Account::master, jvb))
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            BEAST_EXPECT(scAlice_bal.diff() == -tx_fee);
        }

        // Non-existent bridge
        XEnv(*this, true)
            .tx(xchain_create_claim_id(
                    scAlice, bridge(mcAlice, mcAlice["USD"], scBob, scBob["USD"]), reward, mcAlice),
                ter(tecNO_ENTRY))
            .close();

        // Creating the new object would put the account below the reserve
        XEnv(*this, true)
            .tx(create_bridge(Account::master, jvb))
            .fund(res1 - xrp_dust, scuAlice)  // barely not enough
            .close()
            .tx(xchain_create_claim_id(scuAlice, jvb, reward, mcAlice),
                ter(tecINSUFFICIENT_RESERVE))
            .close();

        // The specified reward doesn't match the reward on the bridge (test
        // by giving the reward amount for the other side, as well as a
        // completely non-matching reward)
        XEnv(*this, true)
            .tx(create_bridge(Account::master, jvb))
            .close()
            .tx(xchain_create_claim_id(scAlice, jvb, split_reward_quorum, mcAlice),
                ter(tecXCHAIN_REWARD_MISMATCH))
            .close();

        // A reward amount that isn't XRP
        XEnv(*this, true)
            .tx(create_bridge(Account::master, jvb))
            .close()
            .tx(xchain_create_claim_id(scAlice, jvb, mcUSD(1), mcAlice),
                ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT))
            .close();

        // coverage test: xchain_create_claim_id transaction with incorrect
        // flag
        XEnv(*this, true)
            .tx(create_bridge(Account::master, jvb))
            .close()
            .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice),
                txflags(tfFillOrKill),
                ter(temINVALID_FLAG))
            .close();

        // coverage test: xchain_create_claim_id transaction with xchain
        // feature disabled
        XEnv(*this, true)
            .tx(create_bridge(Account::master, jvb))
            .disableFeature(featureXChainBridge)
            .close()
            .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice), ter(temDISABLED))
            .close();
    }

    void
    testXChainCommit()
    {
        using namespace jtx;
        XRPAmount res0 = reserve(0);
        XRPAmount tx_fee = txFee();

        testcase("Commit");

        // Commit to a non-existent bridge
        XEnv(*this).tx(xchain_commit(mcAlice, jvb, 1, one_xrp, scBob), ter(tecNO_ENTRY));

        // check that reward not deducted when doing the commit
        {
            XEnv xenv(*this);

            Balance alice_bal(xenv, mcAlice);
            auto const amt = XRP(1000);

            xenv.tx(create_bridge(mcDoor, jvb))
                .close()
                .tx(xchain_commit(mcAlice, jvb, 1, amt, scBob))
                .close();

            STAmount claim_cost = amt;
            BEAST_EXPECT(alice_bal.diff() == -(claim_cost + tx_fee));
        }

        // Commit a negative amount
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb))
            .close()
            .tx(xchain_commit(mcAlice, jvb, 1, XRP(-1), scBob), ter(temBAD_AMOUNT));

        // Commit an amount whose issue that does not match the expected
        // issue on the bridge (either LockingChainIssue or
        // IssuingChainIssue, depending on the chain).
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb))
            .close()
            .tx(xchain_commit(mcAlice, jvb, 1, mcUSD(100), scBob), ter(temBAD_ISSUER));

        // Commit an amount that would put the sender below the required
        // reserve (if XRP)
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb))
            .fund(res0 + one_xrp - xrp_dust, mcuAlice)  // barely not enough
            .close()
            .tx(xchain_commit(mcuAlice, jvb, 1, one_xrp, scBob), ter(tecUNFUNDED_PAYMENT));

        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb))
            .fund(
                res0 + one_xrp + xrp_dust,  // "xrp_dust" for tx fees
                mcuAlice)                   // exactly enough => should succeed
            .close()
            .tx(xchain_commit(mcuAlice, jvb, 1, one_xrp, scBob));

        // Commit an amount above the account's balance (for both XRP and
        // IOUs)
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb))
            .fund(res0, mcuAlice)  // barely not enough
            .close()
            .tx(xchain_commit(mcuAlice, jvb, 1, res0 + one_xrp, scBob), ter(tecUNFUNDED_PAYMENT));

        auto jvb_USD = bridge(mcDoor, mcUSD, scGw, scUSD);

        // commit sent from iou issuer (mcGw) succeeds - should it?
        XEnv(*this)
            .tx(trust(mcDoor, mcUSD(10000)))  // door needs to have a trustline
            .tx(create_bridge(mcDoor, jvb_USD))
            .close()
            .tx(xchain_commit(mcGw, jvb_USD, 1, mcUSD(1), scBob));

        // commit to a door account from the door account. This should fail.
        XEnv(*this)
            .tx(trust(mcDoor, mcUSD(10000)))  // door needs to have a trustline
            .tx(create_bridge(mcDoor, jvb_USD))
            .close()
            .tx(xchain_commit(mcDoor, jvb_USD, 1, mcUSD(1), scBob), ter(tecXCHAIN_SELF_COMMIT));

        // commit sent from mcAlice which has no IOU balance => should fail
        XEnv(*this)
            .tx(trust(mcDoor, mcUSD(10000)))  // door needs to have a trustline
            .tx(create_bridge(mcDoor, jvb_USD))
            .close()
            .tx(xchain_commit(mcAlice, jvb_USD, 1, mcUSD(1), scBob), ter(terNO_LINE));

        // commit sent from mcAlice which has no IOU balance => should fail
        // just changed the destination to scGw (which is the door account
        // and may not make much sense)
        XEnv(*this)
            .tx(trust(mcDoor, mcUSD(10000)))  // door needs to have a trustline
            .tx(create_bridge(mcDoor, jvb_USD))
            .close()
            .tx(xchain_commit(mcAlice, jvb_USD, 1, mcUSD(1), scGw), ter(terNO_LINE));

        // commit sent from mcAlice which has a IOU balance => should
        // succeed
        XEnv(*this)
            .tx(trust(mcDoor, mcUSD(10000)))
            .tx(trust(mcAlice, mcUSD(10000)))
            .close()
            .tx(pay(mcGw, mcAlice, mcUSD(10)))
            .tx(create_bridge(mcDoor, jvb_USD))
            .close()
            //.tx(pay(mcAlice, mcDoor, mcUSD(10)));
            .tx(xchain_commit(mcAlice, jvb_USD, 1, mcUSD(10), scAlice));

        // coverage test: xchain_commit transaction with incorrect flag
        XEnv(*this)
            .tx(create_bridge(mcDoor))
            .close()
            .tx(xchain_commit(mcAlice, jvb, 1, one_xrp, scBob),
                txflags(tfFillOrKill),
                ter(temINVALID_FLAG));

        // coverage test: xchain_commit transaction with xchain feature
        // disabled
        XEnv(*this)
            .tx(create_bridge(mcDoor))
            .disableFeature(featureXChainBridge)
            .close()
            .tx(xchain_commit(mcAlice, jvb, 1, one_xrp, scBob), ter(temDISABLED));
    }

    void
    testXChainAddAttestation()
    {
        using namespace jtx;

        testcase("Add Attestation");
        XRPAmount res0 = reserve(0);
        XRPAmount tx_fee = txFee();

        auto multiTtxFee = [&](std::uint32_t m) -> STAmount {
            return multiply(tx_fee, STAmount(m), xrpIssue());
        };

        // Add an attestation to a claim id that has already reached quorum.
        // This should succeed and share in the reward.
        // note: this is true only when either:
        //       1. dest account is not specified, so transfer requires a claim
        //       2. or the extra attestation is sent in the same batch as the
        //          one reaching quorum
        for (auto withClaim : {true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);
            std::uint32_t const claimID = 1;

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            BEAST_EXPECT(!!scEnv.claimID(jvb, claimID));  // claim id present

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(scEnv, Account::master, scBob, scAlice, payees, withClaim);

            scEnv
                .multiTx(claim_attestations(
                    scAttester,
                    jvb,
                    mcAlice,
                    amt,
                    payees,
                    true,
                    claimID,
                    dst,
                    signers,
                    UT_XCHAIN_DEFAULT_QUORUM))
                .close();
            scEnv
                .tx(claim_attestation(
                    scAttester,
                    jvb,
                    mcAlice,
                    amt,
                    payees[UT_XCHAIN_DEFAULT_QUORUM],
                    true,
                    claimID,
                    dst,
                    signers[UT_XCHAIN_DEFAULT_QUORUM]))
                .close();

            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)).close();
                BEAST_EXPECT(!scEnv.claimID(jvb, claimID));  // claim id deleted
                BEAST_EXPECT(scEnv.claimID(jvb) == claimID);
            }

            BEAST_EXPECT(transfer.has_happened(amt, split_reward_everyone));
        }

        // Test that signature weights are correctly handled. Assign
        // signature weights of 1,2,4,4 and a quorum of 7. Check that the
        // 4,4 signatures reach a quorum, the 1,2,4, reach a quorum, but the
        // 4,2, 4,1 and 1,2 do not.

        // 1,2,4 => should succeed
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            std::uint32_t const quorum_7 = 7;
            std::vector<signer> const signers_ = [] {
                constexpr int numSigners = 4;
                std::uint32_t weights[] = {1, 2, 4, 4};

                std::vector<signer> result;
                result.reserve(numSigners);
                for (int i = 0; i < numSigners; ++i)
                {
                    using namespace std::literals;
                    auto const a = Account("signer_"s + std::to_string(i));
                    result.emplace_back(a, weights[i]);
                }
                return result;
            }();

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum_7, signers_))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();
            std::uint32_t const claimID = 1;
            BEAST_EXPECT(!!scEnv.claimID(jvb, claimID));  // claim id present

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);

            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv, Account::master, scBob, scAlice, &payees[0], 3, withClaim);

            scEnv
                .multiTx(claim_attestations(
                    scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers_, 3))
                .close();

            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)).close();
            }

            BEAST_EXPECT(!scEnv.claimID(jvb, 1));  // claim id deleted

            BEAST_EXPECT(transfer.has_happened(amt, divide(reward, STAmount(3), reward.issue())));
        }

        // 4,4 => should succeed
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            std::uint32_t const quorum_7 = 7;
            std::vector<signer> const signers_ = [] {
                constexpr int numSigners = 4;
                std::uint32_t weights[] = {1, 2, 4, 4};

                std::vector<signer> result;
                result.reserve(numSigners);
                for (int i = 0; i < numSigners; ++i)
                {
                    using namespace std::literals;
                    auto const a = Account("signer_"s + std::to_string(i));
                    result.emplace_back(a, weights[i]);
                }
                return result;
            }();
            STAmount const split_reward_ =
                divide(reward, STAmount(signers_.size()), reward.issue());

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum_7, signers_))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();
            std::uint32_t const claimID = 1;
            BEAST_EXPECT(!!scEnv.claimID(jvb, claimID));  // claim id present

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);

            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv, Account::master, scBob, scAlice, &payees[2], 2, withClaim);

            scEnv
                .multiTx(claim_attestations(
                    scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers_, 2, 2))
                .close();

            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)).close();
            }

            BEAST_EXPECT(!scEnv.claimID(jvb, claimID));  // claim id deleted

            BEAST_EXPECT(transfer.has_happened(amt, divide(reward, STAmount(2), reward.issue())));
        }

        // 1,2 => should fail
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            std::uint32_t const quorum_7 = 7;
            std::vector<signer> const signers_ = [] {
                constexpr int numSigners = 4;
                std::uint32_t weights[] = {1, 2, 4, 4};

                std::vector<signer> result;
                result.reserve(numSigners);
                for (int i = 0; i < numSigners; ++i)
                {
                    using namespace std::literals;
                    auto const a = Account("signer_"s + std::to_string(i));
                    result.emplace_back(a, weights[i]);
                }
                return result;
            }();

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum_7, signers_))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            std::uint32_t const claimID = 1;
            BEAST_EXPECT(!!scEnv.claimID(jvb, claimID));  // claim id present

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv, Account::master, scBob, scAlice, &payees[0], 2, withClaim);

            scEnv
                .multiTx(claim_attestations(
                    scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers_, 2))
                .close();
            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv
                    .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
                        ter(tecXCHAIN_CLAIM_NO_QUORUM))
                    .close();
            }

            BEAST_EXPECT(!!scEnv.claimID(jvb, claimID));  // claim id still present
            BEAST_EXPECT(transfer.has_not_happened());
        }

        // 2,4 => should fail
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            std::uint32_t const quorum_7 = 7;
            std::vector<signer> const signers_ = [] {
                constexpr int numSigners = 4;
                std::uint32_t weights[] = {1, 2, 4, 4};

                std::vector<signer> result;
                result.reserve(numSigners);
                for (int i = 0; i < numSigners; ++i)
                {
                    using namespace std::literals;
                    auto const a = Account("signer_"s + std::to_string(i));
                    result.emplace_back(a, weights[i]);
                }
                return result;
            }();

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum_7, signers_))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            std::uint32_t const claimID = 1;
            BEAST_EXPECT(!!scEnv.claimID(jvb, claimID));  // claim id present

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);

            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv, Account::master, scBob, scAlice, &payees[1], 2, withClaim);

            scEnv
                .multiTx(claim_attestations(
                    scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers_, 2, 1))
                .close();

            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv
                    .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
                        ter(tecXCHAIN_CLAIM_NO_QUORUM))
                    .close();
            }

            BEAST_EXPECT(!!scEnv.claimID(jvb, claimID));  // claim id still present
            BEAST_EXPECT(transfer.has_not_happened());
        }

        // Confirm that account create transactions happen in the correct
        // order. If they reach quorum out of order they should not execute
        // until all the previous created transactions have occurred.
        // Re-adding an attestation should move funds.
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);
            auto const amt = XRP(1000);
            auto const amt_plus_reward = amt + reward;

            {
                Balance door(mcEnv, mcDoor);
                Balance carol(mcEnv, mcCarol);

                mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20)))
                    .close()
                    .tx(sidechain_xchain_account_create(mcAlice, jvb, scuAlice, amt, reward))
                    .tx(sidechain_xchain_account_create(mcBob, jvb, scuBob, amt, reward))
                    .tx(sidechain_xchain_account_create(mcCarol, jvb, scuCarol, amt, reward))
                    .close();

                BEAST_EXPECT(
                    door.diff() == (multiply(amt_plus_reward, STAmount(3), xrpIssue()) - tx_fee));
                BEAST_EXPECT(carol.diff() == -(amt + reward + tx_fee));
            }

            scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20)))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close();

            {
                // send first batch of account create attest for all 3
                // account create
                Balance attester(scEnv, scAttester);
                Balance door(scEnv, Account::master);

                scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 2))
                    .multiTx(att_create_acct_vec(3, amt, scuCarol, 2))
                    .multiTx(att_create_acct_vec(2, amt, scuBob, 2))
                    .close();

                BEAST_EXPECT(door.diff() == STAmount(0));
                // att_create_acct_vec return vectors of size 2, so 2*3 txns
                BEAST_EXPECT(attester.diff() == -multiTtxFee(6));

                BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1));   // ca claim id present
                BEAST_EXPECT(!!scEnv.caClaimID(jvb, 2));   // ca claim id present
                BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3));   // ca claim id present
                BEAST_EXPECT(scEnv.claimCount(jvb) == 0);  // claim count still 0
            }

            {
                // complete attestations for 2nd account create => should
                // not complete
                Balance attester(scEnv, scAttester);
                Balance door(scEnv, Account::master);

                scEnv.multiTx(att_create_acct_vec(2, amt, scuBob, 3, 2)).close();

                BEAST_EXPECT(door.diff() == STAmount(0));
                // att_create_acct_vec return vectors of size 3, so 3 txns
                BEAST_EXPECT(attester.diff() == -multiTtxFee(3));

                BEAST_EXPECT(!!scEnv.caClaimID(jvb, 2));   // ca claim id present
                BEAST_EXPECT(scEnv.claimCount(jvb) == 0);  // claim count still 0
            }

            {
                // complete attestations for 3rd account create => should
                // not complete
                Balance attester(scEnv, scAttester);
                Balance door(scEnv, Account::master);

                scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 3, 2)).close();

                BEAST_EXPECT(door.diff() == STAmount(0));
                // att_create_acct_vec return vectors of size 3, so 3 txns
                BEAST_EXPECT(attester.diff() == -multiTtxFee(3));

                BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3));   // ca claim id present
                BEAST_EXPECT(scEnv.claimCount(jvb) == 0);  // claim count still 0
            }

            {
                // complete attestations for 1st account create => account
                // should be created
                Balance attester(scEnv, scAttester);
                Balance door(scEnv, Account::master);

                scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 3, 1)).close();

                BEAST_EXPECT(door.diff() == -amt_plus_reward);
                // att_create_acct_vec return vectors of size 3, so 3 txns
                BEAST_EXPECT(attester.diff() == -multiTtxFee(3));
                BEAST_EXPECT(scEnv.balance(scuAlice) == amt);

                BEAST_EXPECT(!scEnv.caClaimID(jvb, 1));    // claim id 1 deleted
                BEAST_EXPECT(!!scEnv.caClaimID(jvb, 2));   // claim id 2 present
                BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3));   // claim id 3 present
                BEAST_EXPECT(scEnv.claimCount(jvb) == 1);  // claim count now 1
            }

            {
                // resend attestations for 3rd account create => still
                // should not complete
                Balance attester(scEnv, scAttester);
                Balance door(scEnv, Account::master);

                scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 3, 2)).close();

                BEAST_EXPECT(door.diff() == STAmount(0));
                // att_create_acct_vec return vectors of size 3, so 3 txns
                BEAST_EXPECT(attester.diff() == -multiTtxFee(3));

                BEAST_EXPECT(!!scEnv.caClaimID(jvb, 2));   // claim id 2 present
                BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3));   // claim id 3 present
                BEAST_EXPECT(scEnv.claimCount(jvb) == 1);  // claim count still 1
            }

            {
                // resend attestations for 2nd account create => account
                // should be created
                Balance attester(scEnv, scAttester);
                Balance door(scEnv, Account::master);

                scEnv.multiTx(att_create_acct_vec(2, amt, scuBob, 1)).close();

                BEAST_EXPECT(door.diff() == -amt_plus_reward);
                BEAST_EXPECT(attester.diff() == -tx_fee);
                BEAST_EXPECT(scEnv.balance(scuBob) == amt);

                BEAST_EXPECT(!scEnv.caClaimID(jvb, 2));    // claim id 2 deleted
                BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3));   // claim id 3 present
                BEAST_EXPECT(scEnv.claimCount(jvb) == 2);  // claim count now 2
            }
            {
                // resend attestations for 3rc account create => account
                // should be created
                Balance attester(scEnv, scAttester);
                Balance door(scEnv, Account::master);

                scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 1)).close();

                BEAST_EXPECT(door.diff() == -amt_plus_reward);
                BEAST_EXPECT(attester.diff() == -tx_fee);
                BEAST_EXPECT(scEnv.balance(scuCarol) == amt);

                BEAST_EXPECT(!scEnv.caClaimID(jvb, 3));    // claim id 3 deleted
                BEAST_EXPECT(scEnv.claimCount(jvb) == 3);  // claim count now 3
            }
        }

        // Check that creating an account with less than the minimum reserve
        // fails.
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            auto const amt = res0 - XRP(1);
            auto const amt_plus_reward = amt + reward;

            mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20))).close();

            {
                Balance door(mcEnv, mcDoor);
                Balance carol(mcEnv, mcCarol);

                mcEnv.tx(sidechain_xchain_account_create(mcCarol, jvb, scuAlice, amt, reward))
                    .close();

                BEAST_EXPECT(door.diff() == amt_plus_reward);
                BEAST_EXPECT(carol.diff() == -(amt_plus_reward + tx_fee));
            }

            scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20)))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close();

            Balance attester(scEnv, scAttester);
            Balance door(scEnv, Account::master);

            scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 2)).close();
            BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1));   // claim id present
            BEAST_EXPECT(scEnv.claimCount(jvb) == 0);  // claim count is one less

            scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 2, 2)).close();
            BEAST_EXPECT(!scEnv.caClaimID(jvb, 1));    // claim id deleted
            BEAST_EXPECT(scEnv.claimCount(jvb) == 1);  // claim count was incremented

            BEAST_EXPECT(attester.diff() == -multiTtxFee(4));
            BEAST_EXPECT(door.diff() == -reward);
            BEAST_EXPECT(!scEnv.account(scuAlice));
        }

        // Check that sending funds with an account create txn to an
        // existing account works.
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            auto const amt = XRP(111);
            auto const amt_plus_reward = amt + reward;

            mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20))).close();

            {
                Balance door(mcEnv, mcDoor);
                Balance carol(mcEnv, mcCarol);

                mcEnv.tx(sidechain_xchain_account_create(mcCarol, jvb, scAlice, amt, reward))
                    .close();

                BEAST_EXPECT(door.diff() == amt_plus_reward);
                BEAST_EXPECT(carol.diff() == -(amt_plus_reward + tx_fee));
            }

            scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20)))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close();

            Balance attester(scEnv, scAttester);
            Balance door(scEnv, Account::master);
            Balance alice(scEnv, scAlice);

            scEnv.multiTx(att_create_acct_vec(1, amt, scAlice, 2)).close();
            BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1));   // claim id present
            BEAST_EXPECT(scEnv.claimCount(jvb) == 0);  // claim count is one less

            scEnv.multiTx(att_create_acct_vec(1, amt, scAlice, 2, 2)).close();
            BEAST_EXPECT(!scEnv.caClaimID(jvb, 1));    // claim id deleted
            BEAST_EXPECT(scEnv.claimCount(jvb) == 1);  // claim count was incremented

            BEAST_EXPECT(door.diff() == -amt_plus_reward);
            BEAST_EXPECT(attester.diff() == -multiTtxFee(4));
            BEAST_EXPECT(alice.diff() == amt);
        }

        // Check that sending funds to an existing account with deposit auth
        // set fails for account create transactions.
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            auto const amt = XRP(1000);
            auto const amt_plus_reward = amt + reward;

            mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20))).close();

            {
                Balance door(mcEnv, mcDoor);
                Balance carol(mcEnv, mcCarol);

                mcEnv.tx(sidechain_xchain_account_create(mcCarol, jvb, scAlice, amt, reward))
                    .close();

                BEAST_EXPECT(door.diff() == amt_plus_reward);
                BEAST_EXPECT(carol.diff() == -(amt_plus_reward + tx_fee));
            }

            scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20)))
                .tx(jtx::signers(Account::master, quorum, signers))
                .tx(fset("scAlice", asfDepositAuth))  // set deposit auth
                .close();

            Balance attester(scEnv, scAttester);
            Balance door(scEnv, Account::master);
            Balance alice(scEnv, scAlice);

            scEnv.multiTx(att_create_acct_vec(1, amt, scAlice, 2)).close();
            BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1));   // claim id present
            BEAST_EXPECT(scEnv.claimCount(jvb) == 0);  // claim count is one less

            scEnv.multiTx(att_create_acct_vec(1, amt, scAlice, 2, 2)).close();
            BEAST_EXPECT(!scEnv.caClaimID(jvb, 1));    // claim id deleted
            BEAST_EXPECT(scEnv.claimCount(jvb) == 1);  // claim count was incremented

            BEAST_EXPECT(door.diff() == -reward);
            BEAST_EXPECT(attester.diff() == -multiTtxFee(4));
            BEAST_EXPECT(alice.diff() == STAmount(0));
        }

        // If an account is unable to pay the reserve, check that it fails.
        // [greg todo] I don't know what this should test??

        // If an attestation already exists for that server and claim id,
        // the new attestation should replace the old attestation
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);
            auto const amt = XRP(1000);
            auto const amt_plus_reward = amt + reward;

            {
                Balance door(mcEnv, mcDoor);
                Balance carol(mcEnv, mcCarol);

                mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20)))
                    .close()
                    .tx(sidechain_xchain_account_create(mcAlice, jvb, scuAlice, amt, reward))
                    .close()  // make sure Alice gets claim #1
                    .tx(sidechain_xchain_account_create(mcBob, jvb, scuBob, amt, reward))
                    .close()  // make sure Bob gets claim #2
                    .tx(sidechain_xchain_account_create(mcCarol, jvb, scuCarol, amt, reward))
                    .close();  // and Carol will get claim #3

                BEAST_EXPECT(
                    door.diff() == (multiply(amt_plus_reward, STAmount(3), xrpIssue()) - tx_fee));
                BEAST_EXPECT(carol.diff() == -(amt + reward + tx_fee));
            }

            std::uint32_t const red_quorum = 2;
            scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20)))
                .tx(jtx::signers(Account::master, red_quorum, signers))
                .close();

            {
                Balance attester(scEnv, scAttester);
                Balance door(scEnv, Account::master);
                auto const bad_amt = XRP(10);
                std::uint32_t txCount = 0;

                // send attestations with incorrect amounts to for all 3
                // AccountCreate. They will be replaced later
                scEnv.multiTx(att_create_acct_vec(1, bad_amt, scuAlice, 1))
                    .multiTx(att_create_acct_vec(2, bad_amt, scuBob, 1, 2))
                    .multiTx(att_create_acct_vec(3, bad_amt, scuCarol, 1, 1))
                    .close();
                txCount += 3;

                BEAST_EXPECTS(!!scEnv.caClaimID(jvb, 1), "claim id 1 created");
                BEAST_EXPECTS(!!scEnv.caClaimID(jvb, 2), "claim id 2 created");
                BEAST_EXPECTS(!!scEnv.caClaimID(jvb, 3), "claim id 3 created");

                // note: if we send inconsistent attestations in the same
                // batch, the transaction errors.

                // from now on we send correct attestations
                scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 1, 0))
                    .multiTx(att_create_acct_vec(2, amt, scuBob, 1, 2))
                    .multiTx(att_create_acct_vec(3, amt, scuCarol, 1, 4))
                    .close();
                txCount += 3;

                BEAST_EXPECTS(!!scEnv.caClaimID(jvb, 1), "claim id 1 still there");
                BEAST_EXPECTS(!!scEnv.caClaimID(jvb, 2), "claim id 2 still there");
                BEAST_EXPECTS(!!scEnv.caClaimID(jvb, 3), "claim id 3 still there");
                BEAST_EXPECTS(scEnv.claimCount(jvb) == 0, "No account created yet");

                scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 1, 1)).close();
                txCount += 1;

                BEAST_EXPECTS(!!scEnv.caClaimID(jvb, 3), "claim id 3 still there");
                BEAST_EXPECTS(scEnv.claimCount(jvb) == 0, "No account created yet");

                scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 1, 2)).close();
                txCount += 1;

                BEAST_EXPECTS(!scEnv.caClaimID(jvb, 1), "claim id 1 deleted");
                BEAST_EXPECTS(scEnv.claimCount(jvb) == 1, "scuAlice created");

                scEnv.multiTx(att_create_acct_vec(2, amt, scuBob, 1, 3))
                    .multiTx(
                        att_create_acct_vec(1, amt, scuAlice, 1, 3),
                        ter(tecXCHAIN_ACCOUNT_CREATE_PAST))
                    .close();
                txCount += 2;

                BEAST_EXPECTS(!scEnv.caClaimID(jvb, 2), "claim id 2 deleted");
                BEAST_EXPECTS(!scEnv.caClaimID(jvb, 1), "claim id 1 not added");
                BEAST_EXPECTS(scEnv.claimCount(jvb) == 2, "scuAlice & scuBob created");

                scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 1, 0)).close();
                txCount += 1;

                BEAST_EXPECTS(!scEnv.caClaimID(jvb, 3), "claim id 3 deleted");
                BEAST_EXPECTS(scEnv.claimCount(jvb) == 3, "All 3 accounts created");

                // because of the division of the rewards among attesters,
                // sometimes a couple drops are left over unspent in the
                // door account (here 2 drops)
                BEAST_EXPECT(
                    multiply(amt_plus_reward, STAmount(3), xrpIssue()) + door.diff() < drops(3));
                BEAST_EXPECT(attester.diff() == -multiTtxFee(txCount));
                BEAST_EXPECT(scEnv.balance(scuAlice) == amt);
                BEAST_EXPECT(scEnv.balance(scuBob) == amt);
                BEAST_EXPECT(scEnv.balance(scuCarol) == amt);
            }
        }

        // If attestation moves funds, confirm the claim ledger objects are
        // removed (for both account create and "regular" transactions)
        // [greg] we do this in all attestation tests

        // coverage test: add_attestation transaction with incorrect flag
        {
            XEnv scEnv(*this, true);
            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(claim_attestation(
                        scAttester, jvb, mcAlice, XRP(1000), payees[0], true, 1, {}, signers[0]),
                    txflags(tfFillOrKill),
                    ter(temINVALID_FLAG))
                .close();
        }

        // coverage test: add_attestation with xchain feature
        // disabled
        {
            XEnv scEnv(*this, true);
            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .disableFeature(featureXChainBridge)
                .close()
                .tx(claim_attestation(
                        scAttester, jvb, mcAlice, XRP(1000), payees[0], true, 1, {}, signers[0]),
                    ter(temDISABLED))
                .close();
        }
    }

    void
    testXChainAddClaimNonBatchAttestation()
    {
        using namespace jtx;

        testcase("Add Non Batch Claim Attestation");

        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);
            std::uint32_t const claimID = 1;

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            BEAST_EXPECT(!!scEnv.claimID(jvb, claimID));  // claim id present

            Account const dst{scBob};
            auto const amt = XRP(1000);
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            auto const dstStartBalance = scEnv.env_.balance(dst);

            for (int i = 0; i < signers.size(); ++i)
            {
                auto const att = claim_attestation(
                    scAttester, jvb, mcAlice, amt, payees[i], true, claimID, dst, signers[i]);

                TER const expectedTER = i < quorum ? tesSUCCESS : TER{tecXCHAIN_NO_CLAIM_ID};
                if (i + 1 == quorum)
                {
                    scEnv.tx(att, ter(expectedTER)).close();
                }
                else
                {
                    scEnv.tx(att, ter(expectedTER)).close();
                }

                if (i + 1 < quorum)
                {
                    BEAST_EXPECT(dstStartBalance == scEnv.env_.balance(dst));
                }
                else
                {
                    BEAST_EXPECT(dstStartBalance + amt == scEnv.env_.balance(dst));
                }
            }
            BEAST_EXPECT(dstStartBalance + amt == scEnv.env_.balance(dst));
        }

        {
            /**
             * sfAttestationSignerAccount related cases.
             *
             * Good cases:
             * --G1: master key
             * --G2: regular key
             * --G3: public key and non-exist (unfunded) account match
             *
             * Bad cases:
             * --B1: disabled master key
             * --B2: single item signer list
             * --B3: public key and non-exist (unfunded) account mismatch
             * --B4: not on signer list
             * --B5: missing sfAttestationSignerAccount field
             */

            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;

            for (auto i = 0; i < UT_XCHAIN_DEFAULT_NUM_SIGNERS - 2; ++i)
                scEnv.fund(amt, alt_signers[i].account);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, alt_signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            Account const dst{scBob};
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
            auto const dstStartBalance = scEnv.env_.balance(dst);

            {
                // G1: master key
                auto att = claim_attestation(
                    scAttester, jvb, mcAlice, amt, payees[0], true, claimID, dst, alt_signers[0]);
                scEnv.tx(att).close();
            }
            {
                // G2: regular key
                // alt_signers[0] is the regular key of alt_signers[1]
                // There should be 2 attestations after the transaction
                scEnv.tx(jtx::regkey(alt_signers[1].account, alt_signers[0].account)).close();
                auto att = claim_attestation(
                    scAttester, jvb, mcAlice, amt, payees[1], true, claimID, dst, alt_signers[0]);
                att[sfAttestationSignerAccount.getJsonName()] = alt_signers[1].account.human();
                scEnv.tx(att).close();
            }
            {
                // B3: public key and non-exist (unfunded) account mismatch
                // G3: public key and non-exist (unfunded) account match
                auto const unfundedSigner1 = alt_signers[UT_XCHAIN_DEFAULT_NUM_SIGNERS - 1];
                auto const unfundedSigner2 = alt_signers[UT_XCHAIN_DEFAULT_NUM_SIGNERS - 2];
                auto att = claim_attestation(
                    scAttester,
                    jvb,
                    mcAlice,
                    amt,
                    payees[UT_XCHAIN_DEFAULT_NUM_SIGNERS - 1],
                    true,
                    claimID,
                    dst,
                    unfundedSigner1);
                att[sfAttestationSignerAccount.getJsonName()] = unfundedSigner2.account.human();
                scEnv.tx(att, ter(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR)).close();
                att[sfAttestationSignerAccount.getJsonName()] = unfundedSigner1.account.human();
                scEnv.tx(att).close();
            }
            {
                // B2: single item signer list
                std::vector<signer> tempSignerList = {signers[0]};
                scEnv.tx(jtx::signers(alt_signers[2].account, 1, tempSignerList));
                auto att = claim_attestation(
                    scAttester,
                    jvb,
                    mcAlice,
                    amt,
                    payees[2],
                    true,
                    claimID,
                    dst,
                    tempSignerList.front());
                att[sfAttestationSignerAccount.getJsonName()] = alt_signers[2].account.human();
                scEnv.tx(att, ter(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR)).close();
            }
            {
                // B1: disabled master key
                scEnv.tx(fset(alt_signers[2].account, asfDisableMaster, 0)).close();
                auto att = claim_attestation(
                    scAttester, jvb, mcAlice, amt, payees[2], true, claimID, dst, alt_signers[2]);
                scEnv.tx(att, ter(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR)).close();
            }
            {
                // --B4: not on signer list
                auto att = claim_attestation(
                    scAttester, jvb, mcAlice, amt, payees[0], true, claimID, dst, signers[0]);
                scEnv.tx(att, ter(tecNO_PERMISSION)).close();
            }
            {
                // --B5: missing sfAttestationSignerAccount field
                // Then submit the one with the field. Should reach quorum.
                auto att = claim_attestation(
                    scAttester, jvb, mcAlice, amt, payees[3], true, claimID, dst, alt_signers[3]);
                att.removeMember(sfAttestationSignerAccount.getJsonName());
                scEnv.tx(att, ter(temMALFORMED)).close();
                BEAST_EXPECT(dstStartBalance == scEnv.env_.balance(dst));
                att[sfAttestationSignerAccount.getJsonName()] = alt_signers[3].account.human();
                scEnv.tx(att).close();
                BEAST_EXPECT(dstStartBalance + amt == scEnv.env_.balance(dst));
            }
        }
    }

    void
    testXChainAddAccountCreateNonBatchAttestation()  // cspell: disable-line
    {
        using namespace jtx;

        testcase("Add Non Batch Account Create Attestation");

        XEnv mcEnv(*this);
        XEnv scEnv(*this, true);

        XRPAmount tx_fee = mcEnv.txFee();

        Account a{"a"};
        Account doorA{"doorA"};

        STAmount funds{XRP(10000)};
        mcEnv.fund(funds, a);
        mcEnv.fund(funds, doorA);

        Account ua{"ua"};  // unfunded account we want to create

        BridgeDef xrp_b{
            doorA,
            xrpIssue(),
            Account::master,
            xrpIssue(),
            XRP(1),   // reward
            XRP(20),  // minAccountCreate
            4,        // quorum
            signers,
            Json::nullValue};

        xrp_b.initBridge(mcEnv, scEnv);

        auto const amt = XRP(777);
        auto const amt_plus_reward = amt + xrp_b.reward;
        {
            Balance bal_doorA(mcEnv, doorA);
            Balance bal_a(mcEnv, a);

            mcEnv.tx(sidechain_xchain_account_create(a, xrp_b.jvb, ua, amt, xrp_b.reward)).close();

            BEAST_EXPECT(bal_doorA.diff() == amt_plus_reward);
            BEAST_EXPECT(bal_a.diff() == -(amt_plus_reward + tx_fee));
        }

        for (int i = 0; i < signers.size(); ++i)
        {
            auto const att = create_account_attestation(
                signers[0].account,
                xrp_b.jvb,
                a,
                amt,
                xrp_b.reward,
                signers[i].account,
                true,
                1,
                ua,
                signers[i]);
            TER const expectedTER =
                i < xrp_b.quorum ? tesSUCCESS : TER{tecXCHAIN_ACCOUNT_CREATE_PAST};

            scEnv.tx(att, ter(expectedTER)).close();
            if (i + 1 < xrp_b.quorum)
            {
                BEAST_EXPECT(!scEnv.env_.le(ua));
            }
            else
            {
                BEAST_EXPECT(scEnv.env_.le(ua));
            }
        }
        BEAST_EXPECT(scEnv.env_.le(ua));
    }

    void
    testXChainClaim()
    {
        using namespace jtx;

        XRPAmount res0 = reserve(0);
        XRPAmount tx_fee = txFee();

        testcase("Claim");

        // Claim where the amount matches what is attested to, to an account
        // that exists, and there are enough attestations to reach a quorum
        // => should succeed
        // -----------------------------------------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM,
                withClaim);

            scEnv
                .multiTx(claim_attestations(
                    scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers))
                .close();
            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)).close();
            }

            BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));
        }

        // Claim with just one attestation signed by the Master key
        // => should not succeed
        // -----------------------------------------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv
                .tx(create_bridge(Account::master, jvb))
                //.tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv, Account::master, scBob, scAlice, &payees[0], 1, withClaim);

            jtx::signer master_signer(Account::master);
            scEnv
                .tx(claim_attestation(
                        scAttester,
                        jvb,
                        mcAlice,
                        amt,
                        payees[0],
                        true,
                        claimID,
                        dst,
                        master_signer),
                    ter(tecXCHAIN_NO_SIGNERS_LIST))
                .close();

            BEAST_EXPECT(transfer.has_not_happened());
        }

        // Claim with just one attestation signed by a regular key
        // associated to the master account
        // => should not succeed
        // -----------------------------------------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv
                .tx(create_bridge(Account::master, jvb))
                //.tx(jtx::signers(Account::master, quorum, signers))
                .tx(jtx::regkey(Account::master, payees[0]))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv, Account::master, scBob, scAlice, &payees[0], 1, withClaim);

            jtx::signer master_signer(payees[0]);
            scEnv
                .tx(claim_attestation(
                        scAttester,
                        jvb,
                        mcAlice,
                        amt,
                        payees[0],
                        true,
                        claimID,
                        dst,
                        master_signer),
                    ter(tecXCHAIN_NO_SIGNERS_LIST))
                .close();

            BEAST_EXPECT(transfer.has_not_happened());
        }

        // Claim against non-existent bridge
        // ---------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            auto jvb_unknown = bridge(mcBob, xrpIssue(), Account::master, xrpIssue());

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb_unknown, reward, mcAlice), ter(tecNO_ENTRY))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb_unknown, claimID, amt, dst), ter(tecNO_ENTRY))
                .close();

            BalanceTransfer transfer(scEnv, Account::master, scBob, scAlice, payees, withClaim);
            scEnv
                .tx(claim_attestation(
                        scAttester,
                        jvb_unknown,
                        mcAlice,
                        amt,
                        payees[0],
                        true,
                        claimID,
                        dst,
                        signers[0]),
                    ter(tecNO_ENTRY))
                .close();

            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb_unknown, claimID, amt, scBob), ter(tecNO_ENTRY))
                    .close();
            }

            BEAST_EXPECT(transfer.has_not_happened());
        }

        // Claim against non-existent claim id
        // -----------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(scEnv, Account::master, scBob, scAlice, payees, withClaim);

            // attest using non-existent claim id
            scEnv
                .tx(claim_attestation(
                        scAttester, jvb, mcAlice, amt, payees[0], true, 999, dst, signers[0]),
                    ter(tecXCHAIN_NO_CLAIM_ID))
                .close();
            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // claim using non-existent claim id
                scEnv.tx(xchain_claim(scAlice, jvb, 999, amt, scBob), ter(tecXCHAIN_NO_CLAIM_ID))
                    .close();
            }

            BEAST_EXPECT(transfer.has_not_happened());
        }

        // Claim against a claim id owned by another account
        // -------------------------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM,
                withClaim);

            scEnv
                .multiTx(claim_attestations(
                    scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers))
                .close();
            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // submit a claim transaction with the wrong account (scGw
                // instead of scAlice)
                scEnv.tx(xchain_claim(scGw, jvb, claimID, amt, scBob), ter(tecXCHAIN_BAD_CLAIM_ID))
                    .close();
                BEAST_EXPECT(transfer.has_not_happened());
            }
            else
            {
                BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));
            }
        }

        // Claim against a claim id with no attestations
        // ---------------------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(scEnv, Account::master, scBob, scAlice, payees, withClaim);

            // don't send any attestations

            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv
                    .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
                        ter(tecXCHAIN_CLAIM_NO_QUORUM))
                    .close();
            }

            BEAST_EXPECT(transfer.has_not_happened());
        }

        // Claim against a claim id with attestations, but not enough to
        // make a quorum
        // --------------------------------------------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(scEnv, Account::master, scBob, scAlice, payees, withClaim);

            auto tooFew = quorum - 1;
            scEnv
                .multiTx(claim_attestations(
                    scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers, tooFew))
                .close();
            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv
                    .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
                        ter(tecXCHAIN_CLAIM_NO_QUORUM))
                    .close();
            }

            BEAST_EXPECT(transfer.has_not_happened());
        }

        // Claim id of zero
        // ----------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(scEnv, Account::master, scBob, scAlice, payees, withClaim);

            scEnv
                .multiTx(
                    claim_attestations(
                        scAttester, jvb, mcAlice, amt, payees, true, 0, dst, signers),
                    ter(tecXCHAIN_NO_CLAIM_ID))
                .close();
            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, 0, amt, scBob), ter(tecXCHAIN_NO_CLAIM_ID))
                    .close();
            }

            BEAST_EXPECT(transfer.has_not_happened());
        }

        // Claim issue that does not match the expected issue on the bridge
        // (either LockingChainIssue or IssuingChainIssue, depending on the
        // chain). The claim id should already have enough attestations to
        // reach a quorum for this amount (for a different issuer).
        // ---------------------------------------------------------------------
        for (auto withClaim : {true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM,
                withClaim);

            scEnv
                .multiTx(claim_attestations(
                    scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers))
                .close();

            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv
                    .tx(xchain_claim(scAlice, jvb, claimID, scUSD(1000), scBob), ter(temBAD_AMOUNT))
                    .close();
            }

            BEAST_EXPECT(transfer.has_not_happened());
        }

        // Claim to a destination that does not already exist on the chain
        // -----------------------------------------------------------------
        for (auto withClaim : {true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scuBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM,
                withClaim);

            scEnv
                .multiTx(claim_attestations(
                    scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers))
                .close();
            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scuBob), ter(tecNO_DST)).close();
            }

            BEAST_EXPECT(transfer.has_not_happened());
        }

        // Claim where the claim id owner does not have enough XRP to pay
        // the reward
        // ------------------------------------------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();
            STAmount huge_reward{XRP(20000)};
            BEAST_EXPECT(huge_reward > scEnv.balance(scAlice));

            scEnv.tx(create_bridge(Account::master, jvb, huge_reward))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, huge_reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM,
                withClaim);

            if (withClaim)
            {
                scEnv
                    .multiTx(claim_attestations(
                        scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers))
                    .close();
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), ter(tecUNFUNDED_PAYMENT))
                    .close();
            }
            else
            {
                auto txns = claim_attestations(
                    scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers);
                for (int i = 0; i < UT_XCHAIN_DEFAULT_QUORUM - 1; ++i)
                {
                    scEnv.tx(txns[i]).close();
                }
                scEnv.tx(txns.back());
                scEnv.close();
                // The attestation should succeed, because it adds an
                // attestation, but the claim should fail with insufficient
                // funds
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), ter(tecUNFUNDED_PAYMENT))
                    .close();
            }

            BEAST_EXPECT(transfer.has_not_happened());
        }

        // Claim where the claim id owner has enough XRP to pay the reward,
        // but it would put his balance below the reserve
        // --------------------------------------------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .fund(
                    res0 + reward,
                    scuAlice)  // just not enough because of fees
                .close()
                .tx(xchain_create_claim_id(scuAlice, jvb, reward, mcAlice),
                    ter(tecINSUFFICIENT_RESERVE))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(scEnv, Account::master, scBob, scuAlice, payees, withClaim);

            scEnv
                .tx(claim_attestation(
                        scAttester, jvb, mcAlice, amt, payees[0], true, claimID, dst, signers[0]),
                    ter(tecXCHAIN_NO_CLAIM_ID))
                .close();
            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv
                    .tx(xchain_claim(scuAlice, jvb, claimID, amt, scBob),
                        ter(tecXCHAIN_NO_CLAIM_ID))
                    .close();
            }

            BEAST_EXPECT(transfer.has_not_happened());
        }

        // Pay to an account with deposit auth set
        // ---------------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .tx(fset("scBob", asfDepositAuth))  // set deposit auth
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM,
                withClaim);
            auto txns = claim_attestations(
                scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers);
            for (int i = 0; i < UT_XCHAIN_DEFAULT_QUORUM - 1; ++i)
            {
                scEnv.tx(txns[i]).close();
            }
            if (withClaim)
            {
                scEnv.tx(txns.back()).close();

                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), ter(tecNO_PERMISSION))
                    .close();

                // the transfer failed, but check that we can still use the
                // claimID with a different account
                Balance scCarol_bal(scEnv, scCarol);

                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scCarol)).close();
                BEAST_EXPECT(scCarol_bal.diff() == amt);
            }
            else
            {
                scEnv.tx(txns.back()).close();
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), ter(tecNO_PERMISSION))
                    .close();
                // A way would be to remove deposit auth and resubmit the
                // attestations (even though the witness servers won't do
                // it)
                scEnv
                    .tx(fset("scBob", 0, asfDepositAuth))  // clear deposit auth
                    .close();

                Balance scBob_bal(scEnv, scBob);
                scEnv.tx(txns.back()).close();
                BEAST_EXPECT(scBob_bal.diff() == amt);
            }
        }

        // Pay to an account with Destination Tag set
        // ------------------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .tx(fset("scBob", asfRequireDest))  // set dest tag
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM,
                withClaim);
            auto txns = claim_attestations(
                scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers);
            for (int i = 0; i < UT_XCHAIN_DEFAULT_QUORUM - 1; ++i)
            {
                scEnv.tx(txns[i]).close();
            }
            if (withClaim)
            {
                scEnv.tx(txns.back()).close();
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), ter(tecDST_TAG_NEEDED))
                    .close();

                // the transfer failed, but check that we can still use the
                // claimID with a different account
                Balance scCarol_bal(scEnv, scCarol);

                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scCarol)).close();
                BEAST_EXPECT(scCarol_bal.diff() == amt);
            }
            else
            {
                scEnv.tx(txns.back()).close();
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), ter(tecDST_TAG_NEEDED))
                    .close();
                // A way would be to remove the destination tag requirement
                // and resubmit the attestations (even though the witness
                // servers won't do it)
                scEnv
                    .tx(fset("scBob", 0, asfRequireDest))  // clear dest tag
                    .close();

                Balance scBob_bal(scEnv, scBob);

                scEnv.tx(txns.back()).close();
                BEAST_EXPECT(scBob_bal.diff() == amt);
            }
        }

        // Pay to an account with deposit auth set. Check that the attestations
        // are still validated and that we can used the claimID to transfer the
        // funds to a different account (which doesn't have deposit auth set)
        // --------------------------------------------------------------------
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .tx(fset("scBob", asfDepositAuth))  // set deposit auth
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            // we should be able to submit the attestations, but the transfer
            // should not occur because dest account has deposit auth set
            Balance scBob_bal(scEnv, scBob);

            scEnv.multiTx(claim_attestations(
                scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers));
            BEAST_EXPECT(scBob_bal.diff() == STAmount(0));

            // Check that check that we still can use the claimID to transfer
            // the amount to a different account
            Balance scCarol_bal(scEnv, scCarol);

            scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scCarol)).close();
            BEAST_EXPECT(scCarol_bal.diff() == amt);
        }

        // Claim where the amount different from what is attested to
        // ---------------------------------------------------------
        for (auto withClaim : {true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM,
                withClaim);
            scEnv.multiTx(claim_attestations(
                scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers));
            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // claim wrong amount
                scEnv
                    .tx(xchain_claim(scAlice, jvb, claimID, one_xrp, scBob),
                        ter(tecXCHAIN_CLAIM_NO_QUORUM))
                    .close();
            }

            BEAST_EXPECT(transfer.has_not_happened());
        }

        // Verify that rewards are paid from the account that owns the claim
        // id
        // --------------------------------------------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM,
                withClaim);
            Balance scAlice_bal(scEnv, scAlice);
            scEnv.multiTx(claim_attestations(
                scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers));

            STAmount claim_cost = reward;

            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)).close();
                claim_cost += tx_fee;
            }

            BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));
            BEAST_EXPECT(scAlice_bal.diff() == -claim_cost);  // because reward % 4 == 0
        }

        // Verify that if a reward is not evenly divisible among the reward
        // accounts, the remaining amount goes to the claim id owner.
        // ----------------------------------------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb, tiny_reward)).close();

            scEnv.tx(create_bridge(Account::master, jvb, tiny_reward))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, tiny_reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM,
                withClaim);
            Balance scAlice_bal(scEnv, scAlice);
            scEnv.multiTx(claim_attestations(
                scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers));
            STAmount claim_cost = tiny_reward;

            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)).close();
                claim_cost += tx_fee;
            }

            BEAST_EXPECT(transfer.has_happened(amt, tiny_reward_split));
            BEAST_EXPECT(scAlice_bal.diff() == -(claim_cost - tiny_reward_remainder));
        }

        // If a reward distribution fails for one of the reward accounts
        // (the reward account doesn't exist or has deposit auth set), then
        // the txn should still succeed, but that portion should go to the
        // claim id owner.
        // -------------------------------------------------------------------
        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();

            std::vector<Account> alt_payees{payees.begin(), payees.end() - 1};
            alt_payees.back() = Account("inexistent");

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM - 1,
                withClaim);
            scEnv.multiTx(claim_attestations(
                scAttester, jvb, mcAlice, amt, alt_payees, true, claimID, dst, signers));

            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)).close();
            }

            // this also checks that only 3 * split_reward was deducted from
            // scAlice (the payer account), since we passed alt_payees to
            // BalanceTransfer
            BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));
        }

        for (auto withClaim : {false, true})
        {
            XEnv mcEnv(*this);
            XEnv scEnv(*this, true);

            mcEnv.tx(create_bridge(mcDoor, jvb)).close();
            auto& unpaid = payees[UT_XCHAIN_DEFAULT_QUORUM - 1];
            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .tx(fset(unpaid, asfDepositAuth))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
            auto const amt = XRP(1000);
            std::uint32_t const claimID = 1;
            mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();

            // balance of last signer should not change (has deposit auth)
            Balance last_signer(scEnv, unpaid);

            // make sure all signers except the last one get the
            // split_reward

            BalanceTransfer transfer(
                scEnv,
                Account::master,
                scBob,
                scAlice,
                &payees[0],
                UT_XCHAIN_DEFAULT_QUORUM - 1,
                withClaim);
            scEnv.multiTx(claim_attestations(
                scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers));

            if (withClaim)
            {
                BEAST_EXPECT(transfer.has_not_happened());

                // need to submit a claim transactions
                scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)).close();
            }

            // this also checks that only 3 * split_reward was deducted from
            // scAlice (the payer account), since we passed payees.size() -
            // 1 to BalanceTransfer
            BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));

            // and make sure the account with deposit auth received nothing
            BEAST_EXPECT(last_signer.diff() == STAmount(0));
        }

        // coverage test: xchain_claim transaction with incorrect flag
        XEnv(*this, true)
            .tx(create_bridge(Account::master, jvb))
            .close()
            .tx(xchain_claim(scAlice, jvb, 1, XRP(1000), scBob),
                txflags(tfFillOrKill),
                ter(temINVALID_FLAG))
            .close();

        // coverage test: xchain_claim transaction with xchain feature
        // disabled
        XEnv(*this, true)
            .tx(create_bridge(Account::master, jvb))
            .disableFeature(featureXChainBridge)
            .close()
            .tx(xchain_claim(scAlice, jvb, 1, XRP(1000), scBob), ter(temDISABLED))
            .close();

        // coverage test: XChainClaim::preclaim - isLockingChain = true;
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb))
            .close()
            .tx(xchain_claim(mcAlice, jvb, 1, XRP(1000), mcBob), ter(tecXCHAIN_NO_CLAIM_ID));
    }

    void
    testXChainCreateAccount()
    {
        using namespace jtx;

        testcase("Bridge Create Account");
        XRPAmount tx_fee = txFee();

        // coverage test: transferHelper() - dst == src
        {
            XEnv scEnv(*this, true);

            auto const amt = XRP(111);
            auto const amt_plus_reward = amt + reward;

            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close();

            Balance door(scEnv, Account::master);

            // scEnv.tx(att_create_acct_batch1(1, amt,
            // Account::master)).close();
            scEnv.multiTx(att_create_acct_vec(1, amt, Account::master, 2)).close();
            BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1));   // claim id present
            BEAST_EXPECT(scEnv.claimCount(jvb) == 0);  // claim count is one less

            // scEnv.tx(att_create_acct_batch2(1, amt,
            // Account::master)).close();
            scEnv.multiTx(att_create_acct_vec(1, amt, Account::master, 2, 2)).close();
            BEAST_EXPECT(!scEnv.caClaimID(jvb, 1));    // claim id deleted
            BEAST_EXPECT(scEnv.claimCount(jvb) == 1);  // claim count was incremented

            BEAST_EXPECT(door.diff() == -reward);
        }

        // Check that creating an account with less than the minimum create
        // amount fails.
        {
            XEnv mcEnv(*this);

            mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();

            Balance door(mcEnv, mcDoor);
            Balance carol(mcEnv, mcCarol);

            mcEnv
                .tx(sidechain_xchain_account_create(mcCarol, jvb, scuAlice, XRP(19), reward),
                    ter(tecXCHAIN_INSUFF_CREATE_AMOUNT))
                .close();

            BEAST_EXPECT(door.diff() == STAmount(0));
            BEAST_EXPECT(carol.diff() == -tx_fee);
        }

        // Check that creating an account with invalid flags fails.
        {
            XEnv mcEnv(*this);

            mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();

            Balance door(mcEnv, mcDoor);

            mcEnv
                .tx(sidechain_xchain_account_create(mcCarol, jvb, scuAlice, XRP(20), reward),
                    txflags(tfFillOrKill),
                    ter(temINVALID_FLAG))
                .close();

            BEAST_EXPECT(door.diff() == STAmount(0));
        }

        // Check that creating an account with the XChainBridge feature
        // disabled fails.
        {
            XEnv mcEnv(*this);

            mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();

            Balance door(mcEnv, mcDoor);

            mcEnv.disableFeature(featureXChainBridge)
                .tx(sidechain_xchain_account_create(mcCarol, jvb, scuAlice, XRP(20), reward),
                    ter(temDISABLED))
                .close();

            BEAST_EXPECT(door.diff() == STAmount(0));
        }

        // Check that creating an account with a negative amount fails
        {
            XEnv mcEnv(*this);

            mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();

            Balance door(mcEnv, mcDoor);

            mcEnv
                .tx(sidechain_xchain_account_create(mcCarol, jvb, scuAlice, XRP(-20), reward),
                    ter(temBAD_AMOUNT))
                .close();

            BEAST_EXPECT(door.diff() == STAmount(0));
        }

        // Check that creating an account with a negative reward fails
        {
            XEnv mcEnv(*this);

            mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();

            Balance door(mcEnv, mcDoor);

            mcEnv
                .tx(sidechain_xchain_account_create(mcCarol, jvb, scuAlice, XRP(20), XRP(-1)),
                    ter(temBAD_AMOUNT))
                .close();

            BEAST_EXPECT(door.diff() == STAmount(0));
        }

        // Check that door account can't lock funds onto itself
        {
            XEnv mcEnv(*this);

            mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();

            Balance door(mcEnv, mcDoor);

            mcEnv
                .tx(sidechain_xchain_account_create(mcDoor, jvb, scuAlice, XRP(20), XRP(1)),
                    ter(tecXCHAIN_SELF_COMMIT))
                .close();

            BEAST_EXPECT(door.diff() == -tx_fee);
        }

        // Check that reward matches the amount specified in bridge
        {
            XEnv mcEnv(*this);

            mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();

            Balance door(mcEnv, mcDoor);

            mcEnv
                .tx(sidechain_xchain_account_create(mcCarol, jvb, scuAlice, XRP(20), XRP(2)),
                    ter(tecXCHAIN_REWARD_MISMATCH))
                .close();

            BEAST_EXPECT(door.diff() == STAmount(0));
        }
    }

    void
    testFeeDipsIntoReserve()
    {
        using namespace jtx;
        XRPAmount res0 = reserve(0);
        XRPAmount tx_fee = txFee();

        testcase("Fee dips into reserve");

        // commit where the fee dips into the reserve, this should succeed
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb))
            .fund(res0 + one_xrp + tx_fee - drops(1), mcuAlice)
            .close()
            .tx(xchain_commit(mcuAlice, jvb, 1, one_xrp, scBob), ter(tesSUCCESS));

        // commit where the commit amount drips into the reserve, this should
        // fail
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb))
            .fund(res0 + one_xrp - drops(1), mcuAlice)
            .close()
            .tx(xchain_commit(mcuAlice, jvb, 1, one_xrp, scBob), ter(tecUNFUNDED_PAYMENT));

        auto const minAccountCreate = XRP(20);

        // account create commit where the fee dips into the reserve,
        // this should succeed
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb, reward, minAccountCreate))
            .fund(res0 + tx_fee + minAccountCreate + reward - drops(1), mcuAlice)
            .close()
            .tx(sidechain_xchain_account_create(mcuAlice, jvb, scuAlice, minAccountCreate, reward),
                ter(tesSUCCESS));

        // account create commit where the commit dips into the reserve,
        // this should fail
        XEnv(*this)
            .tx(create_bridge(mcDoor, jvb, reward, minAccountCreate))
            .fund(res0 + minAccountCreate + reward - drops(1), mcuAlice)
            .close()
            .tx(sidechain_xchain_account_create(mcuAlice, jvb, scuAlice, minAccountCreate, reward),
                ter(tecUNFUNDED_PAYMENT));
    }

    void
    testXChainDeleteDoor()
    {
        using namespace jtx;

        testcase("Bridge Delete Door Account");

        auto const acctDelFee{drops(XEnv(*this).env_.current()->fees().increment)};

        // Deleting an account that owns bridge should fail
        {
            XEnv mcEnv(*this);

            mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(1))).close();

            // We don't allow an account to be deleted if its sequence
            // number is within 256 of the current ledger.
            for (size_t i = 0; i < 256; ++i)
                mcEnv.close();

            // try to delete mcDoor, send funds to mcAlice
            mcEnv.tx(acctdelete(mcDoor, mcAlice), fee(acctDelFee), ter(tecHAS_OBLIGATIONS));
        }

        // Deleting an account that owns a claim id should fail
        {
            XEnv scEnv(*this, true);

            scEnv.tx(create_bridge(Account::master, jvb))
                .close()
                .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
                .close();

            // We don't allow an account to be deleted if its sequence
            // number is within 256 of the current ledger.
            for (size_t i = 0; i < 256; ++i)
                scEnv.close();

            // try to delete scAlice, send funds to scBob
            scEnv.tx(acctdelete(scAlice, scBob), fee(acctDelFee), ter(tecHAS_OBLIGATIONS));
        }
    }

    void
    testBadPublicKey()
    {
        using namespace jtx;

        testcase("Bad attestations");
        {
            // Create a bridge and add an attestation with a bad public key
            XEnv scEnv(*this, true);
            std::uint32_t const claimID = 1;
            std::optional<Account> dst{scBob};
            auto const amt = XRP(1000);
            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close();
            scEnv.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)).close();
            auto jvAtt = claim_attestation(
                scAttester,
                jvb,
                mcAlice,
                amt,
                payees[UT_XCHAIN_DEFAULT_QUORUM],
                true,
                claimID,
                dst,
                signers[UT_XCHAIN_DEFAULT_QUORUM]);
            {
                // Change to an invalid keytype
                auto k = jvAtt["PublicKey"].asString();
                k.at(1) = '9';
                jvAtt["PublicKey"] = k;
            }
            scEnv.tx(jvAtt, ter(temMALFORMED)).close();
        }
        {
            // Create a bridge and add an create account attestation with a bad
            // public key
            XEnv scEnv(*this, true);
            std::uint32_t const createCount = 1;
            Account dst{scBob};
            auto const amt = XRP(1000);
            auto const rewardAmt = XRP(1);
            scEnv.tx(create_bridge(Account::master, jvb))
                .tx(jtx::signers(Account::master, quorum, signers))
                .close();
            auto jvAtt = create_account_attestation(
                scAttester,
                jvb,
                mcAlice,
                amt,
                rewardAmt,
                payees[UT_XCHAIN_DEFAULT_QUORUM],
                true,
                createCount,
                dst,
                signers[UT_XCHAIN_DEFAULT_QUORUM]);
            {
                // Change to an invalid keytype
                auto k = jvAtt["PublicKey"].asString();
                k.at(1) = '9';
                jvAtt["PublicKey"] = k;
            }
            scEnv.tx(jvAtt, ter(temMALFORMED)).close();
        }
    }

    void
    run() override
    {
        testXChainBridgeExtraFields();
        testXChainCreateBridge();
        testXChainBridgeCreateConstraints();
        testXChainCreateBridgeMatrix();
        testXChainModifyBridge();
        testXChainCreateClaimID();
        testXChainCommit();
        testXChainAddAttestation();
        testXChainAddClaimNonBatchAttestation();
        testXChainAddAccountCreateNonBatchAttestation();  // cspell: disable-line
        testXChainClaim();
        testXChainCreateAccount();
        testFeeDipsIntoReserve();
        testXChainDeleteDoor();
        testBadPublicKey();
    }
};

// -----------------------------------------------------------
// -----------------------------------------------------------
struct XChainSim_test : public beast::unit_test::suite, public jtx::XChainBridgeObjects
{
private:
    static constexpr size_t num_signers = 5;

    // --------------------------------------------------
    enum class WithClaim { no, yes };
    struct Transfer
    {
        jtx::Account from;
        jtx::Account to;
        jtx::Account finaldest;
        STAmount amt;
        bool a2b;  // direction of transfer
        WithClaim with_claim{WithClaim::no};
        uint32_t claim_id{0};
        std::array<bool, num_signers> attested{};
    };

    struct AccountCreate
    {
        jtx::Account from;
        jtx::Account to;
        STAmount amt;
        STAmount reward;
        bool a2b;
        uint32_t claim_id{0};
        std::array<bool, num_signers> attested{};
    };

    using ENV = XEnv<XChainSim_test>;
    using BridgeID = BridgeDef const*;

    // tracking chain state
    // --------------------
    struct AccountStateTrack
    {
        STAmount startAmount{0};
        STAmount expectedDiff{0};

        void
        init(ENV& env, jtx::Account const& acct)
        {
            startAmount = env.balance(acct);
            expectedDiff = STAmount(0);
        }

        bool
        verify(ENV& env, jtx::Account const& acct) const
        {
            STAmount diff{env.balance(acct) - startAmount};
            bool check = diff == expectedDiff;
            return check;
        }
    };

    // --------------------------------------------------
    struct ChainStateTrack
    {
        using ClaimVec = jtx::JValueVec;
        using CreateClaimVec = jtx::JValueVec;
        using CreateClaimMap = std::map<uint32_t, CreateClaimVec>;

        ChainStateTrack(ENV& env) : env(env), tx_fee(env.env_.current()->fees().base)
        {
        }

        void
        sendAttestations(size_t signer_idx, BridgeID bridge, ClaimVec& claims)
        {
            for (auto const& c : claims)
            {
                env.tx(c).close();
                spendFee(bridge->signers[signer_idx].account);
            }
            claims.clear();
        }

        uint32_t
        sendCreateAttestations(size_t signer_idx, BridgeID bridge, CreateClaimVec& claims)
        {
            size_t num_successful = 0;
            for (auto const& c : claims)
            {
                env.tx(c).close();
                if (env.ter() == tesSUCCESS)
                {
                    counters[bridge].signers.push_back(signer_idx);
                    num_successful++;
                }
                spendFee(bridge->signers[signer_idx].account);
            }
            claims.clear();
            return num_successful;
        }

        void
        sendAttestations()
        {
            bool callback_called = false;

            // we have this "do {} while" loop because we want to process
            // all the account create which can reach quorum at this time
            // stamp.
            do
            {
                callback_called = false;
                // cspell: ignore attns
                for (size_t i = 0; i < signers_attns.size(); ++i)
                {
                    for (auto& [bridge, claims] : signers_attns[i])
                    {
                        sendAttestations(i, bridge, claims.xfer_claims);

                        auto& c = counters[bridge];
                        auto& create_claims = claims.create_claims[c.claim_count];
                        auto num_attns = create_claims.size();
                        if (num_attns != 0u)
                        {
                            c.num_create_attn_sent +=
                                sendCreateAttestations(i, bridge, create_claims);
                        }
                        assert(claims.create_claims[c.claim_count].empty());
                    }
                }
                for (auto& [bridge, c] : counters)
                {
                    if (c.num_create_attn_sent >= bridge->quorum)
                    {
                        callback_called = true;
                        c.create_callbacks[c.claim_count](c.signers);
                        ++c.claim_count;
                        c.num_create_attn_sent = 0;
                        c.signers.clear();
                    }
                }
            } while (callback_called);
        }

        void
        init(jtx::Account const& acct)
        {
            accounts[acct].init(env, acct);
        }

        void
        receive(jtx::Account const& acct, STAmount amt, std::uint64_t divisor = 1)
        {
            if (amt.issue() != xrpIssue())
                return;
            auto it = accounts.find(acct);
            if (it == accounts.end())
            {
                accounts[acct].init(env, acct);
                // we just looked up the account, so expectedDiff == 0
            }
            else
            {
                it->second.expectedDiff +=
                    (divisor == 1 ? amt : divide(amt, STAmount(amt.issue(), divisor), amt.issue()));
            }
        }

        void
        spend(jtx::Account const& acct, STAmount amt, std::uint64_t times = 1)
        {
            if (amt.issue() != xrpIssue())
                return;
            receive(
                acct,
                times == 1 ? -amt : -multiply(amt, STAmount(amt.issue(), times), amt.issue()));
        }

        void
        transfer(jtx::Account const& from, jtx::Account const& to, STAmount amt)
        {
            spend(from, amt);
            receive(to, amt);
        }

        void
        spendFee(jtx::Account const& acct, size_t times = 1)
        {
            spend(acct, tx_fee, times);
        }

        bool
        verify() const
        {
            for (auto const& [acct, state] : accounts)
            {
                if (!state.verify(env, acct))
                    return false;
            }
            return true;
        }

        struct BridgeCounters
        {
            using complete_cb = std::function<void(std::vector<size_t> const& signers)>;

            uint32_t claim_id{0};
            uint32_t create_count{0};  // for account create. First should be 1
            uint32_t claim_count{0};   // for account create. Increments after quorum for
                                       // current create_count (starts at 1) is reached.

            uint32_t num_create_attn_sent{0};  // for current claim_count
            std::vector<size_t> signers;
            std::vector<complete_cb> create_callbacks;
        };

        struct Claims
        {
            ClaimVec xfer_claims;
            CreateClaimMap create_claims;
        };

        using SignerAttns = std::unordered_map<BridgeID, Claims>;
        using SignersAttns = std::array<SignerAttns, num_signers>;

        ENV& env;
        std::map<jtx::Account, AccountStateTrack> accounts;
        SignersAttns signers_attns;
        std::map<BridgeID, BridgeCounters> counters;
        STAmount tx_fee;
    };

    struct ChainStateTracker
    {
        ChainStateTracker(ENV& a_env, ENV& b_env) : a_(a_env), b_(b_env)
        {
        }

        bool
        verify() const
        {
            return a_.verify() && b_.verify();
        }

        void
        sendAttestations()
        {
            a_.sendAttestations();
            b_.sendAttestations();
        }

        void
        init(jtx::Account const& acct)
        {
            a_.init(acct);
            b_.init(acct);
        }

        ChainStateTrack a_;
        ChainStateTrack b_;
    };

    enum SmState {
        st_initial,
        st_claim_id_created,
        st_attesting,
        st_attested,
        st_completed,
        st_closed,
    };

    enum Act_Flags { af_a2b = 1 << 0 };

    // --------------------------------------------------
    template <class T>
    class SmBase
    {
        SmBase(std::shared_ptr<ChainStateTracker> const& chainstate, BridgeDef const& bridge)
            : bridge_(bridge), st_(chainstate)
        {
        }

    public:
        ChainStateTrack&
        srcState()
        {
            return static_cast<T&>(*this).a2b() ? st_->a_ : st_->b_;
        }

        ChainStateTrack&
        destState()
        {
            return static_cast<T&>(*this).a2b() ? st_->b_ : st_->a_;
        }

        jtx::Account const&
        srcDoor()
        {
            return static_cast<T&>(*this).a2b() ? bridge_.doorA : bridge_.doorB;
        }

        jtx::Account const&
        dstDoor()
        {
            return static_cast<T&>(*this).a2b() ? bridge_.doorB : bridge_.doorA;
        }

    protected:
        BridgeDef const& bridge_;
        std::shared_ptr<ChainStateTracker> st_;

        friend T;
    };

    // --------------------------------------------------
    class SmCreateAccount : public SmBase<SmCreateAccount>
    {
    public:
        using Base = SmBase<SmCreateAccount>;

        SmCreateAccount(
            std::shared_ptr<ChainStateTracker> const& chainstate,
            BridgeDef const& bridge,
            AccountCreate create)
            : Base(chainstate, bridge), cr(std::move(create))
        {
        }

        bool
        a2b() const
        {
            return cr.a2b;
        }

        uint32_t
        issue_account_create()
        {
            ChainStateTrack& st = srcState();
            jtx::Account const& srcdoor = srcDoor();

            st.env
                .tx(sidechain_xchain_account_create(cr.from, bridge_.jvb, cr.to, cr.amt, cr.reward))
                .close();  // needed for claim_id sequence to be correct'
            st.spendFee(cr.from);
            st.transfer(cr.from, srcdoor, cr.amt);
            st.transfer(cr.from, srcdoor, cr.reward);

            return ++st.counters[&bridge_].create_count;
        }

        void
        attest(uint64_t time, uint32_t rnd)
        {
            ChainStateTrack& st = destState();

            // check all signers, but start at a random one
            size_t i = 0;
            for (i = 0; i < num_signers; ++i)
            {
                size_t signer_idx = (rnd + i) % num_signers;

                if (!(cr.attested[signer_idx]))
                {
                    // enqueue one attestation for this signer
                    cr.attested[signer_idx] = true;

                    st.signers_attns[signer_idx][&bridge_]
                        .create_claims[cr.claim_id - 1]
                        .emplace_back(create_account_attestation(
                            bridge_.signers[signer_idx].account,
                            bridge_.jvb,
                            cr.from,
                            cr.amt,
                            cr.reward,
                            bridge_.signers[signer_idx].account,
                            cr.a2b,
                            cr.claim_id,
                            cr.to,
                            bridge_.signers[signer_idx]));
                    break;
                }
            }

            if (i == num_signers)
                return;  // did not attest

            auto& counters = st.counters[&bridge_];
            if (counters.create_callbacks.size() < cr.claim_id)
                counters.create_callbacks.resize(cr.claim_id);

            auto complete_cb = [&](std::vector<size_t> const& signers) {
                auto num_attestors = signers.size();
                st.env.close();
                assert(num_attestors <= std::count(cr.attested.begin(), cr.attested.end(), true));
                assert(num_attestors >= bridge_.quorum);
                assert(cr.claim_id - 1 == counters.claim_count);

                auto r = cr.reward;
                auto reward = divide(r, STAmount(num_attestors), r.issue());

                for (auto i : signers)
                    st.receive(bridge_.signers[i].account, reward);

                st.spend(dstDoor(), reward, num_attestors);
                st.transfer(dstDoor(), cr.to, cr.amt);
                st.env.env_.memoize(cr.to);
                sm_state = st_completed;
            };

            counters.create_callbacks[cr.claim_id - 1] = std::move(complete_cb);
        }

        SmState
        advance(uint64_t time, uint32_t rnd)
        {
            switch (sm_state)
            {
                case st_initial:
                    cr.claim_id = issue_account_create();
                    sm_state = st_attesting;
                    break;

                case st_attesting:
                    attest(time, rnd);
                    break;

                default:
                    assert(0);
                    break;

                case st_completed:
                    break;  // will get this once
            }
            return sm_state;
        }

    private:
        SmState sm_state{st_initial};
        AccountCreate cr;
    };

    // --------------------------------------------------
    class SmTransfer : public SmBase<SmTransfer>
    {
    public:
        using Base = SmBase<SmTransfer>;

        SmTransfer(
            std::shared_ptr<ChainStateTracker> const& chainstate,
            BridgeDef const& bridge,
            Transfer xfer)
            : Base(chainstate, bridge), xfer(std::move(xfer))
        {
        }

        bool
        a2b() const
        {
            return xfer.a2b;
        }

        uint32_t
        create_claim_id()
        {
            ChainStateTrack& st = destState();

            st.env.tx(xchain_create_claim_id(xfer.to, bridge_.jvb, bridge_.reward, xfer.from))
                .close();  // needed for claim_id sequence to be
                           // correct'
            st.spendFee(xfer.to);
            return ++st.counters[&bridge_].claim_id;
        }

        void
        commit()
        {
            ChainStateTrack& st = srcState();
            jtx::Account const& srcdoor = srcDoor();

            if (xfer.amt.issue() != xrpIssue())
            {
                st.env.tx(pay(srcdoor, xfer.from, xfer.amt));
                st.spendFee(srcdoor);
            }
            st.env.tx(xchain_commit(
                xfer.from,
                bridge_.jvb,
                xfer.claim_id,
                xfer.amt,
                xfer.with_claim == WithClaim::yes ? std::nullopt
                                                  : std::optional<jtx::Account>(xfer.finaldest)));
            st.spendFee(xfer.from);
            st.transfer(xfer.from, srcdoor, xfer.amt);
        }

        void
        distribute_reward(ChainStateTrack& st)
        {
            auto r = bridge_.reward;
            auto reward = divide(r, STAmount(bridge_.quorum), r.issue());

            for (size_t i = 0; i < num_signers; ++i)
            {
                if (xfer.attested[i])
                    st.receive(bridge_.signers[i].account, reward);
            }
            st.spend(xfer.to, reward, bridge_.quorum);
        }

        bool
        attest(uint64_t time, uint32_t rnd)
        {
            ChainStateTrack& st = destState();

            // check all signers, but start at a random one
            for (size_t i = 0; i < num_signers; ++i)
            {
                size_t signer_idx = (rnd + i) % num_signers;
                if (!(xfer.attested[signer_idx]))
                {
                    // enqueue one attestation for this signer
                    xfer.attested[signer_idx] = true;

                    st.signers_attns[signer_idx][&bridge_].xfer_claims.emplace_back(
                        claim_attestation(
                            bridge_.signers[signer_idx].account,
                            bridge_.jvb,
                            xfer.from,
                            xfer.amt,
                            bridge_.signers[signer_idx].account,
                            xfer.a2b,
                            xfer.claim_id,
                            xfer.with_claim == WithClaim::yes
                                ? std::nullopt
                                : std::optional<jtx::Account>(xfer.finaldest),
                            bridge_.signers[signer_idx]));
                    break;
                }
            }

            // return true if quorum was reached, false otherwise
            bool quorum =
                std::count(xfer.attested.begin(), xfer.attested.end(), true) >= bridge_.quorum;
            if (quorum && xfer.with_claim == WithClaim::no)
            {
                distribute_reward(st);
                st.transfer(dstDoor(), xfer.finaldest, xfer.amt);
            }
            return quorum;
        }

        void
        claim()
        {
            ChainStateTrack& st = destState();
            st.env.tx(xchain_claim(xfer.to, bridge_.jvb, xfer.claim_id, xfer.amt, xfer.finaldest));
            distribute_reward(st);
            st.transfer(dstDoor(), xfer.finaldest, xfer.amt);
            st.spendFee(xfer.to);
        }

        SmState
        advance(uint64_t time, uint32_t rnd)
        {
            switch (sm_state)
            {
                case st_initial:
                    xfer.claim_id = create_claim_id();
                    sm_state = st_claim_id_created;
                    break;

                case st_claim_id_created:
                    commit();
                    sm_state = st_attesting;
                    break;

                case st_attesting:
                    if (attest(time, rnd))
                    {
                        sm_state = xfer.with_claim == WithClaim::yes ? st_attested : st_completed;
                    }
                    else
                    {
                        sm_state = st_attesting;
                    }
                    break;

                case st_attested:
                    assert(xfer.with_claim == WithClaim::yes);
                    claim();
                    sm_state = st_completed;
                    break;

                default:
                case st_completed:
                    assert(0);  // should have been removed
                    break;
            }
            return sm_state;
        }

    private:
        Transfer xfer;
        SmState sm_state{st_initial};
    };

    // --------------------------------------------------
    using Sm = std::variant<SmCreateAccount, SmTransfer>;
    using SmCont = std::list<std::pair<uint64_t, Sm>>;

    SmCont sm_;

    void
    xfer(
        uint64_t time,
        std::shared_ptr<ChainStateTracker> const& chainstate,
        BridgeDef const& bridge,
        Transfer transfer)
    {
        sm_.emplace_back(time, SmTransfer(chainstate, bridge, std::move(transfer)));
    }

    void
    ac(uint64_t time,
       std::shared_ptr<ChainStateTracker> const& chainstate,
       BridgeDef const& bridge,
       AccountCreate ac)
    {
        sm_.emplace_back(time, SmCreateAccount(chainstate, bridge, std::move(ac)));
    }

public:
    void
    runSimulation(std::shared_ptr<ChainStateTracker> const& st, bool verify_balances = true)
    {
        using namespace jtx;
        uint64_t time = 0;
        std::mt19937 gen(27);  // Standard mersenne_twister_engine
        std::uniform_int_distribution<uint32_t> distrib(0, 9);

        while (!sm_.empty())
        {
            ++time;
            for (auto it = sm_.begin(); it != sm_.end();)
            {
                auto vis = [&](auto& sm) {
                    uint32_t rnd = distrib(gen);
                    return sm.advance(time, rnd);
                };
                auto& [t, sm] = *it;
                if (t <= time && std::visit(vis, sm) == st_completed)
                {
                    it = sm_.erase(it);
                }
                else
                {
                    ++it;
                }
            }

            // send attestations
            st->sendAttestations();

            // make sure all transactions have been applied
            st->a_.env.close();
            st->b_.env.close();

            if (verify_balances)
            {
                BEAST_EXPECT(st->verify());
            }
        }
    }

    void
    testXChainSimulation()
    {
        using namespace jtx;

        testcase("Bridge usage simulation");

        XEnv mcEnv(*this);
        XEnv scEnv(*this, true);

        auto st = std::make_shared<ChainStateTracker>(mcEnv, scEnv);

        // create 10 accounts + door funded on both chains, and store
        // in ChainStateTracker the initial amount of these accounts
        Account doorXRPLocking("doorXRPLocking"), doorUSDLocking("doorUSDLocking"),
            doorUSDIssuing("doorUSDIssuing");

        constexpr size_t num_acct = 10;
        auto a = [&doorXRPLocking, &doorUSDLocking, &doorUSDIssuing]() {
            using namespace std::literals;
            std::vector<Account> result;
            result.reserve(num_acct);
            for (int i = 0; i < num_acct; ++i)
            {
                result.emplace_back(
                    "a"s + std::to_string(i), (i % 2) ? KeyType::ed25519 : KeyType::secp256k1);
            }
            result.emplace_back("doorXRPLocking");
            doorXRPLocking = result.back();
            result.emplace_back("doorUSDLocking");
            doorUSDLocking = result.back();
            result.emplace_back("doorUSDIssuing");
            doorUSDIssuing = result.back();
            return result;
        }();

        for (auto& acct : a)
        {
            STAmount amt{XRP(100000)};

            mcEnv.fund(amt, acct);
            scEnv.fund(amt, acct);
        }
        Account USDLocking{"USDLocking"};
        IOU usdLocking{USDLocking["USD"]};
        IOU usdIssuing{doorUSDIssuing["USD"]};

        mcEnv.fund(XRP(100000), USDLocking);
        mcEnv.close();
        mcEnv.tx(trust(doorUSDLocking, usdLocking(100000)));
        mcEnv.close();
        mcEnv.tx(pay(USDLocking, doorUSDLocking, usdLocking(50000)));

        for (int i = 0; i < a.size(); ++i)
        {
            auto& acct{a[i]};
            if (i < num_acct)
            {
                mcEnv.tx(trust(acct, usdLocking(100000)));
                scEnv.tx(trust(acct, usdIssuing(100000)));
            }
            st->init(acct);
        }
        for (auto& s : signers)
            st->init(s.account);

        st->b_.init(Account::master);

        // also create some unfunded accounts
        constexpr size_t num_ua = 20;
        auto ua = []() {
            using namespace std::literals;
            std::vector<Account> result;
            result.reserve(num_ua);
            for (int i = 0; i < num_ua; ++i)
            {
                result.emplace_back(
                    "ua"s + std::to_string(i), (i % 2) ? KeyType::ed25519 : KeyType::secp256k1);
            }
            return result;
        }();

        // initialize a bridge from a BridgeDef
        auto initBridge = [&mcEnv, &scEnv, &st](BridgeDef& bd) {
            bd.initBridge(mcEnv, scEnv);
            st->a_.spendFee(bd.doorA, 2);
            st->b_.spendFee(bd.doorB, 2);
        };

        // create XRP -> XRP bridge
        // ------------------------
        BridgeDef xrp_b{
            doorXRPLocking,
            xrpIssue(),
            Account::master,
            xrpIssue(),
            XRP(1),
            XRP(20),
            quorum,
            signers,
            Json::nullValue};

        initBridge(xrp_b);

        // create USD -> USD bridge
        // ------------------------
        BridgeDef usd_b{
            doorUSDLocking,
            usdLocking,
            doorUSDIssuing,
            usdIssuing,
            XRP(1),
            XRP(20),
            quorum,
            signers,
            Json::nullValue};

        initBridge(usd_b);

        // try a single account create + transfer to validate the simulation
        // engine. Do the transfer 8 time steps after the account create, to
        // give  time enough for ua[0] to be funded now so it can reserve
        // the claimID
        // -----------------------------------------------------------------
        ac(0, st, xrp_b, {a[0], ua[0], XRP(777), xrp_b.reward, true});
        xfer(8, st, xrp_b, {a[0], ua[0], a[2], XRP(3), true});
        runSimulation(st);

        // try the same thing in the other direction
        // -----------------------------------------
        ac(0, st, xrp_b, {a[0], ua[0], XRP(777), xrp_b.reward, false});
        xfer(8, st, xrp_b, {a[0], ua[0], a[2], XRP(3), false});
        runSimulation(st);

        // run multiple XRP transfers
        // --------------------------
        xfer(0, st, xrp_b, {a[0], a[0], a[1], XRP(6), true, WithClaim::no});
        xfer(1, st, xrp_b, {a[0], a[0], a[1], XRP(8), false, WithClaim::no});
        xfer(1, st, xrp_b, {a[1], a[1], a[1], XRP(1), true});
        xfer(2, st, xrp_b, {a[0], a[0], a[1], XRP(3), false});
        xfer(2, st, xrp_b, {a[1], a[1], a[1], XRP(5), false});
        xfer(2, st, xrp_b, {a[0], a[0], a[1], XRP(7), false, WithClaim::no});
        xfer(2, st, xrp_b, {a[1], a[1], a[1], XRP(9), true});
        runSimulation(st);

        // run one USD transfer
        // --------------------
        xfer(0, st, usd_b, {a[0], a[1], a[2], usdLocking(3), true});
        runSimulation(st);

        // run multiple USD transfers
        // --------------------------
        xfer(0, st, usd_b, {a[0], a[0], a[1], usdLocking(6), true});
        xfer(1, st, usd_b, {a[0], a[0], a[1], usdIssuing(8), false});
        xfer(1, st, usd_b, {a[1], a[1], a[1], usdLocking(1), true});
        xfer(2, st, usd_b, {a[0], a[0], a[1], usdIssuing(3), false});
        xfer(2, st, usd_b, {a[1], a[1], a[1], usdIssuing(5), false});
        xfer(2, st, usd_b, {a[0], a[0], a[1], usdIssuing(7), false});
        xfer(2, st, usd_b, {a[1], a[1], a[1], usdLocking(9), true});
        runSimulation(st);

        // run mixed transfers
        // -------------------
        xfer(0, st, xrp_b, {a[0], a[0], a[0], XRP(1), true});
        xfer(0, st, usd_b, {a[1], a[3], a[3], usdIssuing(3), false});
        xfer(0, st, usd_b, {a[3], a[2], a[1], usdIssuing(5), false});

        xfer(1, st, xrp_b, {a[0], a[0], a[0], XRP(4), false});
        xfer(1, st, xrp_b, {a[1], a[1], a[0], XRP(8), true});
        xfer(1, st, usd_b, {a[4], a[1], a[1], usdLocking(7), true});

        xfer(3, st, xrp_b, {a[1], a[1], a[0], XRP(7), true});
        xfer(3, st, xrp_b, {a[0], a[4], a[3], XRP(2), false});
        xfer(3, st, xrp_b, {a[1], a[1], a[0], XRP(9), true});
        xfer(3, st, usd_b, {a[3], a[1], a[1], usdIssuing(11), false});
        runSimulation(st);

        // run multiple account create to stress attestation batching
        // ----------------------------------------------------------
        ac(0, st, xrp_b, {a[0], ua[1], XRP(301), xrp_b.reward, true});
        ac(0, st, xrp_b, {a[1], ua[2], XRP(302), xrp_b.reward, true});
        ac(1, st, xrp_b, {a[0], ua[3], XRP(303), xrp_b.reward, true});
        ac(2, st, xrp_b, {a[1], ua[4], XRP(304), xrp_b.reward, true});
        ac(3, st, xrp_b, {a[0], ua[5], XRP(305), xrp_b.reward, true});
        ac(4, st, xrp_b, {a[1], ua[6], XRP(306), xrp_b.reward, true});
        ac(6, st, xrp_b, {a[0], ua[7], XRP(307), xrp_b.reward, true});
        ac(7, st, xrp_b, {a[2], ua[8], XRP(308), xrp_b.reward, true});
        ac(9, st, xrp_b, {a[0], ua[9], XRP(309), xrp_b.reward, true});
        ac(9, st, xrp_b, {a[0], ua[9], XRP(309), xrp_b.reward, true});
        ac(10, st, xrp_b, {a[0], ua[10], XRP(310), xrp_b.reward, true});
        ac(12, st, xrp_b, {a[0], ua[11], XRP(311), xrp_b.reward, true});
        ac(12, st, xrp_b, {a[3], ua[12], XRP(312), xrp_b.reward, true});
        ac(12, st, xrp_b, {a[4], ua[13], XRP(313), xrp_b.reward, true});
        ac(12, st, xrp_b, {a[3], ua[14], XRP(314), xrp_b.reward, true});
        ac(12, st, xrp_b, {a[6], ua[15], XRP(315), xrp_b.reward, true});
        ac(13, st, xrp_b, {a[7], ua[16], XRP(316), xrp_b.reward, true});
        ac(15, st, xrp_b, {a[3], ua[17], XRP(317), xrp_b.reward, true});
        runSimulation(st, true);  // balances verification working now.
    }

    void
    run() override
    {
        testXChainSimulation();
    }
};

BEAST_DEFINE_TESTSUITE(XChain, app, xrpl);
BEAST_DEFINE_TESTSUITE(XChainSim, app, xrpl);

}  // namespace xrpl::test
