diff --git a/src/ripple/app/tx/impl/Clawback.cpp b/src/ripple/app/tx/impl/Clawback.cpp index 2b4689bbda1..098eeacb4aa 100644 --- a/src/ripple/app/tx/impl/Clawback.cpp +++ b/src/ripple/app/tx/impl/Clawback.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -33,23 +34,44 @@ Clawback::preflight(PreflightContext const& ctx) if (!ctx.rules.enabled(featureClawback)) return temDISABLED; + auto const mptHolder = ctx.tx[~sfMPTokenHolder]; + STAmount const clawAmount = ctx.tx[sfAmount]; + if ((mptHolder || clawAmount.isMPT()) && + !ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; + if (!mptHolder && clawAmount.isMPT()) + return temMALFORMED; + + if (mptHolder && !clawAmount.isMPT()) + return temMALFORMED; + if (ctx.tx.getFlags() & tfClawbackMask) return temINVALID_FLAG; AccountID const issuer = ctx.tx[sfAccount]; - STAmount const clawAmount = ctx.tx[sfAmount]; - - if (clawAmount.isMPT()) - return temMPT_NOT_SUPPORTED; - // The issuer field is used for the token holder instead - AccountID const& holder = clawAmount.getIssuer(); + // The issuer field is used for the token holder if asset is IOU + AccountID const& holder = + clawAmount.isMPT() ? *mptHolder : clawAmount.getIssuer(); - if (issuer == holder || isXRP(clawAmount) || clawAmount <= beast::zero) - return temBAD_AMOUNT; + if (clawAmount.isMPT()) + { + if (issuer == holder) + return temMALFORMED; + + if (clawAmount.mpt() > MPTAmount{maxMPTokenAmount} || + clawAmount <= beast::zero) + return temBAD_AMOUNT; + } + else + { + if (issuer == holder || isXRP(clawAmount) || clawAmount <= beast::zero) + return temBAD_AMOUNT; + } return preflight2(ctx); } @@ -59,7 +81,8 @@ Clawback::preclaim(PreclaimContext const& ctx) { AccountID const issuer = ctx.tx[sfAccount]; STAmount const clawAmount = ctx.tx[sfAmount]; - AccountID const& holder = clawAmount.getIssuer(); + AccountID const& holder = + clawAmount.isMPT() ? ctx.tx[sfMPTokenHolder] : clawAmount.getIssuer(); auto const sleIssuer = ctx.view.read(keylet::account(issuer)); auto const sleHolder = ctx.view.read(keylet::account(holder)); @@ -69,46 +92,75 @@ Clawback::preclaim(PreclaimContext const& ctx) if (sleHolder->isFieldPresent(sfAMMID)) return tecAMM_ACCOUNT; - std::uint32_t const issuerFlagsIn = sleIssuer->getFieldU32(sfFlags); - - // If AllowTrustLineClawback is not set or NoFreeze is set, return no - // permission - if (!(issuerFlagsIn & lsfAllowTrustLineClawback) || - (issuerFlagsIn & lsfNoFreeze)) - return tecNO_PERMISSION; - - auto const sleRippleState = - ctx.view.read(keylet::line(holder, issuer, clawAmount.getCurrency())); - if (!sleRippleState) - return tecNO_LINE; - - STAmount const balance = (*sleRippleState)[sfBalance]; - - // If balance is positive, issuer must have higher address than holder - if (balance > beast::zero && issuer < holder) - return tecNO_PERMISSION; - - // If balance is negative, issuer must have lower address than holder - if (balance < beast::zero && issuer > holder) - return tecNO_PERMISSION; - - // At this point, we know that issuer and holder accounts - // are correct and a trustline exists between them. - // - // Must now explicitly check the balance to make sure - // available balance is non-zero. - // - // We can't directly check the balance of trustline because - // the available balance of a trustline is prone to new changes (eg. - // XLS-34). So we must use `accountHolds`. - if (accountHolds( - ctx.view, - holder, - clawAmount.getCurrency(), - issuer, - fhIGNORE_FREEZE, - ctx.j) <= beast::zero) - return tecINSUFFICIENT_FUNDS; + if (clawAmount.isMPT()) + { + auto const issuanceKey = + keylet::mptIssuance(clawAmount.mptIssue().mpt()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + if (!((*sleIssuance)[sfFlags] & lsfMPTCanClawback)) + return tecNO_PERMISSION; + + if (sleIssuance->getAccountID(sfIssuer) != issuer) + return tecNO_PERMISSION; + + if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, holder))) + return tecOBJECT_NOT_FOUND; + + if (accountHolds( + ctx.view, + holder, + clawAmount.mptIssue(), + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + ctx.j) <= beast::zero) + return tecINSUFFICIENT_FUNDS; + } + else + { + std::uint32_t const issuerFlagsIn = sleIssuer->getFieldU32(sfFlags); + + // If AllowTrustLineClawback is not set or NoFreeze is set, return no + // permission + if (!(issuerFlagsIn & lsfAllowTrustLineClawback) || + (issuerFlagsIn & lsfNoFreeze)) + return tecNO_PERMISSION; + + auto const sleRippleState = ctx.view.read( + keylet::line(holder, issuer, clawAmount.getCurrency())); + if (!sleRippleState) + return tecNO_LINE; + + STAmount const& balance = (*sleRippleState)[sfBalance]; + + // If balance is positive, issuer must have higher address than holder + if (balance > beast::zero && issuer < holder) + return tecNO_PERMISSION; + + // If balance is negative, issuer must have lower address than holder + if (balance < beast::zero && issuer > holder) + return tecNO_PERMISSION; + + // At this point, we know that issuer and holder accounts + // are correct and a trustline exists between them. + // + // Must now explicitly check the balance to make sure + // available balance is non-zero. + // + // We can't directly check the balance of trustline because + // the available balance of a trustline is prone to new changes (eg. + // XLS-34). So we must use `accountHolds`. + if (accountHolds( + ctx.view, + holder, + clawAmount.getCurrency(), + issuer, + fhIGNORE_FREEZE, + ctx.j) <= beast::zero) + return tecINSUFFICIENT_FUNDS; + } return tesSUCCESS; } @@ -118,9 +170,27 @@ Clawback::doApply() { AccountID const& issuer = account_; STAmount clawAmount = ctx_.tx[sfAmount]; - AccountID const holder = clawAmount.getIssuer(); // cannot be reference + AccountID const holder = clawAmount.isMPT() + ? ctx_.tx[sfMPTokenHolder] + : clawAmount.getIssuer(); // cannot be reference because clawAmount is + // modified beblow + + if (clawAmount.isMPT()) + { + // Get the spendable balance. Must use `accountHolds`. + STAmount const spendableAmount = accountHolds( + view(), + holder, + clawAmount.mptIssue(), + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + j_); + + return rippleMPTCredit( + view(), holder, issuer, std::min(spendableAmount, clawAmount), j_); + } - // Replace the `issuer` field with issuer's account + // Replace the `issuer` field with issuer's account if asset is IOU clawAmount.setIssuer(issuer); if (holder == issuer) return tecINTERNAL; diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index 51b4d142a58..883495c83ad 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -753,6 +753,9 @@ ValidClawback::visitEntry( { if (before && before->getType() == ltRIPPLE_STATE) trustlinesChanged++; + + if (before && before->getType() == ltMPTOKEN) + mptokensChanged++; } bool @@ -775,18 +778,28 @@ ValidClawback::finalize( return false; } - AccountID const issuer = tx.getAccountID(sfAccount); - STAmount const amount = tx.getFieldAmount(sfAmount); - AccountID const& holder = amount.getIssuer(); - STAmount const holderBalance = accountHolds( - view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j); - - if (holderBalance.signum() < 0) + if (mptokensChanged > 1) { JLOG(j.fatal()) - << "Invariant failed: trustline balance is negative"; + << "Invariant failed: more than one mptokens changed."; return false; } + + if (trustlinesChanged == 1) + { + AccountID const issuer = tx.getAccountID(sfAccount); + STAmount const& amount = tx.getFieldAmount(sfAmount); + AccountID const& holder = amount.getIssuer(); + STAmount const holderBalance = accountHolds( + view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j); + + if (holderBalance.signum() < 0) + { + JLOG(j.fatal()) + << "Invariant failed: trustline balance is negative"; + return false; + } + } } else { @@ -796,6 +809,13 @@ ValidClawback::finalize( "despite failure of the transaction."; return false; } + + if (mptokensChanged != 0) + { + JLOG(j.fatal()) << "Invariant failed: some mptokens were changed " + "despite failure of the transaction."; + return false; + } } return true; diff --git a/src/ripple/app/tx/impl/InvariantCheck.h b/src/ripple/app/tx/impl/InvariantCheck.h index d30cc413b2d..0422d6a7cba 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.h +++ b/src/ripple/app/tx/impl/InvariantCheck.h @@ -401,6 +401,7 @@ class NFTokenCountTracking class ValidClawback { std::uint32_t trustlinesChanged = 0; + std::uint32_t mptokensChanged = 0; public: void diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index 9e6af0f36d2..c0356bbb310 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -78,6 +78,9 @@ hasExpired(ReadView const& view, std::optional const& exp); /** Controls the treatment of frozen account balances */ enum FreezeHandling { fhIGNORE_FREEZE, fhZERO_IF_FROZEN }; +/** Controls the treatment of unauthorized MPT balances */ +enum AuthHandling { ahIGNORE_AUTH, ahZERO_IF_UNAUTHORIZED }; + [[nodiscard]] bool isGlobalFrozen(ReadView const& view, AccountID const& issuer); @@ -142,6 +145,15 @@ accountHolds( FreezeHandling zeroIfFrozen, beast::Journal j); +[[nodiscard]] STAmount +accountHolds( + ReadView const& view, + AccountID const& account, + MPTIssue const& issue, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j); + // Returns the amount an account can spend of the currency type saDefault, or // returns saDefault if this account is the issuer of the currency in // question. Should be used in favor of accountHolds when questioning how much diff --git a/src/ripple/ledger/impl/View.cpp b/src/ripple/ledger/impl/View.cpp index de14d12f160..50e58ab86e3 100644 --- a/src/ripple/ledger/impl/View.cpp +++ b/src/ripple/ledger/impl/View.cpp @@ -305,6 +305,47 @@ accountHolds( view, account, issue.currency, issue.account, zeroIfFrozen, j); } +STAmount +accountHolds( + ReadView const& view, + AccountID const& account, + MPTIssue const& issue, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j) +{ + STAmount amount; + + auto const sleMpt = view.read(keylet::mptoken(issue.mpt(), account)); + if (!sleMpt) + amount.clear(issue); + else if (zeroIfFrozen == fhZERO_IF_FROZEN && isFrozen(view, account, issue)) + amount.clear(issue); + else + { + auto const amt = sleMpt->getFieldU64(sfMPTAmount); + auto const locked = sleMpt->getFieldU64(sfLockedAmount); + if (amt > locked) + amount = STAmount{issue, amt - locked}; + + // only if auth check is needed, as it needs to do an additional read + // operation + if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED) + { + auto const sleIssuance = + view.read(keylet::mptIssuance(issue.getMptID())); + + // if auth is enabled on the issuance and mpt is not authorized, + // clear amount + if (sleIssuance && sleIssuance->isFlag(lsfMPTRequireAuth) && + !sleMpt->isFlag(lsfMPTAuthorized)) + amount.clear(issue); + } + } + + return amount; +} + STAmount accountFunds( ReadView const& view, diff --git a/src/ripple/protocol/impl/STTx.cpp b/src/ripple/protocol/impl/STTx.cpp index 2f5f3458ffc..9995acd1a76 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -535,19 +535,21 @@ isMemoOkay(STObject const& st, std::string& reason) } // Ensure all account fields are 160-bits and that MPT amount is only passed -// to Payment tx (until MPT is supported in more tx) +// to Payment or Clawback tx (until MPT is supported in more tx) static bool isAccountAndMPTFieldOkay(STObject const& st) { auto const txType = st[~sfTransactionType]; - bool const isPaymentTx = txType && safe_cast(*txType) == ttPAYMENT; + static std::unordered_set const mptAmountTx{ttPAYMENT, ttCLAWBACK}; + bool const isMPTAmountAllowed = txType && + (mptAmountTx.find(safe_cast(*txType)) != mptAmountTx.end()); for (int i = 0; i < st.getCount(); ++i) { auto t = dynamic_cast(st.peekAtPIndex(i)); if (t && t->isDefault()) return false; auto amt = dynamic_cast(st.peekAtPIndex(i)); - if (amt && amt->isMPT() && !isPaymentTx) + if (amt && amt->isMPT() && !isMPTAmountAllowed) return false; } diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 5e8f5a2ce0d..2491a1a164b 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -375,6 +375,7 @@ TxFormats::TxFormats() ttCLAWBACK, { {sfAmount, soeREQUIRED}, + {sfMPTokenHolder, soeOPTIONAL}, }, commonFields); diff --git a/src/test/app/Clawback_test.cpp b/src/test/app/Clawback_test.cpp index 630e4836d8e..3408c06d5ec 100644 --- a/src/test/app/Clawback_test.cpp +++ b/src/test/app/Clawback_test.cpp @@ -965,6 +965,7 @@ class Clawback_test : public beast::unit_test::suite using namespace test::jtx; FeatureBitset const all{supported_amendments()}; + testWithFeats(all - featureMPTokensV1); testWithFeats(all); } }; diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 574b04e8639..d2a1d1e5e55 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -20,6 +20,7 @@ #include #include #include +#include namespace ripple { @@ -858,6 +859,246 @@ class MPToken_test : public beast::unit_test::suite meta[jss::mpt_issuance_id] == to_string(mptAlice.issuanceID())); } + void + testClawbackValidation(FeatureBitset features) + { + testcase("MPT clawback validations"); + using namespace test::jtx; + + // Make sure clawback cannot work when featureMPTokensV1 is disabled + { + Env env(*this, features - featureMPTokensV1); + Account const alice{"alice"}; + Account const bob{"bob"}; + + env.fund(XRP(1000), alice, bob); + env.close(); + + auto const USD = alice["USD"]; + auto const mpt = ripple::test::jtx::MPT( + alice.name(), std::make_pair(env.seq(alice), alice.id())); + + env(claw(alice, bob["USD"](5), bob), ter(temDISABLED)); + env.close(); + + env(claw(alice, mpt(5)), ter(temDISABLED)); + env.close(); + + env(claw(alice, mpt(5), bob), ter(temDISABLED)); + env.close(); + } + + // Test preflight + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + env.fund(XRP(1000), alice, bob); + env.close(); + + auto const USD = alice["USD"]; + auto const mpt = ripple::test::jtx::MPT( + alice.name(), std::make_pair(env.seq(alice), alice.id())); + + // clawing back IOU from a MPT holder fails + env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED)); + env.close(); + + // clawing back MPT without specifying a holder fails + env(claw(alice, mpt(5)), ter(temMALFORMED)); + env.close(); + + // clawing back zero amount fails + env(claw(alice, mpt(0), bob), ter(temBAD_AMOUNT)); + env.close(); + + // alice can't claw back from herself + env(claw(alice, mpt(5), alice), ter(temMALFORMED)); + env.close(); + + // TODO: uncomment after stamount changes + // env(claw(alice, mpt(maxMPTokenAmount), bob), ter(temBAD_AMOUNT)); + // env.close(); + } + + // Preclaim - clawback fails when MPTCanClawback is disabled on issuance + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // enable asfAllowTrustLineClawback for alice + env(fset(alice, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(alice, asfAllowTrustLineClawback)); + + // Create issuance without enabling clawback + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &bob}); + + mptAlice.pay(alice, bob, 100); + + // alice cannot clawback before she didn't enable MPTCanClawback + // asfAllowTrustLineClawback has no effect + mptAlice.claw(alice, bob, 1, tecNO_PERMISSION); + } + + // Preclaim - test various scenarios + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + env.fund(XRP(1000), carol); + env.close(); + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + auto const fakeMpt = ripple::test::jtx::MPT( + alice.name(), std::make_pair(env.seq(alice), alice.id())); + + // issuer tries to clawback MPT where issuance doesn't exist + env(claw(alice, fakeMpt(5), bob), ter(tecOBJECT_NOT_FOUND)); + env.close(); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback}); + + // alice tries to clawback from someone who doesn't have MPToken + mptAlice.claw(alice, bob, 1, tecOBJECT_NOT_FOUND); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // clawback fails because bob currently has a balance of zero + mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + // carol fails tries to clawback from bob because he is not the + // issuer + mptAlice.claw(carol, bob, 1, tecNO_PERMISSION); + } + } + + void + testClawback(FeatureBitset features) + { + testcase("MPT Clawback"); + using namespace test::jtx; + + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + mptAlice.claw(alice, bob, 1); + + mptAlice.claw(alice, bob, 1000); + + // clawback fails because bob currently has a balance of zero + mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS); + } + + // Test that globally locked funds can be clawed + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanLock | tfMPTCanClawback}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + mptAlice.set({.account = &alice, .flags = tfMPTLock}); + + mptAlice.claw(alice, bob, 100); + } + + // Test that individually locked funds can be clawed + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanLock | tfMPTCanClawback}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + mptAlice.set( + {.account = &alice, .holder = &bob, .flags = tfMPTLock}); + + mptAlice.claw(alice, bob, 100); + } + + // Test that unauthorized funds can be clawed back + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanClawback | tfMPTRequireAuth}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // alice authorizes bob + mptAlice.authorize({.account = &alice, .holder = &bob}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + // alice unauthorizes bob + mptAlice.authorize( + {.account = &alice, .holder = &bob, .flags = tfMPTUnauthorize}); + + mptAlice.claw(alice, bob, 100); + } + } + public: void run() override @@ -881,6 +1122,10 @@ class MPToken_test : public beast::unit_test::suite testSetValidation(all); testSetEnabled(all); + // MPT clawback + testClawbackValidation(all); + testClawback(all); + // Test Direct Payment testPayment(all); diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 49ccf9bd1b7..8c15f72c277 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -342,6 +342,31 @@ MPTTester::pay( } } +void +MPTTester::claw( + Account const& issuer, + Account const& holder, + std::uint64_t amount, + std::optional err) +{ + assert(mpt_); + auto const issuerAmt = getAmount(issuer); + auto const holderAmt = getAmount(holder); + if (err) + env_(jtx::claw(issuer, mpt(amount), holder), ter(*err)); + else + env_(jtx::claw(issuer, mpt(amount), holder)); + if (env_.ter() != tesSUCCESS) + amount = 0; + if (close_) + env_.close(); + + env_.require( + mptpay(*this, issuer, issuerAmt - std::min(holderAmt, amount))); + env_.require( + mptpay(*this, holder, holderAmt - std::min(holderAmt, amount))); +} + PrettyAmount MPTTester::mpt(std::uint64_t amount) const { diff --git a/src/test/jtx/impl/trust.cpp b/src/test/jtx/impl/trust.cpp index cce4657e025..34207746213 100644 --- a/src/test/jtx/impl/trust.cpp +++ b/src/test/jtx/impl/trust.cpp @@ -60,13 +60,19 @@ trust( } Json::Value -claw(Account const& account, STAmount const& amount) +claw( + Account const& account, + STAmount const& amount, + std::optional const& mptHolder) { Json::Value jv; jv[jss::Account] = account.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); jv[jss::TransactionType] = jss::Clawback; + if (mptHolder) + jv[sfMPTokenHolder.jsonName] = mptHolder->human(); + return jv; } diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 12b384c2902..6423c5c5976 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -189,6 +189,13 @@ class MPTTester std::uint64_t amount, std::optional err = std::nullopt); + void + claw( + Account const& issuer, + Account const& holder, + std::uint64_t amount, + std::optional err = std::nullopt); + PrettyAmount mpt(std::uint64_t amount) const; diff --git a/src/test/jtx/trust.h b/src/test/jtx/trust.h index 5b6dd78b3cd..a0c2d8d645e 100644 --- a/src/test/jtx/trust.h +++ b/src/test/jtx/trust.h @@ -41,7 +41,10 @@ trust( std::uint32_t flags); Json::Value -claw(Account const& account, STAmount const& amount); +claw( + Account const& account, + STAmount const& amount, + std::optional const& mptHolder = std::nullopt); } // namespace jtx } // namespace test