diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 58ebbe69cc..e3efe8fec2 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -145,7 +145,8 @@ enum LedgerSpecificFlags { 0x10000000, // True, reject new paychans lsfDisallowIncomingTrustline = 0x20000000, // True, reject new trustlines (only if no issued assets) - // 0x40000000 is available + lsfAllowTrustLineLocking = + 0x40000000, // True, enable trustline locking lsfAllowTrustLineClawback = 0x80000000, // True, enable clawback diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index c66d273254..f1e34463b6 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -703,6 +703,12 @@ isXRP(STAmount const& amount) return amount.native(); } +bool +canAdd(STAmount const& amt1, STAmount const& amt2); + +bool +canSubtract(STAmount const& amt1, STAmount const& amt2); + // Since `canonicalize` does not have access to a ledger, this is needed to put // the low-level routine stAmountCanonicalize on an amendment switch. Only // transactions need to use this switchover. Outside of a transaction it's safe diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 4483d6251a..f71153cddb 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -360,6 +360,7 @@ enum TECcodes : TERUnderlyingType { tecWRONG_ASSET = 194, tecLIMIT_EXCEEDED = 195, tecPSEUDO_ACCOUNT = 196, + tecPRECISION_LOSS = 197, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 80f6a78727..2ce7a6b6a8 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -92,6 +92,7 @@ constexpr std::uint32_t asfDisallowIncomingCheck = 13; constexpr std::uint32_t asfDisallowIncomingPayChan = 14; constexpr std::uint32_t asfDisallowIncomingTrustline = 15; constexpr std::uint32_t asfAllowTrustLineClawback = 16; +constexpr std::uint32_t asfAllowTrustLineLocking = 17; // OfferCreate flags: constexpr std::uint32_t tfPassive = 0x00010000; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index df4af23e96..1be0af5d01 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -32,6 +32,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 06fe9d45bd..46c6e60db3 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -354,6 +354,8 @@ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({ {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, {sfDestinationNode, soeOPTIONAL}, + {sfTransferRate, soeOPTIONAL}, + {sfIssuerNode, soeOPTIONAL}, })) /** A ledger object describing a single unidirectional XRP payment channel. @@ -405,6 +407,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({ {sfAssetScale, soeDEFAULT}, {sfMaximumAmount, soeOPTIONAL}, {sfOutstandingAmount, soeREQUIRED}, + {sfLockedAmount, soeOPTIONAL}, {sfMPTokenMetadata, soeOPTIONAL}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, @@ -418,6 +421,7 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({ {sfAccount, soeREQUIRED}, {sfMPTokenIssuanceID, soeREQUIRED}, {sfMPTAmount, soeDEFAULT}, + {sfLockedAmount, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 2053ac94bb..537fcae479 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -144,6 +144,7 @@ TYPED_SFIELD(sfOutstandingAmount, UINT64, 25, SField::sMD_BaseTen|SFie TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SField::sMD_Default) TYPED_SFIELD(sfIssuerNode, UINT64, 27) TYPED_SFIELD(sfSubjectNode, UINT64, 28) +TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SField::sMD_Default) // 128-bit TYPED_SFIELD(sfEmailHash, UINT128, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 6992410e4c..1d59e71850 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -44,7 +44,7 @@ TRANSACTION(ttPAYMENT, 0, Payment, Delegation::delegatable, ({ /** This transaction type creates an escrow object. */ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, Delegation::delegatable, ({ {sfDestination, soeREQUIRED}, - {sfAmount, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, {sfCondition, soeOPTIONAL}, {sfCancelAfter, soeOPTIONAL}, {sfFinishAfter, soeOPTIONAL}, diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 9dff4cc4f3..67a045fa58 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -391,6 +391,7 @@ JSS(load_fee); // out: LoadFeeTrackImp, NetworkOPs JSS(local); // out: resource/Logic.h JSS(local_txs); // out: GetCounts JSS(local_static_keys); // out: ValidatorList +JSS(locked); // out: GatewayBalances JSS(low); // out: BookChanges JSS(lowest_sequence); // out: AccountInfo JSS(lowest_ticket); // out: AccountInfo diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 02de5d4c58..845ad6481a 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -506,6 +506,157 @@ getRate(STAmount const& offerOut, STAmount const& offerIn) return 0; } +/** + * @brief Safely checks if two STAmount values can be added without overflow, + * underflow, or precision loss. + * + * This function determines whether the addition of two STAmount objects is + * safe, depending on their type: + * - For XRP amounts, it checks for integer overflow and underflow. + * - For IOU amounts, it checks for acceptable precision loss. + * - For MPT amounts, it checks for overflow and underflow within 63-bit signed + * integer limits. + * - If either amount is zero, addition is always considered safe. + * - If the amounts are of different currencies or types, addition is not + * allowed. + * + * @param a The first STAmount to add. + * @param b The second STAmount to add. + * @return true if the addition is safe; false otherwise. + */ +bool +canAdd(STAmount const& a, STAmount const& b) +{ + // cannot add different currencies + if (!areComparable(a, b)) + return false; + + // special case: adding anything to zero is always fine + if (a == beast::zero || b == beast::zero) + return true; + + // XRP case (overflow & underflow check) + if (isXRP(a) && isXRP(b)) + { + XRPAmount A = a.xrp(); + XRPAmount B = b.xrp(); + + if ((B > XRPAmount{0} && + A > XRPAmount{std::numeric_limits::max()} - + B) || + (B < XRPAmount{0} && + A < XRPAmount{std::numeric_limits::min()} - + B)) + { + return false; + } + return true; + } + + // IOU case (precision check) + if (a.holds() && b.holds()) + { + static STAmount const one{IOUAmount{1, 0}, noIssue()}; + static STAmount const maxLoss{IOUAmount{1, -4}, noIssue()}; + STAmount lhs = divide((a - b) + b, a, noIssue()) - one; + STAmount rhs = divide((b - a) + a, b, noIssue()) - one; + return ((rhs.negative() ? -rhs : rhs) + + (lhs.negative() ? -lhs : lhs)) <= maxLoss; + } + + // MPT (overflow & underflow check) + if (a.holds() && b.holds()) + { + MPTAmount A = a.mpt(); + MPTAmount B = b.mpt(); + if ((B > MPTAmount{0} && + A > MPTAmount{std::numeric_limits::max()} - + B) || + (B < MPTAmount{0} && + A < MPTAmount{std::numeric_limits::min()} - + B)) + { + return false; + } + + return true; + } + return false; +} + +/** + * @brief Determines if it is safe to subtract one STAmount from another. + * + * This function checks whether subtracting amount `b` from amount `a` is valid, + * considering currency compatibility and underflow conditions for specific + * types. + * + * - Subtracting zero is always allowed. + * - Subtraction is only allowed between comparable currencies. + * - For XRP amounts, ensures no underflow or overflow occurs. + * - For IOU amounts, subtraction is always allowed (no underflow). + * - For MPT amounts, ensures no underflow or overflow occurs. + * + * @param a The minuend (amount to subtract from). + * @param b The subtrahend (amount to subtract). + * @return true if subtraction is allowed, false otherwise. + */ +bool +canSubtract(STAmount const& a, STAmount const& b) +{ + // Cannot subtract different currencies + if (!areComparable(a, b)) + return false; + + // Special case: subtracting zero is always fine + if (b == beast::zero) + return true; + + // XRP case (underflow & overflow check) + if (isXRP(a) && isXRP(b)) + { + XRPAmount A = a.xrp(); + XRPAmount B = b.xrp(); + // Check for underflow + if (B > XRPAmount{0} && A < B) + return false; + + // Check for overflow + if (B < XRPAmount{0} && + A > XRPAmount{std::numeric_limits::max()} + + B) + return false; + + return true; + } + + // IOU case (no underflow) + if (a.holds() && b.holds()) + { + return true; + } + + // MPT case (underflow & overflow check) + if (a.holds() && b.holds()) + { + MPTAmount A = a.mpt(); + MPTAmount B = b.mpt(); + + // Underflow check + if (B > MPTAmount{0} && A < B) + return false; + + // Overflow check + if (B < MPTAmount{0} && + A > MPTAmount{std::numeric_limits::max()} + + B) + return false; + return true; + } + + return false; +} + void STAmount::setJson(Json::Value& elem) const { diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 68125fab83..18bf0e2936 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -126,6 +126,7 @@ transResults() MAKE_ERROR(tecWRONG_ASSET, "Wrong asset given."), MAKE_ERROR(tecLIMIT_EXCEEDED, "Limit exceeded."), MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."), + MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 7211d37730..2ee9e5f1f3 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -3651,10 +3651,10 @@ private: // Can't pay into AMM with escrow. testAMM([&](AMM& ammAlice, Env& env) { auto const baseFee = env.current()->fees().base; - env(escrow(carol, ammAlice.ammAccount(), XRP(1)), - condition(cb1), - finish_time(env.now() + 1s), - cancel_time(env.now() + 2s), + env(escrow::create(carol, ammAlice.ammAccount(), XRP(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), fee(baseFee * 150), ter(tecNO_PERMISSION)); }); diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index 03283e4611..1ac0256dcb 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -335,26 +335,11 @@ public: env(check::cancel(becky, checkId)); env.close(); - // Lambda to create an escrow. - auto escrowCreate = [](jtx::Account const& account, - jtx::Account const& to, - STAmount const& amount, - NetClock::time_point const& cancelAfter) { - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Account] = account.human(); - jv[jss::Destination] = to.human(); - jv[jss::Amount] = amount.getJson(JsonOptions::none); - jv[sfFinishAfter.jsonName] = - cancelAfter.time_since_epoch().count() + 1; - jv[sfCancelAfter.jsonName] = - cancelAfter.time_since_epoch().count() + 2; - return jv; - }; - using namespace std::chrono_literals; std::uint32_t const escrowSeq{env.seq(alice)}; - env(escrowCreate(alice, becky, XRP(333), env.now() + 2s)); + env(escrow::create(alice, becky, XRP(333)), + escrow::finish_time(env.now() + 3s), + escrow::cancel_time(env.now() + 4s)); env.close(); // alice and becky should be unable to delete their accounts because @@ -366,17 +351,39 @@ public: // Now cancel the escrow, but create a payment channel between // alice and becky. - // Lambda to cancel an escrow. - auto escrowCancel = - [](Account const& account, Account const& from, std::uint32_t seq) { - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCancel; - jv[jss::Account] = account.human(); - jv[sfOwner.jsonName] = from.human(); - jv[sfOfferSequence.jsonName] = seq; - return jv; - }; - env(escrowCancel(becky, alice, escrowSeq)); + bool const withTokenEscrow = + env.current()->rules().enabled(featureTokenEscrow); + if (withTokenEscrow) + { + Account const gw1("gw1"); + Account const carol("carol"); + auto const USD = gw1["USD"]; + env.fund(XRP(100000), carol, gw1); + env(fset(gw1, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10000), carol); + env.close(); + env(pay(gw1, carol, USD(100))); + env.close(); + + std::uint32_t const escrowSeq{env.seq(carol)}; + env(escrow::create(carol, becky, USD(1)), + escrow::finish_time(env.now() + 3s), + escrow::cancel_time(env.now() + 4s)); + env.close(); + + incLgrSeqForAccDel(env, gw1); + + env(acctdelete(gw1, becky), + fee(acctDelFee), + ter(tecHAS_OBLIGATIONS)); + env.close(); + + env(escrow::cancel(becky, carol, escrowSeq)); + env.close(); + } + + env(escrow::cancel(becky, alice, escrowSeq)); env.close(); Keylet const alicePayChanKey{ diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index c8dc3c00eb..6f314e3a79 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -714,12 +714,12 @@ struct DepositPreauth_test : public beast::unit_test::suite if (!supportsPreauth) { auto const seq1 = env.seq(alice); - env(escrow(alice, becky, XRP(100)), - finish_time(env.now() + 1s)); + env(escrow::create(alice, becky, XRP(100)), + escrow::finish_time(env.now() + 1s)); env.close(); // Failed as rule is disabled - env(finish(gw, alice, seq1), + env(escrow::finish(gw, alice, seq1), fee(1500), ter(tecNO_PERMISSION)); env.close(); @@ -1387,12 +1387,13 @@ struct DepositPreauth_test : public beast::unit_test::suite env.close(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s)); env.close(); // zelda can't finish escrow with invalid credentials { - env(finish(zelda, alice, seq), + env(escrow::finish(zelda, alice, seq), credentials::ids({}), ter(temMALFORMED)); env.close(); @@ -1404,14 +1405,14 @@ struct DepositPreauth_test : public beast::unit_test::suite "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E" "01E034"; - env(finish(zelda, alice, seq), + env(escrow::finish(zelda, alice, seq), credentials::ids({invalidIdx}), ter(tecBAD_CREDENTIALS)); env.close(); } { // Ledger closed, time increased, zelda can't finish escrow - env(finish(zelda, alice, seq), + env(escrow::finish(zelda, alice, seq), credentials::ids({credIdx}), fee(1500), ter(tecEXPIRED)); diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp new file mode 100644 index 0000000000..da9610f0c3 --- /dev/null +++ b/src/test/app/EscrowToken_test.cpp @@ -0,0 +1,3736 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace ripple { +namespace test { + +struct EscrowToken_test : public beast::unit_test::suite +{ + static uint64_t + mptEscrowed( + jtx::Env const& env, + jtx::Account const& account, + jtx::MPT const& mpt) + { + auto const sle = env.le(keylet::mptoken(mpt.mpt(), account)); + if (sle && sle->isFieldPresent(sfLockedAmount)) + return (*sle)[sfLockedAmount]; + return 0; + } + + static uint64_t + issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt) + { + auto const sle = env.le(keylet::mptIssuance(mpt.mpt())); + if (sle && sle->isFieldPresent(sfLockedAmount)) + return (*sle)[sfLockedAmount]; + return 0; + } + + void + issuerIOUEscrowed( + jtx::Env& env, + jtx::Account const& account, + Currency const& currency, + int const& outstanding, + int const& locked) + { + Json::Value params; + params[jss::account] = account.human(); + auto jrr = env.rpc("json", "gateway_balances", to_string(params)); + auto const result = jrr[jss::result]; + auto const actualOutstanding = + result[jss::obligations][to_string(currency)]; + BEAST_EXPECT(actualOutstanding == to_string(outstanding)); + if (locked != 0) + { + auto const actualEscrowed = + result[jss::locked][to_string(currency)]; + BEAST_EXPECT(actualEscrowed == to_string(locked)); + } + } + + void + testIOUEnablement(FeatureBitset features) + { + testcase("IOU Enablement"); + + using namespace jtx; + using namespace std::chrono; + + for (bool const withTokenEscrow : {false, true}) + { + auto const amend = + withTokenEscrow ? features : features - featureTokenEscrow; + Env env{*this, amend}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + auto const createResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(temBAD_AMOUNT); + auto const finishResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(tecNO_TARGET); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + createResult); + env.close(); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + finishResult); + env.close(); + + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + createResult); + env.close(); + env(escrow::cancel(bob, alice, seq2), finishResult); + env.close(); + } + } + + void + testIOUAllowLockingFlag(FeatureBitset features) + { + testcase("IOU Allow Locking Flag"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // Create Escrow #1 & #2 + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 3s), + fee(baseFee), + ter(tesSUCCESS)); + env.close(); + + // Clear the asfAllowTrustLineLocking flag + env(fclear(gw, asfAllowTrustLineLocking)); + env.close(); + env.require(nflags(gw, asfAllowTrustLineLocking)); + + // Cannot Create Escrow without asfAllowTrustLineLocking + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + + // Can finish the escrow created before the flag was cleared + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // Can cancel the escrow created before the flag was cleared + env(escrow::cancel(bob, alice, seq2), ter(tesSUCCESS)); + env.close(); + } + + void + testIOUCreatePreflight(FeatureBitset features) + { + testcase("IOU Create Preflight"); + using namespace test::jtx; + using namespace std::literals; + + // temBAD_FEE: Exercises invalid preflight1. + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + + env(escrow::create(alice, bob, USD(1)), + escrow::finish_time(env.now() + 1s), + fee(XRP(-1)), + ter(temBAD_FEE)); + env.close(); + } + + // temBAD_AMOUNT: amount <= 0 + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + + env(escrow::create(alice, bob, USD(-1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(temBAD_AMOUNT)); + env.close(); + } + + // temBAD_CURRENCY: badCurrency() == amount.getCurrency() + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const BAD = IOU(gw, badCurrency()); + env.fund(XRP(5000), alice, bob, gw); + + env(escrow::create(alice, bob, BAD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(temBAD_CURRENCY)); + env.close(); + } + } + + void + testIOUCreatePreclaim(FeatureBitset features) + { + testcase("IOU Create Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_PERMISSION: issuer is the same as the account + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + + env(escrow::create(gw, alice, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + // tecNO_ISSUER: Issuer does not exist + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob); + env.close(); + env.memoize(gw); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_ISSUER)); + env.close(); + } + + // tecNO_PERMISSION: asfAllowTrustLineLocking is not set + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + env(escrow::create(gw, alice, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + // tecNO_LINE: account does not have a trustline to the issuer + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_LINE)); + env.close(); + } + + // tecNO_PERMISSION: Not testable + // tecNO_PERMISSION: Not testable + // tecNO_AUTH: requireAuth + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecNO_AUTH: requireAuth + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth)); + env.trust(USD(10'000), alice, bob); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecFROZEN: account is frozen + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfSetFreeze)); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + } + + // tecFROZEN: dest is frozen + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze)); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + env(escrow::create(alice, bob, USD(10'001)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecPRECISION_LOSS + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100000000000000000), alice); + env.trust(USD(100000000000000000), bob); + env.close(); + env(pay(gw, alice, USD(10000000000000000))); + env(pay(gw, bob, USD(1))); + env.close(); + + // alice cannot create escrow for 1/10 iou - precision loss + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecPRECISION_LOSS)); + env.close(); + } + } + + void + testIOUFinishPreclaim(FeatureBitset features) + { + testcase("IOU Finish Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth)); + env(trust(gw, bobUSD(10'000)), txflags(tfSetfAuth)); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(pay(bob, gw, USD(10'000))); + env(trust(gw, bobUSD(0)), txflags(tfSetfAuth)); + env(trust(bob, USD(0))); + env.close(); + + env.trust(USD(10'000), bob); + env.close(); + + // bob cannot finish because he is not authorized + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecFROZEN: issuer has deep frozen the dest + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + + // bob cannot finish because of deep freeze + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + } + } + + void + testIOUFinishDoApply(FeatureBitset features) + { + testcase("IOU Finish Do Apply"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_LINE_INSUF_RESERVE: insufficient reserve to create line + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, gw); + env.fund(acctReserve + (incReserve - 1), bob); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice); + env.close(); + env(pay(gw, alice, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // bob cannot finish because insufficient reserve to create line + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + } + + // tecNO_LINE: alice submits; finish IOU not created + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice); + env.close(); + env(pay(gw, alice, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // alice cannot finish because bob does not have a trustline + env(escrow::finish(alice, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_LINE)); + env.close(); + } + + // tecLIMIT_EXCEEDED: alice submits; IOU Limit < balance + amount + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(1000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(5)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env.trust(USD(1), bob); + env.close(); + + // alice cannot finish because bobs limit is too low + env(escrow::finish(alice, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecLIMIT_EXCEEDED)); + env.close(); + } + + // tesSUCCESS: bob submits; IOU Limit < balance + amount + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(1000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(5)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env.trust(USD(1), bob); + env.close(); + + // bob can finish even if bobs limit is too low + auto const bobPreLimit = env.limit(bob, USD); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // bobs limit is not changed + BEAST_EXPECT(env.limit(bob, USD) == bobPreLimit); + } + } + + void + testIOUCancelPreclaim(FeatureBitset features) + { + testcase("IOU Cancel Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: account not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth)); + env(trust(gw, bobUSD(10'000)), txflags(tfSetfAuth)); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee), + ter(tesSUCCESS)); + env.close(); + + env(pay(alice, gw, USD(9'999))); + env(trust(gw, aliceUSD(0)), txflags(tfSetfAuth)); + env(trust(alice, USD(0))); + env.close(); + + env.trust(USD(10'000), alice); + env.close(); + + // alice cannot cancel because she is not authorized + env(escrow::cancel(bob, alice, seq1), + fee(baseFee), + ter(tecNO_AUTH)); + env.close(); + } + } + + void + testIOUBalances(FeatureBitset features) + { + testcase("IOU Balances"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + env(escrow::cancel(bob, alice, seq2), ter(tesSUCCESS)); + env.close(); + } + + void + testIOUMetaAndOwnership(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + { + testcase("IOU Metadata to self"); + + Env env{*this, features}; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow::create(alice, alice, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 500s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + auto const aa = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(aa); + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), aa) != aod.end()); + } + + { + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), aa) != iod.end()); + } + + env(escrow::create(bob, bob, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + auto const bb = env.le(keylet::escrow(bob.id(), bseq)); + BEAST_EXPECT(bb); + + { + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) != bod.end()); + } + + { + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 5); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bb) != iod.end()); + } + + env.close(5s); + env(escrow::finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), aa) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) != bod.end()); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bb) != iod.end()); + } + + env.close(5s); + env(escrow::cancel(bob, bob, bseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) == bod.end()); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bb) == iod.end()); + } + } + { + testcase("IOU Metadata to other"); + + Env env{*this, features}; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow::create(alice, bob, USD(1'000)), + escrow::finish_time(env.now() + 1s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + env(escrow::create(bob, carol, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + + auto const ab = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(ab); + + auto const bc = env.le(keylet::escrow(bob.id(), bseq)); + BEAST_EXPECT(bc); + + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) != aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) != bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) != bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + BEAST_EXPECT( + std::find(cod.begin(), cod.end(), bc) != cod.end()); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 5); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ab) != iod.end()); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bc) != iod.end()); + } + + env.close(5s); + env(escrow::finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(env.le(keylet::escrow(bob.id(), bseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) != bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ab) == iod.end()); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bc) != iod.end()); + } + + env.close(5s); + env(escrow::cancel(bob, bob, bseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) == bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ab) == iod.end()); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bc) == iod.end()); + } + } + + { + testcase("IOU Metadata to issuer"); + + Env env{*this, features}; + env.fund(XRP(5000), alice, carol, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + + env(escrow::create(alice, gw, USD(1'000)), + escrow::finish_time(env.now() + 1s)); + + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + env(escrow::create(gw, carol, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + ter(tecNO_PERMISSION)); + env.close(5s); + + auto const ag = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(ag); + + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ag) != aod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ag) != iod.end()); + } + + env.close(5s); + env(escrow::finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ag) == aod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 2); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ag) == iod.end()); + } + } + } + + void + testIOURippleState(FeatureBitset features) + { + testcase("IOU RippleState"); + using namespace test::jtx; + using namespace std::literals; + + struct TestAccountData + { + Account src; + Account dst; + Account gw; + bool hasTrustline; + bool negative; + }; + + std::array tests = {{ + // src > dst && src > issuer && dst no trustline + {Account("alice2"), Account("bob0"), Account{"gw0"}, false, true}, + // src < dst && src < issuer && dst no trustline + {Account("carol0"), Account("dan1"), Account{"gw1"}, false, false}, + // dst > src && dst > issuer && dst no trustline + {Account("dan1"), Account("alice2"), Account{"gw0"}, false, true}, + // dst < src && dst < issuer && dst no trustline + {Account("bob0"), Account("carol0"), Account{"gw1"}, false, false}, + // src > dst && src > issuer && dst has trustline + {Account("alice2"), Account("bob0"), Account{"gw0"}, true, true}, + // src < dst && src < issuer && dst has trustline + {Account("carol0"), Account("dan1"), Account{"gw1"}, true, false}, + // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account("alice2"), Account{"gw0"}, true, true}, + // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account("carol0"), Account{"gw1"}, true, false}, + }}; + + for (auto const& t : tests) + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const USD = t.gw["USD"]; + env.fund(XRP(5000), t.src, t.dst, t.gw); + env(fset(t.gw, asfAllowTrustLineLocking)); + env.close(); + + if (t.hasTrustline) + env.trust(USD(100'000), t.src, t.dst); + else + env.trust(USD(100'000), t.src); + env.close(); + + env(pay(t.gw, t.src, USD(10'000))); + if (t.hasTrustline) + env(pay(t.gw, t.dst, USD(10'000))); + env.close(); + + // src can create escrow + auto const seq1 = env.seq(t.src); + auto const delta = USD(1'000); + env(escrow::create(t.src, t.dst, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // dst can finish escrow + auto const preSrc = env.balance(t.src, USD); + auto const preDst = env.balance(t.dst, USD); + + env(escrow::finish(t.dst, t.src, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(t.src, USD) == preSrc); + BEAST_EXPECT(env.balance(t.dst, USD) == preDst + delta); + } + } + + void + testIOUGateway(FeatureBitset features) + { + testcase("IOU Gateway"); + using namespace test::jtx; + using namespace std::literals; + + struct TestAccountData + { + Account src; + Account dst; + bool hasTrustline; + }; + + // issuer is source + { + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.close(); + + env(pay(gw, alice, USD(10'000))); + env.close(); + + // issuer cannot create escrow + env(escrow::create(gw, alice, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + std::array gwDstTests = {{ + // src > dst && src > issuer && dst has trustline + {Account("alice2"), Account{"gw0"}, true}, + // src < dst && src < issuer && dst has trustline + {Account("carol0"), Account{"gw1"}, true}, + // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account{"gw0"}, true}, + // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account{"gw1"}, true}, + }}; + + // issuer is destination + for (auto const& t : gwDstTests) + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const USD = t.dst["USD"]; + env.fund(XRP(5000), t.dst, t.src); + env(fset(t.dst, asfAllowTrustLineLocking)); + env.close(); + + env.trust(USD(100'000), t.src); + env.close(); + + env(pay(t.dst, t.src, USD(10'000))); + env.close(); + + // issuer can receive escrow + auto const seq1 = env.seq(t.src); + auto const preSrc = env.balance(t.src, USD); + env(escrow::create(t.src, t.dst, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // issuer can finish escrow, no dest trustline + env(escrow::finish(t.dst, t.src, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + auto const preAmount = 10'000; + BEAST_EXPECT(preSrc == USD(preAmount)); + auto const postAmount = 9000; + BEAST_EXPECT(env.balance(t.src, USD) == USD(postAmount)); + BEAST_EXPECT(env.balance(t.dst, USD) == USD(0)); + } + + // issuer is source and destination + { + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(5000), gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + // issuer cannot receive escrow + env(escrow::create(gw, gw, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + } + + void + testIOULockedRate(FeatureBitset features) + { + testcase("IOU Locked Rate"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test locked rate + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + auto const transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // bob can finish escrow + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10'100)); + } + // test rate change - higher + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + auto transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate higher + env(rate(gw, 1.26)); + env.close(); + + // bob can finish escrow - rate unchanged + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10'100)); + } + + // test rate change - lower + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + auto transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate lower + env(rate(gw, 1.00)); + env.close(); + + // bob can finish escrow - rate changed + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10125)); + } + + // test cancel doesnt charge rate + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 3s), + fee(baseFee)); + env.close(); + auto transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate lower + env(rate(gw, 1.00)); + env.close(); + + // alice can cancel escrow - rate is not charged + env(escrow::cancel(alice, alice, seq1), fee(baseFee)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice); + BEAST_EXPECT(env.balance(bob, USD) == USD(10000)); + } + } + + void + testIOULimitAmount(FeatureBitset features) + { + testcase("IOU Limit"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test LimitAmount + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(1'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1'000))); + env(pay(gw, bob, USD(1'000))); + env.close(); + + // alice can create escrow + auto seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // bob can finish + auto const preBobLimit = env.limit(bob, USD); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + auto const postBobLimit = env.limit(bob, USD); + // bobs limit is NOT changed + BEAST_EXPECT(postBobLimit == preBobLimit); + } + } + + void + testIOURequireAuth(FeatureBitset features) + { + testcase("IOU Require Auth"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(1'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth)); + env(trust(alice, USD(10'000))); + env(trust(bob, USD(10'000))); + env.close(); + env(pay(gw, alice, USD(1'000))); + env.close(); + + // alice cannot create escrow - fails without auth + auto seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + + // set auth on bob + env(trust(gw, bobUSD(10'000)), txflags(tfSetfAuth)); + env(trust(bob, USD(10'000))); + env.close(); + env(pay(gw, bob, USD(1'000))); + env.close(); + + // alice can create escrow - bob has auth + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // bob can finish + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + } + + void + testIOUFreeze(FeatureBitset features) + { + testcase("IOU Freeze"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test Global Freeze + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + + // create escrow fails - frozen trustline + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + + // clear global freeze + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // bob finish escrow success regardless of frozen assets + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + // clear global freeze + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // bob cancel escrow success regardless of frozen assets + env(escrow::cancel(bob, alice, seq1), fee(baseFee)); + env.close(); + } + + // test Individual Freeze + { + // Env Setup + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfSetFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + + // create escrow fails - frozen trustline + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfClearFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze)); + env.close(); + + // bob finish escrow success regardless of frozen assets + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + // reset freeze on bob and alice trustline + env(trust(gw, USD(10'000), alice, tfClearFreeze)); + env(trust(gw, USD(10'000), bob, tfClearFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze)); + env.close(); + + // bob cancel escrow success regardless of frozen assets + env(escrow::cancel(bob, alice, seq1), fee(baseFee)); + env.close(); + } + + // test Deep Freeze + { + // Env Setup + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + + // create escrow fails - frozen trustline + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust( + gw, USD(10'000), alice, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // bob finish escrow fails because of deep frozen assets + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + + // reset freeze on alice and bob trustline + env(trust( + gw, USD(10'000), alice, tfClearFreeze | tfClearDeepFreeze)); + env(trust(gw, USD(10'000), bob, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // bob cancel escrow fails because of deep frozen assets + env(escrow::cancel(bob, alice, seq1), + fee(baseFee), + ter(tesSUCCESS)); + env.close(); + } + } + void + testIOUINSF(FeatureBitset features) + { + testcase("IOU Insuficient Funds"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + { + // test tecPATH_PARTIAL + // ie. has 10'000, escrow 1'000 then try to pay 10'000 + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // create escrow success + auto const delta = USD(1'000); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + env(pay(alice, gw, USD(10'000)), ter(tecPATH_PARTIAL)); + } + { + // test tecINSUFFICIENT_FUNDS + // ie. has 10'000 escrow 1'000 then try to escrow 10'000 + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const delta = USD(1'000); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + env(escrow::create(alice, bob, USD(10'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + } + + void + testIOUPrecisionLoss(FeatureBitset features) + { + testcase("IOU Precision Loss"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test min create precision loss + { + Env env(*this, features); + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100000000000000000), alice); + env.trust(USD(100000000000000000), bob); + env.close(); + env(pay(gw, alice, USD(10000000000000000))); + env(pay(gw, bob, USD(1))); + env.close(); + + // alice cannot create escrow for 1/10 iou - precision loss + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecPRECISION_LOSS)); + env.close(); + + auto const seq1 = env.seq(alice); + // alice can create escrow for 1'000 iou + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // bob finish escrow success + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + } + } + + void + testMPTEnablement(FeatureBitset features) + { + testcase("MPT Enablement"); + + using namespace jtx; + using namespace std::chrono; + + for (bool const withTokenEscrow : {false, true}) + { + auto const amend = + withTokenEscrow ? features : features - featureTokenEscrow; + Env env{*this, amend}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(5000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const createResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(temBAD_AMOUNT); + auto const finishResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(tecNO_TARGET); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + createResult); + env.close(); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + finishResult); + env.close(); + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + createResult); + env.close(); + env(escrow::cancel(bob, alice, seq2), finishResult); + env.close(); + } + } + + void + testMPTCreatePreflight(FeatureBitset features) + { + testcase("MPT Create Preflight"); + using namespace test::jtx; + using namespace std::literals; + + for (bool const withMPT : {true, false}) + { + auto const amend = + withMPT ? features : features - featureMPTokensV1; + Env env{*this, amend}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(1'000), alice, bob, gw); + + Json::Value jv = escrow::create(alice, bob, XRP(1)); + jv.removeMember(jss::Amount); + jv[jss::Amount][jss::mpt_issuance_id] = + "00000004A407AF5856CCF3C42619DAA925813FC955C72983"; + jv[jss::Amount][jss::value] = "-1"; + + auto const result = withMPT ? ter(temBAD_AMOUNT) : ter(temDISABLED); + env(jv, + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + result); + env.close(); + } + + // temBAD_AMOUNT: amount < 0 + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + env(escrow::create(alice, bob, MPT(-1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(temBAD_AMOUNT)); + env.close(); + } + } + + void + testMPTCreatePreclaim(FeatureBitset features) + { + testcase("MPT Create Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_PERMISSION: issuer is the same as the account + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + env(escrow::create(gw, alice, MPT(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + // tecOBJECT_NOT_FOUND: mpt does not exist + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), alice, bob, gw); + env.close(); + + auto const mpt = ripple::test::jtx::MPT( + alice.name(), makeMptID(env.seq(alice), alice)); + Json::Value jv = escrow::create(alice, bob, mpt(2)); + jv[jss::Amount][jss::mpt_issuance_id] = + "00000004A407AF5856CCF3C42619DAA925813FC955C72983"; + env(jv, + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecOBJECT_NOT_FOUND)); + env.close(); + } + + // tecNO_PERMISSION: tfMPTCanEscrow is not enabled + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + env(escrow::create(alice, bob, MPT(3)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + // tecOBJECT_NOT_FOUND: account does not have the mpt + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + auto const MPT = mptGw["MPT"]; + + env(escrow::create(alice, bob, MPT(4)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecOBJECT_NOT_FOUND)); + env.close(); + } + + // tecNO_AUTH: requireAuth set: account not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = + tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + // unauthorize account + mptGw.authorize( + {.account = gw, .holder = alice, .flags = tfMPTUnauthorize}); + + env(escrow::create(alice, bob, MPT(5)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = + tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // unauthorize dest + mptGw.authorize( + {.account = gw, .holder = bob, .flags = tfMPTUnauthorize}); + + env(escrow::create(alice, bob, MPT(6)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecLOCKED: issuer has locked the account + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // lock account + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock}); + + env(escrow::create(alice, bob, MPT(7)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecLOCKED)); + env.close(); + } + + // tecLOCKED: issuer has locked the dest + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // lock dest + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + env(escrow::create(alice, bob, MPT(8)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecLOCKED)); + env.close(); + } + + // tecNO_AUTH: mpt cannot be transferred + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + env(escrow::create(alice, bob, MPT(9)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS: spendable amount is zero + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10))); + env(pay(gw, bob, MPT(10))); + env.close(); + + env(escrow::create(alice, bob, MPT(11)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS: spendable amount is less than the amount + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10))); + env(pay(gw, bob, MPT(10))); + env.close(); + + env(escrow::create(alice, bob, MPT(11)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + } + + void + testMPTFinishPreclaim(FeatureBitset features) + { + testcase("MPT Finish Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = + tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // unauthorize dest + mptGw.authorize( + {.account = gw, .holder = bob, .flags = tfMPTUnauthorize}); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecLOCKED: issuer has locked the dest + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(8)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // lock dest + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecLOCKED)); + env.close(); + } + } + + void + testMPTFinishDoApply(FeatureBitset features) + { + testcase("MPT Finish Do Apply"); + using namespace test::jtx; + using namespace std::literals; + + // tecINSUFFICIENT_RESERVE: insufficient reserve to create MPT + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(acctReserve + (incReserve - 1), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + + // tesSUCCESS: bob submits; finish MPT created + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + } + + // tecNO_PERMISSION: carol submits; finish MPT not created + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob, carol); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::finish(carol, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + } + + void + testMPTCancelPreclaim(FeatureBitset features) + { + testcase("MPT Cancel Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: account not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = + tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::cancel_time(env.now() + 2s), + escrow::condition(escrow::cb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // unauthorize account + mptGw.authorize( + {.account = gw, .holder = alice, .flags = tfMPTUnauthorize}); + + env(escrow::cancel(bob, alice, seq1), ter(tecNO_AUTH)); + env.close(); + } + } + + void + testMPTBalances(FeatureBitset features) + { + testcase("MPT Balances"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + env.fund(XRP(5000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice, carol}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = carol}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, carol, MPT(10'000))); + env.close(); + + auto outstandingMPT = env.balance(gw, MPT); + + // Create & Finish Escrow + auto const seq1 = env.seq(alice); + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + } + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT + MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Create & Cancel Escrow + auto const seq2 = env.seq(alice); + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + } + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + env(escrow::cancel(bob, alice, seq2), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT + MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Self Escrow Create & Finish + { + auto const seq = env.seq(alice); + auto const preAliceMPT = env.balance(alice, MPT); + env(escrow::create(alice, alice, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + + env(escrow::finish(alice, alice, seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Self Escrow Create & Cancel + { + auto const seq = env.seq(alice); + auto const preAliceMPT = env.balance(alice, MPT); + env(escrow::create(alice, alice, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + + env(escrow::cancel(alice, alice, seq), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Multiple Escrows + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const preCarolMPT = env.balance(carol, MPT); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::create(carol, bob, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(carol, MPT) == preCarolMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, carol, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 2'000); + } + + // Max MPT Amount Issued (Escrow 1 MPT) + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(maxMPTokenAmount))); + env.close(); + + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const outstandingMPT = env.balance(gw, MPT); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(!env.le(keylet::mptoken(MPT.mpt(), alice)) + ->isFieldPresent(sfLockedAmount)); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT + MPT(1)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + BEAST_EXPECT(!env.le(keylet::mptIssuance(MPT.mpt())) + ->isFieldPresent(sfLockedAmount)); + } + + // Max MPT Amount Issued (Escrow Max MPT) + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(maxMPTokenAmount))); + env.close(); + + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const outstandingMPT = env.balance(gw, MPT); + + // Escrow Max MPT - 10 + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(maxMPTokenAmount - 10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // Escrow 10 MPT + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT( + env.balance(alice, MPT) == preAliceMPT - MPT(maxMPTokenAmount)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == maxMPTokenAmount); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == maxMPTokenAmount); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::finish(bob, alice, seq2), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + env.balance(alice, MPT) == preAliceMPT - MPT(maxMPTokenAmount)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT( + env.balance(bob, MPT) == preBobMPT + MPT(maxMPTokenAmount)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + } + + void + testMPTMetaAndOwnership(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + { + testcase("MPT Metadata to self"); + + Env env{*this, features}; + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow::create(alice, alice, MPT(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 500s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + auto const aa = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(aa); + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), aa) != aod.end()); + } + + { + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 1); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), aa) == iod.end()); + } + + env(escrow::create(bob, bob, MPT(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + auto const bb = env.le(keylet::escrow(bob.id(), bseq)); + BEAST_EXPECT(bb); + + { + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) != bod.end()); + } + + env.close(5s); + env(escrow::finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), aa) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) != bod.end()); + } + + env.close(5s); + env(escrow::cancel(bob, bob, bseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) == bod.end()); + } + } + + { + testcase("MPT Metadata to other"); + + Env env{*this, features}; + MPTTester mptGw(env, gw, {.holders = {alice, bob, carol}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = carol}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env(pay(gw, carol, MPT(10'000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow::create(alice, bob, MPT(1'000)), + escrow::finish_time(env.now() + 1s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + env(escrow::create(bob, carol, MPT(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + + auto const ab = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(ab); + + auto const bc = env.le(keylet::escrow(bob.id(), bseq)); + BEAST_EXPECT(bc); + + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) != aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) != bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) != bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + BEAST_EXPECT( + std::find(cod.begin(), cod.end(), bc) != cod.end()); + } + + env.close(5s); + env(escrow::finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(env.le(keylet::escrow(bob.id(), bseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) != bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + } + + env.close(5s); + env(escrow::cancel(bob, bob, bseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) == bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + } + } + } + + void + testMPTGateway(FeatureBitset features) + { + testcase("MPT Gateway Balances"); + using namespace test::jtx; + using namespace std::literals; + + // issuer is dest; alice w/ authorization + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + // issuer can be destination + auto const seq1 = env.seq(alice); + auto const preAliceMPT = env.balance(alice, MPT); + auto const preOutstanding = env.balance(gw, MPT); + auto const preEscrowed = issuerMPTEscrowed(env, MPT); + BEAST_EXPECT(preOutstanding == MPT(10'000)); + BEAST_EXPECT(preEscrowed == 0); + + env(escrow::create(alice, gw, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == preEscrowed + 1'000); + + // issuer (dest) can finish escrow + env(escrow::finish(gw, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding - MPT(1'000)); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == preEscrowed); + } + } + + void + testMPTLockedRate(FeatureBitset features) + { + testcase("MPT Locked Rate"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test locked rate: finish + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.transferFee = 25000, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, MPT); + auto const seq1 = env.seq(alice); + auto const delta = MPT(125); + env(escrow::create(alice, bob, MPT(125)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + auto const transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // bob can finish escrow + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, MPT) == MPT(10'100)); + } + + // test locked rate: cancel + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.transferFee = 25000, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, MPT); + auto const preBob = env.balance(bob, MPT); + auto const seq1 = env.seq(alice); + auto const delta = MPT(125); + env(escrow::create(alice, bob, MPT(125)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 3s), + fee(baseFee * 150)); + env.close(); + auto const transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // alice can cancel escrow + env(escrow::cancel(alice, alice, seq1), fee(baseFee)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAlice); + BEAST_EXPECT(env.balance(bob, MPT) == preBob); + } + } + + void + testMPTRequireAuth(FeatureBitset features) + { + testcase("MPT Require Auth"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto seq = env.seq(alice); + auto const delta = MPT(125); + // alice can create escrow - is authorized + env(escrow::create(alice, bob, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // bob can finish escrow - is authorized + env(escrow::finish(bob, alice, seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + } + + void + testMPTLock(FeatureBitset features) + { + testcase("MPT Lock"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice create escrow + auto seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150)); + env.close(); + + // lock account & dest + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock}); + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + // bob cannot finish + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecLOCKED)); + env.close(); + + // bob can cancel + env(escrow::cancel(bob, alice, seq1)); + env.close(); + } + + void + testMPTCanTransfer(FeatureBitset features) + { + testcase("MPT Can Transfer"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice cannot create escrow to non issuer + env(escrow::create(alice, bob, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + + // Escrow Create & Finish + { + // alice an create escrow to issuer + auto seq = env.seq(alice); + env(escrow::create(alice, gw, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // gw can finish + env(escrow::finish(gw, alice, seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + } + + // Escrow Create & Cancel + { + // alice an create escrow to issuer + auto seq = env.seq(alice); + env(escrow::create(alice, gw, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150)); + env.close(); + + // alice can cancel + env(escrow::cancel(alice, alice, seq)); + env.close(); + } + } + + void + testMPTDestroy(FeatureBitset features) + { + testcase("MPT Destroy"); + using namespace test::jtx; + using namespace std::literals; + + // tecHAS_OBLIGATIONS: issuer cannot destroy issuance + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + env(pay(alice, gw, MPT(9'990))); + env(pay(bob, gw, MPT(10'000))); + BEAST_EXPECT(env.balance(alice, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 10); + BEAST_EXPECT(env.balance(bob, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + mptGw.authorize({.account = bob, .flags = tfMPTUnauthorize}); + mptGw.destroy( + {.id = mptGw.issuanceID(), + .ownerCount = 1, + .err = tecHAS_OBLIGATIONS}); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(pay(bob, gw, MPT(10))); + mptGw.destroy({.id = mptGw.issuanceID(), .ownerCount = 0}); + } + + // tecHAS_OBLIGATIONS: holder cannot destroy mptoken + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(pay(alice, gw, MPT(9'990))); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 10); + mptGw.authorize( + {.account = alice, + .flags = tfMPTUnauthorize, + .err = tecHAS_OBLIGATIONS}); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + mptGw.authorize({.account = alice, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(MPT.mpt(), alice))); + } + } + + void + testIOUWithFeats(FeatureBitset features) + { + testIOUEnablement(features); + testIOUAllowLockingFlag(features); + testIOUCreatePreflight(features); + testIOUCreatePreclaim(features); + testIOUFinishPreclaim(features); + testIOUFinishDoApply(features); + testIOUCancelPreclaim(features); + testIOUBalances(features); + testIOUMetaAndOwnership(features); + testIOURippleState(features); + testIOUGateway(features); + testIOULockedRate(features); + testIOULimitAmount(features); + testIOURequireAuth(features); + testIOUFreeze(features); + testIOUINSF(features); + testIOUPrecisionLoss(features); + } + + void + testMPTWithFeats(FeatureBitset features) + { + testMPTEnablement(features); + testMPTCreatePreflight(features); + testMPTCreatePreclaim(features); + testMPTFinishPreclaim(features); + testMPTFinishDoApply(features); + testMPTCancelPreclaim(features); + testMPTBalances(features); + testMPTMetaAndOwnership(features); + testMPTGateway(features); + testMPTLockedRate(features); + testMPTRequireAuth(features); + testMPTLock(features); + testMPTCanTransfer(features); + testMPTDestroy(features); + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + testIOUWithFeats(all); + testMPTWithFeats(all); + } +}; + +BEAST_DEFINE_TESTSUITE(EscrowToken, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index 1129019aab..aa86ad338e 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -35,81 +35,53 @@ namespace test { struct Escrow_test : public beast::unit_test::suite { - // A PreimageSha256 fulfillments and its associated condition. - std::array const fb1 = {{0xA0, 0x02, 0x80, 0x00}}; - - std::array const cb1 = { - {0xA0, 0x25, 0x80, 0x20, 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, - 0x1C, 0x14, 0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24, - 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95, - 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55, 0x81, 0x01, 0x00}}; - - // Another PreimageSha256 fulfillments and its associated condition. - std::array const fb2 = { - {0xA0, 0x05, 0x80, 0x03, 0x61, 0x61, 0x61}}; - - std::array const cb2 = { - {0xA0, 0x25, 0x80, 0x20, 0x98, 0x34, 0x87, 0x6D, 0xCF, 0xB0, - 0x5C, 0xB1, 0x67, 0xA5, 0xC2, 0x49, 0x53, 0xEB, 0xA5, 0x8C, - 0x4A, 0xC8, 0x9B, 0x1A, 0xDF, 0x57, 0xF2, 0x8F, 0x2F, 0x9D, - 0x09, 0xAF, 0x10, 0x7E, 0xE8, 0xF0, 0x81, 0x01, 0x03}}; - - // Another PreimageSha256 fulfillment and its associated condition. - std::array const fb3 = { - {0xA0, 0x06, 0x80, 0x04, 0x6E, 0x69, 0x6B, 0x62}}; - - std::array const cb3 = { - {0xA0, 0x25, 0x80, 0x20, 0x6E, 0x4C, 0x71, 0x45, 0x30, 0xC0, - 0xA4, 0x26, 0x8B, 0x3F, 0xA6, 0x3B, 0x1B, 0x60, 0x6F, 0x2D, - 0x26, 0x4A, 0x2D, 0x85, 0x7B, 0xE8, 0xA0, 0x9C, 0x1D, 0xFD, - 0x57, 0x0D, 0x15, 0x85, 0x8B, 0xD4, 0x81, 0x01, 0x04}}; - void - testEnablement() + testEnablement(FeatureBitset features) { testcase("Enablement"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); - env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() + 1s)); env.close(); auto const seq1 = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - condition(cb1), - finish_time(env.now() + 1s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), fee(baseFee * 150)); env.close(); - env(finish("bob", "alice", seq1), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(baseFee * 150)); auto const seq2 = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - condition(cb2), - finish_time(env.now() + 1s), - cancel_time(env.now() + 2s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), fee(baseFee * 150)); env.close(); - env(cancel("bob", "alice", seq2), fee(baseFee * 150)); + env(escrow::cancel("bob", "alice", seq2), fee(baseFee * 150)); } void - testTiming() + testTiming(FeatureBitset features) { using namespace jtx; using namespace std::chrono; { testcase("Timing: Finish Only"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -118,21 +90,22 @@ struct Escrow_test : public beast::unit_test::suite auto const ts = env.now() + 97s; auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), finish_time(ts)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(ts)); // Advance the ledger, verifying that the finish won't complete // prematurely. for (; env.now() < ts; env.close()) - env(finish("bob", "alice", seq), + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), fee(baseFee * 150)); + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150)); } { testcase("Timing: Cancel Only"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -141,31 +114,31 @@ struct Escrow_test : public beast::unit_test::suite auto const ts = env.now() + 117s; auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - condition(cb1), - cancel_time(ts)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(escrow::cb1), + escrow::cancel_time(ts)); // Advance the ledger, verifying that the cancel won't complete // prematurely. for (; env.now() < ts; env.close()) - env(cancel("bob", "alice", seq), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); // Verify that a finish won't work anymore. - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(baseFee * 150), ter(tecNO_PERMISSION)); // Verify that the cancel will succeed - env(cancel("bob", "alice", seq), fee(baseFee * 150)); + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150)); } { testcase("Timing: Finish and Cancel -> Finish"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -175,34 +148,34 @@ struct Escrow_test : public beast::unit_test::suite auto const cts = env.now() + 192s; auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - finish_time(fts), - cancel_time(cts)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(fts), + escrow::cancel_time(cts)); // Advance the ledger, verifying that the finish and cancel won't // complete prematurely. for (; env.now() < fts; env.close()) { - env(finish("bob", "alice", seq), + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); } // Verify that a cancel still won't work - env(cancel("bob", "alice", seq), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); // And verify that a finish will - env(finish("bob", "alice", seq), fee(baseFee * 150)); + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150)); } { testcase("Timing: Finish and Cancel -> Cancel"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -212,18 +185,18 @@ struct Escrow_test : public beast::unit_test::suite auto const cts = env.now() + 184s; auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - finish_time(fts), - cancel_time(cts)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(fts), + escrow::cancel_time(cts)); // Advance the ledger, verifying that the finish and cancel won't // complete prematurely. for (; env.now() < fts; env.close()) { - env(finish("bob", "alice", seq), + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); } @@ -231,30 +204,30 @@ struct Escrow_test : public beast::unit_test::suite // Continue advancing, verifying that the cancel won't complete // prematurely. At this point a finish would succeed. for (; env.now() < cts; env.close()) - env(cancel("bob", "alice", seq), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); // Verify that finish will no longer work, since we are past the // cancel activation time. - env(finish("bob", "alice", seq), + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); // And verify that a cancel will succeed. - env(cancel("bob", "alice", seq), fee(baseFee * 150)); + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150)); } } void - testTags() + testTags(FeatureBitset features) { testcase("Tags"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -264,15 +237,15 @@ struct Escrow_test : public beast::unit_test::suite // Check to make sure that we correctly detect if tags are really // required: env(fset(bob, asfRequireDest)); - env(escrow(alice, bob, XRP(1000)), - finish_time(env.now() + 1s), + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s), ter(tecDST_TAG_NEEDED)); // set source and dest tags auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), - finish_time(env.now() + 1s), + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s), stag(1), dtag(2)); @@ -283,7 +256,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testDisallowXRP() + testDisallowXRP(FeatureBitset features) { testcase("Disallow XRP"); @@ -292,27 +265,28 @@ struct Escrow_test : public beast::unit_test::suite { // Respect the "asfDisallowXRP" account flag: - Env env(*this, supported_amendments() - featureDepositAuth); + Env env(*this, features - featureDepositAuth); env.fund(XRP(5000), "bob", "george"); env(fset("george", asfDisallowXRP)); - env(escrow("bob", "george", XRP(10)), - finish_time(env.now() + 1s), + env(escrow::create("bob", "george", XRP(10)), + escrow::finish_time(env.now() + 1s), ter(tecNO_TARGET)); } { // Ignore the "asfDisallowXRP" account flag, which we should // have been doing before. - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "bob", "george"); env(fset("george", asfDisallowXRP)); - env(escrow("bob", "george", XRP(10)), finish_time(env.now() + 1s)); + env(escrow::create("bob", "george", XRP(10)), + escrow::finish_time(env.now() + 1s)); } } void - test1571() + test1571(FeatureBitset features) { using namespace jtx; using namespace std::chrono; @@ -328,11 +302,11 @@ struct Escrow_test : public beast::unit_test::suite // Creating an escrow without a finish time and finishing it // is allowed without fix1571: auto const seq1 = env.seq("alice"); - env(escrow("alice", "bob", XRP(100)), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 1s), fee(baseFee * 150)); env.close(); - env(finish("carol", "alice", seq1), fee(baseFee * 150)); + env(escrow::finish("carol", "alice", seq1), fee(baseFee * 150)); BEAST_EXPECT(env.balance("bob") == XRP(5100)); env.close(); @@ -340,14 +314,14 @@ struct Escrow_test : public beast::unit_test::suite // Creating an escrow without a finish time and a condition is // also allowed without fix1571: auto const seq2 = env.seq("alice"); - env(escrow("alice", "bob", XRP(100)), - cancel_time(env.now() + 1s), - condition(cb1), + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 1s), + escrow::condition(escrow::cb1), fee(baseFee * 150)); env.close(); - env(finish("carol", "alice", seq2), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("carol", "alice", seq2), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(baseFee * 150)); BEAST_EXPECT(env.balance("bob") == XRP(5200)); } @@ -355,117 +329,125 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Implied Finish Time (with fix1571)"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); // Creating an escrow with only a cancel time is not allowed: - env(escrow("alice", "bob", XRP(100)), - cancel_time(env.now() + 90s), + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 90s), fee(baseFee * 150), ter(temMALFORMED)); // Creating an escrow with only a cancel time and a condition is // allowed: auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(100)), - cancel_time(env.now() + 90s), - condition(cb1), + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 90s), + escrow::condition(escrow::cb1), fee(baseFee * 150)); env.close(); - env(finish("carol", "alice", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("carol", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(baseFee * 150)); BEAST_EXPECT(env.balance("bob") == XRP(5100)); } } void - testFails() + testFails(FeatureBitset features) { testcase("Failure Cases"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; - env.fund(XRP(5000), "alice", "bob"); + env.fund(XRP(5000), "alice", "bob", "gw"); env.close(); // Finish time is in the past - env(escrow("alice", "bob", XRP(1000)), - finish_time(env.now() - 5s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() - 5s), ter(tecNO_PERMISSION)); // Cancel time is in the past - env(escrow("alice", "bob", XRP(1000)), - condition(cb1), - cancel_time(env.now() - 5s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() - 5s), ter(tecNO_PERMISSION)); // no destination account - env(escrow("alice", "carol", XRP(1000)), - finish_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::finish_time(env.now() + 1s), ter(tecNO_DST)); env.fund(XRP(5000), "carol"); // Using non-XRP: - env(escrow("alice", "carol", Account("alice")["USD"](500)), - finish_time(env.now() + 1s), - ter(temBAD_AMOUNT)); + bool const withTokenEscrow = + env.current()->rules().enabled(featureTokenEscrow); + { + // tecNO_PERMISSION: token escrow is enabled but the issuer did not + // set the asfAllowTrustLineLocking flag + auto const txResult = + withTokenEscrow ? ter(tecNO_PERMISSION) : ter(temBAD_AMOUNT); + env(escrow::create("alice", "carol", Account("alice")["USD"](500)), + escrow::finish_time(env.now() + 5s), + txResult); + } // Sending zero or no XRP: - env(escrow("alice", "carol", XRP(0)), - finish_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(0)), + escrow::finish_time(env.now() + 1s), ter(temBAD_AMOUNT)); - env(escrow("alice", "carol", XRP(-1000)), - finish_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(-1000)), + escrow::finish_time(env.now() + 1s), ter(temBAD_AMOUNT)); // Fail if neither CancelAfter nor FinishAfter are specified: - env(escrow("alice", "carol", XRP(1)), ter(temBAD_EXPIRATION)); + env(escrow::create("alice", "carol", XRP(1)), ter(temBAD_EXPIRATION)); // Fail if neither a FinishTime nor a condition are attached: - env(escrow("alice", "carol", XRP(1)), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::cancel_time(env.now() + 1s), ter(temMALFORMED)); // Fail if FinishAfter has already passed: - env(escrow("alice", "carol", XRP(1)), - finish_time(env.now() - 1s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::finish_time(env.now() - 1s), ter(tecNO_PERMISSION)); // If both CancelAfter and FinishAfter are set, then CancelAfter must // be strictly later than FinishAfter. - env(escrow("alice", "carol", XRP(1)), - condition(cb1), - finish_time(env.now() + 10s), - cancel_time(env.now() + 10s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 10s), + escrow::cancel_time(env.now() + 10s), ter(temBAD_EXPIRATION)); - env(escrow("alice", "carol", XRP(1)), - condition(cb1), - finish_time(env.now() + 10s), - cancel_time(env.now() + 5s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 10s), + escrow::cancel_time(env.now() + 5s), ter(temBAD_EXPIRATION)); // Carol now requires the use of a destination tag env(fset("carol", asfRequireDest)); // missing destination tag - env(escrow("alice", "carol", XRP(1)), - condition(cb1), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), ter(tecDST_TAG_NEEDED)); // Success! - env(escrow("alice", "carol", XRP(1)), - condition(cb1), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), dtag(1)); { // Fail if the sender wants to send more than he has: @@ -474,29 +456,29 @@ struct Escrow_test : public beast::unit_test::suite drops(env.current()->fees().increment); env.fund(accountReserve + accountIncrement + XRP(50), "daniel"); - env(escrow("daniel", "bob", XRP(51)), - finish_time(env.now() + 1s), + env(escrow::create("daniel", "bob", XRP(51)), + escrow::finish_time(env.now() + 1s), ter(tecUNFUNDED)); env.fund(accountReserve + accountIncrement + XRP(50), "evan"); - env(escrow("evan", "bob", XRP(50)), - finish_time(env.now() + 1s), + env(escrow::create("evan", "bob", XRP(50)), + escrow::finish_time(env.now() + 1s), ter(tecUNFUNDED)); env.fund(accountReserve, "frank"); - env(escrow("frank", "bob", XRP(1)), - finish_time(env.now() + 1s), + env(escrow::create("frank", "bob", XRP(1)), + escrow::finish_time(env.now() + 1s), ter(tecINSUFFICIENT_RESERVE)); } { // Specify incorrect sequence number env.fund(XRP(5000), "hannah"); auto const seq = env.seq("hannah"); - env(escrow("hannah", "hannah", XRP(10)), - finish_time(env.now() + 1s), + env(escrow::create("hannah", "hannah", XRP(10)), + escrow::finish_time(env.now() + 1s), fee(150 * baseFee)); env.close(); - env(finish("hannah", "hannah", seq + 7), + env(escrow::finish("hannah", "hannah", seq + 7), fee(150 * baseFee), ter(tecNO_TARGET)); } @@ -505,18 +487,19 @@ struct Escrow_test : public beast::unit_test::suite env.fund(XRP(5000), "ivan"); auto const seq = env.seq("ivan"); - env(escrow("ivan", "ivan", XRP(10)), finish_time(env.now() + 1s)); + env(escrow::create("ivan", "ivan", XRP(10)), + escrow::finish_time(env.now() + 1s)); env.close(); - env(finish("ivan", "ivan", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("ivan", "ivan", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); } } void - testLockup() + testLockup(FeatureBitset features) { testcase("Lockup"); @@ -525,49 +508,50 @@ struct Escrow_test : public beast::unit_test::suite { // Unconditional - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); - env(escrow("alice", "alice", XRP(1000)), - finish_time(env.now() + 5s)); + env(escrow::create("alice", "alice", XRP(1000)), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); // Not enough time has elapsed for a finish and canceling isn't // possible. - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq), ter(tecNO_PERMISSION)); env.close(); // Cancel continues to not be possible - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); // Finish should succeed. Verify funds. - env(finish("bob", "alice", seq)); + env(escrow::finish("bob", "alice", seq)); env.require(balance("alice", XRP(5000) - drops(baseFee))); } { // Unconditionally pay from Alice to Bob. Zelda (neither source nor // destination) signs all cancels and finishes. This shows that // Escrow will make a payment to Bob with no intervention from Bob. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "zelda"); auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); // Not enough time has elapsed for a finish and canceling isn't // possible. - env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); env.close(); // Cancel continues to not be possible - env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); // Finish should succeed. Verify funds. - env(finish("zelda", "alice", seq)); + env(escrow::finish("zelda", "alice", seq)); env.close(); env.require(balance("alice", XRP(4000) - drops(baseFee))); @@ -576,7 +560,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Bob sets DepositAuth so only Bob can finish the escrow. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "zelda"); @@ -584,27 +568,28 @@ struct Escrow_test : public beast::unit_test::suite env.close(); auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); // Not enough time has elapsed for a finish and canceling isn't // possible. - env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq), ter(tecNO_PERMISSION)); env.close(); // Cancel continues to not be possible. Finish will only succeed for // Bob, because of DepositAuth. - env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq)); + env(escrow::cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq)); env.close(); env.require(balance("alice", XRP(4000) - (baseFee * 5))); @@ -614,7 +599,7 @@ struct Escrow_test : public beast::unit_test::suite { // Bob sets DepositAuth but preauthorizes Zelda, so Zelda can // finish the escrow. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "zelda"); @@ -624,15 +609,16 @@ struct Escrow_test : public beast::unit_test::suite env.close(); auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); env.close(); // DepositPreauth allows Finish to succeed for either Zelda or // Bob. But Finish won't succeed for Alice since she is not // preauthorized. - env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq)); + env(escrow::finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("zelda", "alice", seq)); env.close(); env.require(balance("alice", XRP(4000) - (baseFee * 2))); @@ -641,93 +627,97 @@ struct Escrow_test : public beast::unit_test::suite } { // Conditional - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); - env(escrow("alice", "alice", XRP(1000)), - condition(cb2), - finish_time(env.now() + 5s)); + env(escrow::create("alice", "alice", XRP(1000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); // Not enough time has elapsed for a finish and canceling isn't // possible. - env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("alice", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecNO_PERMISSION)); env.close(); // Cancel continues to not be possible. Finish is possible but // requires the fulfillment associated with the escrow. - env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("alice", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("alice", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); env.close(); - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee)); } { // Self-escrowed conditional with DepositAuth. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); - env(escrow("alice", "alice", XRP(1000)), - condition(cb3), - finish_time(env.now() + 5s)); + env(escrow::create("alice", "alice", XRP(1000)), + escrow::condition(escrow::cb3), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); env.close(); // Finish is now possible but requires the cryptocondition. - env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("alice", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("bob", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("alice", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); // Enable deposit authorization. After this only Alice can finish // the escrow. env(fset("alice", asfDepositAuth)); env.close(); - env(finish("alice", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("alice", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("alice", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee)); } { // Self-escrowed conditional with DepositAuth and DepositPreauth. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "zelda"); auto const seq = env.seq("alice"); - env(escrow("alice", "alice", XRP(1000)), - condition(cb3), - finish_time(env.now() + 5s)); + env(escrow::create("alice", "alice", XRP(1000)), + escrow::condition(escrow::cb3), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); env.close(); @@ -737,34 +727,37 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Finish is now possible but requires the cryptocondition. - env(finish("alice", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("zelda", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("alice", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("bob", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("zelda", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); // Alice enables deposit authorization. After this only Alice or // Zelda (because Zelda is preauthorized) can finish the escrow. env(fset("alice", asfDepositAuth)); env.close(); - env(finish("alice", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("alice", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("zelda", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee)); } } void - testEscrowConditions() + testEscrowConditions(FeatureBitset features) { testcase("Escrow with CryptoConditions"); @@ -772,126 +765,127 @@ struct Escrow_test : public beast::unit_test::suite using namespace std::chrono; { // Test cryptoconditions - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); auto const seq = env.seq("alice"); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); - env(escrow("alice", "carol", XRP(1000)), - condition(cb1), - cancel_time(env.now() + 1s)); + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); env.require(balance("alice", XRP(4000) - drops(baseFee))); env.require(balance("carol", XRP(5000))); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish without a fulfillment - env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("bob", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish with a condition instead of a fulfillment - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(cb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::cb1), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(cb2), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::cb2), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(cb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::cb3), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish with an incorrect condition and various // combinations of correct and incorrect fulfillments. - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish with the correct condition & fulfillment - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee)); // SLE removed on finish BEAST_EXPECT(!env.le(keylet::escrow(Account("alice").id(), seq))); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); env.require(balance("carol", XRP(6000))); - env(cancel("bob", "alice", seq), ter(tecNO_TARGET)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_TARGET)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); - env(cancel("bob", "carol", 1), ter(tecNO_TARGET)); + env(escrow::cancel("bob", "carol", 1), ter(tecNO_TARGET)); } { // Test cancel when condition is present - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); auto const seq = env.seq("alice"); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); - env(escrow("alice", "carol", XRP(1000)), - condition(cb2), - cancel_time(env.now() + 1s)); + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(escrow::cb2), + escrow::cancel_time(env.now() + 1s)); env.close(); env.require(balance("alice", XRP(4000) - drops(baseFee))); // balance restored on cancel - env(cancel("bob", "alice", seq)); + env(escrow::cancel("bob", "alice", seq)); env.require(balance("alice", XRP(5000) - drops(baseFee))); // SLE removed on cancel BEAST_EXPECT(!env.le(keylet::escrow(Account("alice").id(), seq))); } { - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); auto const seq = env.seq("alice"); - env(escrow("alice", "carol", XRP(1000)), - condition(cb3), - cancel_time(env.now() + 1s)); + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(escrow::cb3), + escrow::cancel_time(env.now() + 1s)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // cancel fails before expiration - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); env.close(); // finish fails after expiration - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecNO_PERMISSION)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); env.require(balance("carol", XRP(5000))); } { // Test long & short conditions during creation - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "carol"); std::vector v; - v.resize(cb1.size() + 2, 0x78); - std::memcpy(v.data() + 1, cb1.data(), cb1.size()); + v.resize(escrow::cb1.size() + 2, 0x78); + std::memcpy(v.data() + 1, escrow::cb1.data(), escrow::cb1.size()); auto const p = v.data(); auto const s = v.size(); @@ -900,63 +894,63 @@ struct Escrow_test : public beast::unit_test::suite // All these are expected to fail, because the // condition we pass in is malformed in some way - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p, s}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p, s}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p, s - 1}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p, s - 1}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p, s - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p, s - 2}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 1, s - 1}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 1, s - 1}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 1, s - 3}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 1, s - 3}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 2, s - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 2, s - 2}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 2, s - 3}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 2, s - 3}), + escrow::cancel_time(ts), ter(temMALFORMED)); auto const seq = env.seq("alice"); auto const baseFee = env.current()->fees().base; - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 1, s - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 1, s - 2}), + escrow::cancel_time(ts), fee(10 * baseFee)); - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee)); env.require(balance("alice", XRP(4000) - drops(10 * baseFee))); env.require(balance("bob", XRP(5000) - drops(150 * baseFee))); env.require(balance("carol", XRP(6000))); } { // Test long and short conditions & fulfillments during finish - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "carol"); std::vector cv; - cv.resize(cb2.size() + 2, 0x78); - std::memcpy(cv.data() + 1, cb2.data(), cb2.size()); + cv.resize(escrow::cb2.size() + 2, 0x78); + std::memcpy(cv.data() + 1, escrow::cb2.data(), escrow::cb2.size()); auto const cp = cv.data(); auto const cs = cv.size(); std::vector fv; - fv.resize(fb2.size() + 2, 0x13); - std::memcpy(fv.data() + 1, fb2.data(), fb2.size()); + fv.resize(escrow::fb2.size() + 2, 0x13); + std::memcpy(fv.data() + 1, escrow::fb2.data(), escrow::fb2.size()); auto const fp = fv.data(); auto const fs = fv.size(); @@ -965,180 +959,182 @@ struct Escrow_test : public beast::unit_test::suite // All these are expected to fail, because the // condition we pass in is malformed in some way - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp, cs}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp, cs}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp, cs - 1}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp, cs - 1}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp, cs - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp, cs - 2}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 1, cs - 1}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 1, cs - 1}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 1, cs - 3}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 1, cs - 3}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 2, cs - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 2, cs - 2}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 2, cs - 3}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 2, cs - 3}), + escrow::cancel_time(ts), ter(temMALFORMED)); auto const seq = env.seq("alice"); auto const baseFee = env.current()->fees().base; - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 1, cs - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::cancel_time(ts), fee(10 * baseFee)); // Now, try to fulfill using the same sequence of // malformed conditions. - env(finish("bob", "alice", seq), - condition(Slice{cp, cs}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp, cs}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp, cs - 1}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp, cs - 1}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp, cs - 2}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp, cs - 2}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 1}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 1}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 3}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 3}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 2, cs - 2}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 2, cs - 2}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 2, cs - 3}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 2, cs - 3}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); // Now, using the correct condition, try malformed fulfillments: - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp, fs - 1}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp, fs - 1}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp, fs - 2}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp, fs - 2}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 1, fs - 1}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 1, fs - 1}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 1, fs - 3}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 1, fs - 3}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 1, fs - 3}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 1, fs - 3}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 2, fs - 2}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 2, fs - 2}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 2, fs - 3}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 2, fs - 3}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); // Now try for the right one - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee)); env.require(balance("alice", XRP(4000) - drops(10 * baseFee))); env.require(balance("carol", XRP(6000))); } { // Test empty condition during creation and // empty condition & fulfillment during finish - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "carol"); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{}), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{}), + escrow::cancel_time(env.now() + 1s), ter(temMALFORMED)); auto const seq = env.seq("alice"); auto const baseFee = env.current()->fees().base; - env(escrow("alice", "carol", XRP(1000)), - condition(cb3), - cancel_time(env.now() + 1s)); + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(escrow::cb3), + escrow::cancel_time(env.now() + 1s)); - env(finish("bob", "alice", seq), - condition(Slice{}), - fulfillment(Slice{}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{}), + escrow::fulfillment(Slice{}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(Slice{}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(Slice{}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{}), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{}), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); // Assemble finish that is missing the Condition or the Fulfillment // since either both must be present, or neither can: - env(finish("bob", "alice", seq), condition(cb3), ter(temMALFORMED)); - env(finish("bob", "alice", seq), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + ter(temMALFORMED)); + env(escrow::finish("bob", "alice", seq), + escrow::fulfillment(escrow::fb3), ter(temMALFORMED)); // Now finish it. - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee)); env.require(balance("carol", XRP(6000))); env.require(balance("alice", XRP(4000) - drops(baseFee))); } { // Test a condition other than PreimageSha256, which // would require a separate amendment - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob"); std::array cb = { @@ -1150,15 +1146,15 @@ struct Escrow_test : public beast::unit_test::suite // FIXME: this transaction should, eventually, return temDISABLED // instead of temMALFORMED. - env(escrow("alice", "bob", XRP(1000)), - condition(cb), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(cb), + escrow::cancel_time(env.now() + 1s), ter(temMALFORMED)); } } void - testMetaAndOwnership() + testMetaAndOwnership(FeatureBitset features) { using namespace jtx; using namespace std::chrono; @@ -1170,14 +1166,14 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Metadata to self"); - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bruce, carol); auto const aseq = env.seq(alice); auto const bseq = env.seq(bruce); - env(escrow(alice, alice, XRP(1000)), - finish_time(env.now() + 1s), - cancel_time(env.now() + 500s)); + env(escrow::create(alice, alice, XRP(1000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 500s)); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -1192,9 +1188,9 @@ struct Escrow_test : public beast::unit_test::suite std::find(aod.begin(), aod.end(), aa) != aod.end()); } - env(escrow(bruce, bruce, XRP(1000)), - finish_time(env.now() + 1s), - cancel_time(env.now() + 2s)); + env(escrow::create(bruce, bruce, XRP(1000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -1210,7 +1206,7 @@ struct Escrow_test : public beast::unit_test::suite } env.close(5s); - env(finish(alice, alice, aseq)); + env(escrow::finish(alice, alice, aseq)); { BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); BEAST_EXPECT( @@ -1229,7 +1225,7 @@ struct Escrow_test : public beast::unit_test::suite } env.close(5s); - env(cancel(bruce, bruce, bseq)); + env(escrow::cancel(bruce, bruce, bseq)); { BEAST_EXPECT(!env.le(keylet::escrow(bruce.id(), bseq))); BEAST_EXPECT( @@ -1245,19 +1241,20 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Metadata to other"); - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bruce, carol); auto const aseq = env.seq(alice); auto const bseq = env.seq(bruce); - env(escrow(alice, bruce, XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create(alice, bruce, XRP(1000)), + escrow::finish_time(env.now() + 1s)); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); env.close(5s); - env(escrow(bruce, carol, XRP(1000)), - finish_time(env.now() + 1s), - cancel_time(env.now() + 2s)); + env(escrow::create(bruce, carol, XRP(1000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -1289,7 +1286,7 @@ struct Escrow_test : public beast::unit_test::suite } env.close(5s); - env(finish(alice, alice, aseq)); + env(escrow::finish(alice, alice, aseq)); { BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); BEAST_EXPECT(env.le(keylet::escrow(bruce.id(), bseq))); @@ -1311,7 +1308,7 @@ struct Escrow_test : public beast::unit_test::suite } env.close(5s); - env(cancel(bruce, bruce, bseq)); + env(escrow::cancel(bruce, bruce, bseq)); { BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); BEAST_EXPECT(!env.le(keylet::escrow(bruce.id(), bseq))); @@ -1335,13 +1332,13 @@ struct Escrow_test : public beast::unit_test::suite } void - testConsequences() + testConsequences(FeatureBitset features) { testcase("Consequences"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.memoize("alice"); @@ -1350,8 +1347,8 @@ struct Escrow_test : public beast::unit_test::suite { auto const jtx = env.jt( - escrow("alice", "carol", XRP(1000)), - finish_time(env.now() + 1s), + escrow::create("alice", "carol", XRP(1000)), + escrow::finish_time(env.now() + 1s), seq(1), fee(baseFee)); auto const pf = preflight( @@ -1368,7 +1365,7 @@ struct Escrow_test : public beast::unit_test::suite { auto const jtx = - env.jt(cancel("bob", "alice", 3), seq(1), fee(baseFee)); + env.jt(escrow::cancel("bob", "alice", 3), seq(1), fee(baseFee)); auto const pf = preflight( env.app(), env.current()->rules(), @@ -1383,7 +1380,7 @@ struct Escrow_test : public beast::unit_test::suite { auto const jtx = - env.jt(finish("bob", "alice", 3), seq(1), fee(baseFee)); + env.jt(escrow::finish("bob", "alice", 3), seq(1), fee(baseFee)); auto const pf = preflight( env.app(), env.current()->rules(), @@ -1398,7 +1395,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testEscrowWithTickets() + testEscrowWithTickets(FeatureBitset features) { testcase("Escrow with tickets"); @@ -1409,7 +1406,7 @@ struct Escrow_test : public beast::unit_test::suite { // Create escrow and finish using tickets. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), alice, bob); env.close(); @@ -1437,8 +1434,8 @@ struct Escrow_test : public beast::unit_test::suite auto const ts = env.now() + 97s; std::uint32_t const escrowSeq = aliceTicket; - env(escrow(alice, bob, XRP(1000)), - finish_time(ts), + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(ts), ticket::use(aliceTicket)); BEAST_EXPECT(env.seq(alice) == aliceRootSeq); env.require(tickets(alice, 0)); @@ -1448,7 +1445,7 @@ struct Escrow_test : public beast::unit_test::suite // prematurely. Note that each tec consumes one of bob's tickets. for (; env.now() < ts; env.close()) { - env(finish(bob, alice, escrowSeq), + env(escrow::finish(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(--bobTicket), ter(tecNO_PERMISSION)); @@ -1456,13 +1453,13 @@ struct Escrow_test : public beast::unit_test::suite } // bob tries to re-use a ticket, which is rejected. - env(finish(bob, alice, escrowSeq), + env(escrow::finish(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(bobTicket), ter(tefNO_TICKET)); // bob uses one of his remaining tickets. Success! - env(finish(bob, alice, escrowSeq), + env(escrow::finish(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(--bobTicket)); env.close(); @@ -1470,7 +1467,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Create escrow and cancel using tickets. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), alice, bob); env.close(); @@ -1497,9 +1494,9 @@ struct Escrow_test : public beast::unit_test::suite auto const ts = env.now() + 117s; std::uint32_t const escrowSeq = aliceTicket; - env(escrow(alice, bob, XRP(1000)), - condition(cb1), - cancel_time(ts), + env(escrow::create(alice, bob, XRP(1000)), + escrow::condition(escrow::cb1), + escrow::cancel_time(ts), ticket::use(aliceTicket)); BEAST_EXPECT(env.seq(alice) == aliceRootSeq); env.require(tickets(alice, 0)); @@ -1509,7 +1506,7 @@ struct Escrow_test : public beast::unit_test::suite // prematurely. for (; env.now() < ts; env.close()) { - env(cancel(bob, alice, escrowSeq), + env(escrow::cancel(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(bobTicket++), ter(tecNO_PERMISSION)); @@ -1517,16 +1514,16 @@ struct Escrow_test : public beast::unit_test::suite } // Verify that a finish won't work anymore. - env(finish(bob, alice, escrowSeq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish(bob, alice, escrowSeq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee), ticket::use(bobTicket++), ter(tecNO_PERMISSION)); BEAST_EXPECT(env.seq(bob) == bobRootSeq); // Verify that the cancel succeeds. - env(cancel(bob, alice, escrowSeq), + env(escrow::cancel(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(bobTicket++)); env.close(); @@ -1538,7 +1535,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testCredentials() + testCredentials(FeatureBitset features) { testcase("Test with credentials"); @@ -1555,12 +1552,13 @@ struct Escrow_test : public beast::unit_test::suite { // Credentials amendment not enabled - Env env(*this, supported_amendments() - featureCredentials); + Env env(*this, features - featureCredentials); env.fund(XRP(5000), alice, bob); env.close(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s)); env.close(); env(fset(bob, asfDepositAuth)); @@ -1571,13 +1569,13 @@ struct Escrow_test : public beast::unit_test::suite std::string const credIdx = "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" "E4"; - env(finish(bob, alice, seq), + env(escrow::finish(bob, alice, seq), credentials::ids({credIdx}), ter(temDISABLED)); } { - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bob, carol, dillon, zelda); env.close(); @@ -1589,7 +1587,8 @@ struct Escrow_test : public beast::unit_test::suite std::string const credIdx = jv[jss::result][jss::index].asString(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 50s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 50s)); env.close(); // Bob require preauthorization @@ -1597,7 +1596,7 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Fail, credentials not accepted - env(finish(carol, alice, seq), + env(escrow::finish(carol, alice, seq), credentials::ids({credIdx}), ter(tecBAD_CREDENTIALS)); @@ -1607,12 +1606,12 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Fail, credentials doesn’t belong to root account - env(finish(dillon, alice, seq), + env(escrow::finish(dillon, alice, seq), credentials::ids({credIdx}), ter(tecBAD_CREDENTIALS)); // Fail, no depositPreauth - env(finish(carol, alice, seq), + env(escrow::finish(carol, alice, seq), credentials::ids({credIdx}), ter(tecNO_PERMISSION)); @@ -1621,7 +1620,7 @@ struct Escrow_test : public beast::unit_test::suite // Success env.close(); - env(finish(carol, alice, seq), credentials::ids({credIdx})); + env(escrow::finish(carol, alice, seq), credentials::ids({credIdx})); env.close(); } @@ -1629,7 +1628,7 @@ struct Escrow_test : public beast::unit_test::suite testcase("Escrow with credentials without depositPreauth"); using namespace std::chrono; - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bob, carol, dillon, zelda); env.close(); @@ -1643,7 +1642,8 @@ struct Escrow_test : public beast::unit_test::suite std::string const credIdx = jv[jss::result][jss::index].asString(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 50s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 50s)); // time advance env.close(); env.close(); @@ -1653,7 +1653,7 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Succeed, Bob doesn't require preauthorization - env(finish(carol, alice, seq), credentials::ids({credIdx})); + env(escrow::finish(carol, alice, seq), credentials::ids({credIdx})); env.close(); { @@ -1669,7 +1669,8 @@ struct Escrow_test : public beast::unit_test::suite .asString(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s)); env.close(); // Bob require preauthorization @@ -1679,27 +1680,38 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Use any valid credentials if account == dst - env(finish(bob, alice, seq), credentials::ids({credIdxBob})); + env(escrow::finish(bob, alice, seq), + credentials::ids({credIdxBob})); env.close(); } } } + void + testWithFeats(FeatureBitset features) + { + testEnablement(features); + testTiming(features); + testTags(features); + testDisallowXRP(features); + test1571(features); + testFails(features); + testLockup(features); + testEscrowConditions(features); + testMetaAndOwnership(features); + testConsequences(features); + testEscrowWithTickets(features); + testCredentials(features); + } + +public: void run() override { - testEnablement(); - testTiming(); - testTags(); - testDisallowXRP(); - test1571(); - testFails(); - testLockup(); - testEscrowConditions(); - testMetaAndOwnership(); - testConsequences(); - testEscrowWithTickets(); - testCredentials(); + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + testWithFeats(all); + testWithFeats(all - featureTokenEscrow); } }; diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index a6055d85f6..deee217aa8 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1694,15 +1694,6 @@ class MPToken_test : public beast::unit_test::suite jv[jss::SendMax] = mpt.getJson(JsonOptions::none); test(jv, jss::SendMax.c_str()); } - // EscrowCreate - { - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Account] = alice.human(); - jv[jss::Destination] = carol.human(); - jv[jss::Amount] = mpt.getJson(JsonOptions::none); - test(jv, jss::Amount.c_str()); - } // OfferCreate { Json::Value jv = offer(alice, USD(100), mpt); diff --git a/src/test/jtx.h b/src/test/jtx.h index fa67780cbd..4188910085 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 042bda39a6..53417a6079 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -472,6 +472,15 @@ public: PrettyAmount balance(Account const& account, Issue const& issue) const; + PrettyAmount + balance(Account const& account, MPTIssue const& mptIssue) const; + + /** Returns the IOU limit on an account. + Returns 0 if the trust line does not exist. + */ + PrettyAmount + limit(Account const& account, Issue const& issue) const; + /** Return the number of objects owned by an account. * Returns 0 if the account does not exist. */ diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index ae46ea4fe3..d4a39b6498 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -233,127 +233,6 @@ expectLedgerEntryRoot( Account const& acct, STAmount const& expectedValue); -/* Escrow */ -/******************************************************************************/ - -Json::Value -escrow(AccountID const& account, AccountID const& to, STAmount const& amount); - -inline Json::Value -escrow(Account const& account, Account const& to, STAmount const& amount) -{ - return escrow(account.id(), to.id(), amount); -} - -Json::Value -finish(AccountID const& account, AccountID const& from, std::uint32_t seq); - -inline Json::Value -finish(Account const& account, Account const& from, std::uint32_t seq) -{ - return finish(account.id(), from.id(), seq); -} - -Json::Value -cancel(AccountID const& account, Account const& from, std::uint32_t seq); - -inline Json::Value -cancel(Account const& account, Account const& from, std::uint32_t seq) -{ - return cancel(account.id(), from, seq); -} - -std::array constexpr cb1 = { - {0xA0, 0x25, 0x80, 0x20, 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, - 0x1C, 0x14, 0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24, - 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95, - 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55, 0x81, 0x01, 0x00}}; - -// A PreimageSha256 fulfillments and its associated condition. -std::array const fb1 = {{0xA0, 0x02, 0x80, 0x00}}; - -/** Set the "FinishAfter" time tag on a JTx */ -struct finish_time -{ -private: - NetClock::time_point value_; - -public: - explicit finish_time(NetClock::time_point const& value) : value_(value) - { - } - - void - operator()(Env&, JTx& jt) const - { - jt.jv[sfFinishAfter.jsonName] = value_.time_since_epoch().count(); - } -}; - -/** Set the "CancelAfter" time tag on a JTx */ -struct cancel_time -{ -private: - NetClock::time_point value_; - -public: - explicit cancel_time(NetClock::time_point const& value) : value_(value) - { - } - - void - operator()(jtx::Env&, jtx::JTx& jt) const - { - jt.jv[sfCancelAfter.jsonName] = value_.time_since_epoch().count(); - } -}; - -struct condition -{ -private: - std::string value_; - -public: - explicit condition(Slice const& cond) : value_(strHex(cond)) - { - } - - template - explicit condition(std::array const& c) - : condition(makeSlice(c)) - { - } - - void - operator()(Env&, JTx& jt) const - { - jt.jv[sfCondition.jsonName] = value_; - } -}; - -struct fulfillment -{ -private: - std::string value_; - -public: - explicit fulfillment(Slice condition) : value_(strHex(condition)) - { - } - - template - explicit fulfillment(std::array f) - : fulfillment(makeSlice(f)) - { - } - - void - operator()(Env&, JTx& jt) const - { - jt.jv[sfFulfillment.jsonName] = value_; - } -}; - /* Payment Channel */ /******************************************************************************/ diff --git a/src/test/jtx/escrow.h b/src/test/jtx/escrow.h new file mode 100644 index 0000000000..3147b44c65 --- /dev/null +++ b/src/test/jtx/escrow.h @@ -0,0 +1,185 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_ESCROW_H_INCLUDED +#define RIPPLE_TEST_JTX_ESCROW_H_INCLUDED + +#include +#include +#include +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Escrow operations. */ +namespace escrow { + +Json::Value +create(AccountID const& account, AccountID const& to, STAmount const& amount); + +inline Json::Value +create(Account const& account, Account const& to, STAmount const& amount) +{ + return create(account.id(), to.id(), amount); +} + +Json::Value +finish(AccountID const& account, AccountID const& from, std::uint32_t seq); + +inline Json::Value +finish(Account const& account, Account const& from, std::uint32_t seq) +{ + return finish(account.id(), from.id(), seq); +} + +Json::Value +cancel(AccountID const& account, Account const& from, std::uint32_t seq); + +inline Json::Value +cancel(Account const& account, Account const& from, std::uint32_t seq) +{ + return cancel(account.id(), from, seq); +} + +Rate +rate(Env& env, Account const& account, std::uint32_t const& seq); + +// A PreimageSha256 fulfillments and its associated condition. +std::array const fb1 = {{0xA0, 0x02, 0x80, 0x00}}; + +std::array const cb1 = { + {0xA0, 0x25, 0x80, 0x20, 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, + 0x1C, 0x14, 0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24, + 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95, + 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55, 0x81, 0x01, 0x00}}; + +// Another PreimageSha256 fulfillments and its associated condition. +std::array const fb2 = { + {0xA0, 0x05, 0x80, 0x03, 0x61, 0x61, 0x61}}; + +std::array const cb2 = { + {0xA0, 0x25, 0x80, 0x20, 0x98, 0x34, 0x87, 0x6D, 0xCF, 0xB0, + 0x5C, 0xB1, 0x67, 0xA5, 0xC2, 0x49, 0x53, 0xEB, 0xA5, 0x8C, + 0x4A, 0xC8, 0x9B, 0x1A, 0xDF, 0x57, 0xF2, 0x8F, 0x2F, 0x9D, + 0x09, 0xAF, 0x10, 0x7E, 0xE8, 0xF0, 0x81, 0x01, 0x03}}; + +// Another PreimageSha256 fulfillment and its associated condition. +std::array const fb3 = { + {0xA0, 0x06, 0x80, 0x04, 0x6E, 0x69, 0x6B, 0x62}}; + +std::array const cb3 = { + {0xA0, 0x25, 0x80, 0x20, 0x6E, 0x4C, 0x71, 0x45, 0x30, 0xC0, + 0xA4, 0x26, 0x8B, 0x3F, 0xA6, 0x3B, 0x1B, 0x60, 0x6F, 0x2D, + 0x26, 0x4A, 0x2D, 0x85, 0x7B, 0xE8, 0xA0, 0x9C, 0x1D, 0xFD, + 0x57, 0x0D, 0x15, 0x85, 0x8B, 0xD4, 0x81, 0x01, 0x04}}; + +/** Set the "FinishAfter" time tag on a JTx */ +struct finish_time +{ +private: + NetClock::time_point value_; + +public: + explicit finish_time(NetClock::time_point const& value) : value_(value) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfFinishAfter.jsonName] = value_.time_since_epoch().count(); + } +}; + +/** Set the "CancelAfter" time tag on a JTx */ +struct cancel_time +{ +private: + NetClock::time_point value_; + +public: + explicit cancel_time(NetClock::time_point const& value) : value_(value) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv[sfCancelAfter.jsonName] = value_.time_since_epoch().count(); + } +}; + +struct condition +{ +private: + std::string value_; + +public: + explicit condition(Slice const& cond) : value_(strHex(cond)) + { + } + + template + explicit condition(std::array const& c) + : condition(makeSlice(c)) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfCondition.jsonName] = value_; + } +}; + +struct fulfillment +{ +private: + std::string value_; + +public: + explicit fulfillment(Slice condition) : value_(strHex(condition)) + { + } + + template + explicit fulfillment(std::array f) + : fulfillment(makeSlice(f)) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfFulfillment.jsonName] = value_; + } +}; + +} // namespace escrow + +} // namespace jtx + +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/flags.h b/src/test/jtx/flags.h index 09e5dac52f..aa048c3e55 100644 --- a/src/test/jtx/flags.h +++ b/src/test/jtx/flags.h @@ -96,6 +96,9 @@ private: case asfDisallowIncomingTrustline: mask_ |= lsfDisallowIncomingTrustline; break; + case asfAllowTrustLineLocking: + mask_ |= lsfAllowTrustLineLocking; + break; default: Throw("unknown flag"); } diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index e45042e310..58d26da26e 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -199,6 +199,48 @@ Env::balance(Account const& account, Issue const& issue) const return {amount, lookup(issue.account).name()}; } +PrettyAmount +Env::balance(Account const& account, MPTIssue const& mptIssue) const +{ + MPTID const id = mptIssue.getMptID(); + if (!id) + return {STAmount(mptIssue, 0), account.name()}; + + AccountID const issuer = mptIssue.getIssuer(); + if (account.id() == issuer) + { + // Issuer balance + auto const sle = le(keylet::mptIssuance(id)); + if (!sle) + return {STAmount(mptIssue, 0), account.name()}; + + STAmount const amount{mptIssue, sle->getFieldU64(sfOutstandingAmount)}; + return {amount, lookup(issuer).name()}; + } + else + { + // Holder balance + auto const sle = le(keylet::mptoken(id, account)); + if (!sle) + return {STAmount(mptIssue, 0), account.name()}; + + STAmount const amount{mptIssue, sle->getFieldU64(sfMPTAmount)}; + return {amount, lookup(issuer).name()}; + } +} + +PrettyAmount +Env::limit(Account const& account, Issue const& issue) const +{ + auto const sle = le(keylet::line(account.id(), issue)); + if (!sle) + return {STAmount(issue, 0), account.name()}; + auto const aHigh = account.id() > issue.account; + if (sle && sle->isFieldPresent(aHigh ? sfLowLimit : sfHighLimit)) + return {(*sle)[aHigh ? sfLowLimit : sfHighLimit], account.name()}; + return {STAmount(issue, 0), account.name()}; +} + std::uint32_t Env::ownerCount(Account const& account) const { diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index cb8141b9f3..5f8c53877a 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -211,42 +211,6 @@ expectLedgerEntryRoot( return accountBalance(env, acct) == to_string(expectedValue.xrp()); } -/* Escrow */ -/******************************************************************************/ - -Json::Value -escrow(AccountID const& account, AccountID const& to, STAmount const& amount) -{ - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Account] = to_string(account); - jv[jss::Destination] = to_string(to); - jv[jss::Amount] = amount.getJson(JsonOptions::none); - return jv; -} - -Json::Value -finish(AccountID const& account, AccountID const& from, std::uint32_t seq) -{ - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowFinish; - jv[jss::Account] = to_string(account); - jv[sfOwner.jsonName] = to_string(from); - jv[sfOfferSequence.jsonName] = seq; - return jv; -} - -Json::Value -cancel(AccountID const& account, Account const& from, std::uint32_t seq) -{ - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCancel; - jv[jss::Account] = to_string(account); - jv[sfOwner.jsonName] = from.human(); - jv[sfOfferSequence.jsonName] = seq; - return jv; -} - /* Payment Channel */ /******************************************************************************/ Json::Value diff --git a/src/test/jtx/impl/escrow.cpp b/src/test/jtx/impl/escrow.cpp new file mode 100644 index 0000000000..a1ec6a3c5e --- /dev/null +++ b/src/test/jtx/impl/escrow.cpp @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Escrow operations. */ +namespace escrow { + +Json::Value +create(AccountID const& account, AccountID const& to, STAmount const& amount) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowCreate; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv[jss::Destination] = to_string(to); + jv[jss::Amount] = amount.getJson(JsonOptions::none); + return jv; +} + +Json::Value +finish(AccountID const& account, AccountID const& from, std::uint32_t seq) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowFinish; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv[sfOwner.jsonName] = to_string(from); + jv[sfOfferSequence.jsonName] = seq; + return jv; +} + +Json::Value +cancel(AccountID const& account, Account const& from, std::uint32_t seq) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowCancel; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv[sfOwner.jsonName] = from.human(); + jv[sfOfferSequence.jsonName] = seq; + return jv; +} + +Rate +rate(Env& env, Account const& account, std::uint32_t const& seq) +{ + auto const sle = env.le(keylet::escrow(account.id(), seq)); + if (sle->isFieldPresent(sfTransferRate)) + return ripple::Rate((*sle)[sfTransferRate]); + return Rate{0}; +} + +} // namespace escrow + +} // namespace jtx + +} // namespace test +} // namespace ripple diff --git a/src/test/ledger/Invariants_test.cpp b/src/test/ledger/Invariants_test.cpp index 76fcb34c7f..ebd2235cc9 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -731,21 +731,6 @@ class Invariants_test : public beast::unit_test::suite using namespace test::jtx; testcase << "no zero escrow"; - doInvariantCheck( - {{"Cannot return non-native STAmount as XRPAmount"}}, - [](Account const& A1, Account const& A2, ApplyContext& ac) { - // escrow with nonnative amount - auto const sle = ac.view().peek(keylet::account(A1.id())); - if (!sle) - return false; - auto sleNew = std::make_shared( - keylet::escrow(A1, (*sle)[sfSequence] + 2)); - STAmount nonNative(A2["USD"](51)); - sleNew->setFieldAmount(sfAmount, nonNative); - ac.view().insert(sleNew); - return true; - }); - doInvariantCheck( {{"XRP net change of -1000000 doesn't match fee 0"}, {"escrow specifies invalid amount"}}, diff --git a/src/test/protocol/STAmount_test.cpp b/src/test/protocol/STAmount_test.cpp index 712c91000e..d62241f2f4 100644 --- a/src/test/protocol/STAmount_test.cpp +++ b/src/test/protocol/STAmount_test.cpp @@ -17,6 +17,8 @@ */ //============================================================================== +#include + #include #include #include @@ -668,6 +670,366 @@ public: } } + void + testCanAddXRP() + { + testcase("can add xrp"); + + // Adding zero + { + STAmount amt1(XRPAmount(0)); + STAmount amt2(XRPAmount(1000)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding zero + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(XRPAmount(0)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding two positive XRP amounts + { + STAmount amt1(XRPAmount(500)); + STAmount amt2(XRPAmount(1500)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding two negative XRP amounts + { + STAmount amt1(XRPAmount(-500)); + STAmount amt2(XRPAmount(-1500)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding a positive and a negative XRP amount + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(XRPAmount(-1000)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Overflow check for max XRP amounts + { + STAmount amt1(std::numeric_limits::max()); + STAmount amt2(XRPAmount(1)); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Overflow check for min XRP amounts + { + STAmount amt1(std::numeric_limits::max()); + amt1 += XRPAmount(1); + STAmount amt2(XRPAmount(-1)); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + } + + void + testCanAddIOU() + { + testcase("can add iou"); + + Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)}; + Issue const eur{Currency(0x4555520000000000), AccountID(0x4985601)}; + + // Adding two IOU amounts + { + STAmount amt1(usd, 500); + STAmount amt2(usd, 1500); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding a positive and a negative IOU amount + { + STAmount amt1(usd, 1000); + STAmount amt2(usd, -1000); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Overflow check for max IOU amounts + { + STAmount amt1(usd, std::numeric_limits::max()); + STAmount amt2(usd, 1); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Overflow check for min IOU amounts + { + STAmount amt1(usd, std::numeric_limits::min()); + STAmount amt2(usd, -1); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding XRP and IOU + { + STAmount amt1(XRPAmount(1)); + STAmount amt2(usd, 1); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding different IOU issues (non zero) + { + STAmount amt1(usd, 1000); + STAmount amt2(eur, 500); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding different IOU issues (zero) + { + STAmount amt1(usd, 0); + STAmount amt2(eur, 500); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + } + + void + testCanAddMPT() + { + testcase("can add mpt"); + + MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + MPTIssue const mpt2{MPTIssue{makeMptID(2, AccountID(0x4985601))}}; + + // Adding zero + { + STAmount amt1(mpt, 0); + STAmount amt2(mpt, 1000); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding zero + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt, 0); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding two positive MPT amounts + { + STAmount amt1(mpt, 500); + STAmount amt2(mpt, 1500); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding two negative MPT amounts + { + STAmount amt1(mpt, -500); + STAmount amt2(mpt, -1500); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding a positive and a negative MPT amount + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt, -1000); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Overflow check for max MPT amounts + { + STAmount amt1( + mpt, std::numeric_limits::max()); + STAmount amt2(mpt, 1); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Overflow check for min MPT amounts + // Note: Cannot check min MPT overflow because you cannot initialize the + // STAmount with a negative MPT amount. + + // Adding MPT and XRP + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(mpt, 1000); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding different MPT issues (non zero) + { + STAmount amt1(mpt2, 500); + STAmount amt2(mpt, 500); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding different MPT issues (non zero) + { + STAmount amt1(mpt2, 0); + STAmount amt2(mpt, 500); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + } + + void + testCanSubtractXRP() + { + testcase("can subtract xrp"); + + // Subtracting zero + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(XRPAmount(0)); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting zero + { + STAmount amt1(XRPAmount(0)); + STAmount amt2(XRPAmount(1000)); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting two positive XRP amounts + { + STAmount amt1(XRPAmount(1500)); + STAmount amt2(XRPAmount(500)); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting two negative XRP amounts + { + STAmount amt1(XRPAmount(-1500)); + STAmount amt2(XRPAmount(-500)); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting a positive and a negative XRP amount + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(XRPAmount(-1000)); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Underflow check for min XRP amounts + { + STAmount amt1(std::numeric_limits::max()); + amt1 += XRPAmount(1); + STAmount amt2(XRPAmount(1)); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Overflow check for max XRP amounts + { + STAmount amt1(std::numeric_limits::max()); + STAmount amt2(XRPAmount(-1)); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + } + + void + testCanSubtractIOU() + { + testcase("can subtract iou"); + Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)}; + Issue const eur{Currency(0x4555520000000000), AccountID(0x4985601)}; + + // Subtracting two IOU amounts + { + STAmount amt1(usd, 1500); + STAmount amt2(usd, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting XRP and IOU + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(usd, 1000); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting different IOU issues (non zero) + { + STAmount amt1(usd, 1000); + STAmount amt2(eur, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting different IOU issues (zero) + { + STAmount amt1(usd, 0); + STAmount amt2(eur, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + } + + void + testCanSubtractMPT() + { + testcase("can subtract mpt"); + + MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + MPTIssue const mpt2{MPTIssue{makeMptID(2, AccountID(0x4985601))}}; + + // Subtracting zero + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt, 0); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting zero + { + STAmount amt1(mpt, 0); + STAmount amt2(mpt, 1000); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting two positive MPT amounts + { + STAmount amt1(mpt, 1500); + STAmount amt2(mpt, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting two negative MPT amounts + { + STAmount amt1(mpt, -1500); + STAmount amt2(mpt, -500); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting a positive and a negative MPT amount + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt, -1000); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Underflow check for min MPT amounts + // Note: Cannot check min MPT underflow because you cannot initialize + // the STAmount with a negative MPT amount. + + // Overflow check for max positive MPT amounts (should fail) + { + STAmount amt1( + mpt, std::numeric_limits::max()); + STAmount amt2(mpt, -2); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting MPT and XRP + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(mpt, 1000); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting different MPT issues (non zero) + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt2, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting different MPT issues (zero) + { + STAmount amt1(mpt, 0); + STAmount amt2(mpt2, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + } + //-------------------------------------------------------------------------- void @@ -681,6 +1043,12 @@ public: testRounding(); testConvertXRP(); testConvertIOU(); + testCanAddXRP(); + testCanAddIOU(); + testCanAddMPT(); + testCanSubtractXRP(); + testCanSubtractIOU(); + testCanSubtractMPT(); } }; diff --git a/src/test/rpc/AccountLines_test.cpp b/src/test/rpc/AccountLines_test.cpp index 42acea4111..9215f4087a 100644 --- a/src/test/rpc/AccountLines_test.cpp +++ b/src/test/rpc/AccountLines_test.cpp @@ -573,21 +573,6 @@ public: env.fund(XRP(10000), alice, becky, gw1); env.close(); - // A couple of helper lambdas - auto escrow = [&env]( - Account const& account, - Account const& to, - STAmount const& amount) { - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Account] = account.human(); - jv[jss::Destination] = to.human(); - jv[jss::Amount] = amount.getJson(JsonOptions::none); - NetClock::time_point finish = env.now() + 1s; - jv[sfFinishAfter.jsonName] = finish.time_since_epoch().count(); - return jv; - }; - auto payChan = [](Account const& account, Account const& to, STAmount const& amount, @@ -623,8 +608,10 @@ public: env.close(); // Escrow, in each direction - env(escrow(alice, becky, XRP(1000))); - env(escrow(becky, alice, XRP(1000))); + env(escrow::create(alice, becky, XRP(1000)), + escrow::finish_time(env.now() + 1s)); + env(escrow::create(becky, alice, XRP(1000)), + escrow::finish_time(env.now() + 1s)); // Pay channels, in each direction env(payChan(alice, becky, XRP(1000), 100s, alice.pk())); diff --git a/src/test/rpc/AccountSet_test.cpp b/src/test/rpc/AccountSet_test.cpp index 7bca51ae96..c056279bf1 100644 --- a/src/test/rpc/AccountSet_test.cpp +++ b/src/test/rpc/AccountSet_test.cpp @@ -99,6 +99,12 @@ public: // is tested elsewhere. continue; } + if (flag == asfAllowTrustLineLocking) + { + // These flags are part of the AllowTokenLocking amendment + // and are tested elsewhere + continue; + } if (std::find(goodFlags.begin(), goodFlags.end(), flag) != goodFlags.end()) diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index 0b58957fcf..f7840650f7 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include @@ -79,7 +81,41 @@ namespace ripple { TxConsequences EscrowCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ + ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::zero}; +} + +template +static NotTEC +escrowCreatePreflightHelper(PreflightContext const& ctx); + +template <> +NotTEC +escrowCreatePreflightHelper(PreflightContext const& ctx) +{ + STAmount const amount = ctx.tx[sfAmount]; + if (amount.native() || amount <= beast::zero) + return temBAD_AMOUNT; + + if (badCurrency() == amount.getCurrency()) + return temBAD_CURRENCY; + + return tesSUCCESS; +} + +template <> +NotTEC +escrowCreatePreflightHelper(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + auto const amount = ctx.tx[sfAmount]; + if (amount.native() || amount.mpt() > MPTAmount{maxMPTokenAmount} || + amount <= beast::zero) + return temBAD_AMOUNT; + + return tesSUCCESS; } NotTEC @@ -91,11 +127,25 @@ EscrowCreate::preflight(PreflightContext const& ctx) if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; - if (!isXRP(ctx.tx[sfAmount])) - return temBAD_AMOUNT; + STAmount const amount{ctx.tx[sfAmount]}; + if (!isXRP(amount)) + { + if (!ctx.rules.enabled(featureTokenEscrow)) + return temBAD_AMOUNT; - if (ctx.tx[sfAmount] <= beast::zero) - return temBAD_AMOUNT; + if (auto const ret = std::visit( + [&](T const&) { + return escrowCreatePreflightHelper(ctx); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + else + { + if (amount <= beast::zero) + return temBAD_AMOUNT; + } // We must specify at least one timeout value if (!ctx.tx[~sfCancelAfter] && !ctx.tx[~sfFinishAfter]) @@ -142,10 +192,181 @@ EscrowCreate::preflight(PreflightContext const& ctx) return preflight2(ctx); } +template +static TER +escrowCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount); + +template <> +TER +escrowCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecNO_PERMISSION + if (issuer == account) + return tecNO_PERMISSION; + + // If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION + auto const sleIssuer = ctx.view.read(keylet::account(issuer)); + if (!sleIssuer) + return tecNO_ISSUER; + if (!sleIssuer->isFlag(lsfAllowTrustLineLocking)) + return tecNO_PERMISSION; + + // If the account does not have a trustline to the issuer, return tecNO_LINE + auto const sleRippleState = + ctx.view.read(keylet::line(account, issuer, amount.getCurrency())); + if (!sleRippleState) + return tecNO_LINE; + + STAmount const balance = (*sleRippleState)[sfBalance]; + + // If balance is positive, issuer must have higher address than account + if (balance > beast::zero && issuer < account) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If balance is negative, issuer must have lower address than account + if (balance < beast::zero && issuer > account) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If the issuer has requireAuth set, check if the account is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), account); + ter != tesSUCCESS) + return ter; + + // If the issuer has requireAuth set, check if the destination is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); + ter != tesSUCCESS) + return ter; + + // If the issuer has frozen the account, return tecFROZEN + if (isFrozen(ctx.view, account, amount.issue())) + return tecFROZEN; + + // If the issuer has frozen the destination, return tecFROZEN + if (isFrozen(ctx.view, dest, amount.issue())) + return tecFROZEN; + + STAmount const spendableAmount = accountHolds( + ctx.view, + account, + amount.getCurrency(), + issuer, + fhIGNORE_FREEZE, + ctx.j); + + // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS + if (spendableAmount <= beast::zero) + return tecINSUFFICIENT_FUNDS; + + // If the spendable amount is less than the amount, return + // tecINSUFFICIENT_FUNDS + if (spendableAmount < amount) + return tecINSUFFICIENT_FUNDS; + + // If the amount is not addable to the balance, return tecPRECISION_LOSS + if (!canAdd(spendableAmount, amount)) + return tecPRECISION_LOSS; + + return tesSUCCESS; +} + +template <> +TER +escrowCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecNO_PERMISSION + if (issuer == account) + return tecNO_PERMISSION; + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = + keylet::mptIssuance(amount.get().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION + if (!sleIssuance->isFlag(lsfMPTCanEscrow)) + return tecNO_PERMISSION; + + // If the issuer is not the same as the issuer of the mpt, return + // tecNO_PERMISSION + if (sleIssuance->getAccountID(sfIssuer) != issuer) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If the account does not have the mpt, return tecOBJECT_NOT_FOUND + if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account))) + return tecOBJECT_NOT_FOUND; + + // If the issuer has requireAuth set, check if the account is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = + requireAuth(ctx.view, mptIssue, account, MPTAuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + // If the issuer has requireAuth set, check if the destination is + // authorized + if (auto const ter = + requireAuth(ctx.view, mptIssue, dest, MPTAuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + // If the issuer has frozen the account, return tecLOCKED + if (isFrozen(ctx.view, account, mptIssue)) + return tecLOCKED; + + // If the issuer has frozen the destination, return tecLOCKED + if (isFrozen(ctx.view, dest, mptIssue)) + return tecLOCKED; + + // If the mpt cannot be transferred, return tecNO_AUTH + if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); + ter != tesSUCCESS) + return ter; + + STAmount const spendableAmount = accountHolds( + ctx.view, + account, + amount.get(), + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + ctx.j); + + // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS + if (spendableAmount <= beast::zero) + return tecINSUFFICIENT_FUNDS; + + // If the spendable amount is less than the amount, return + // tecINSUFFICIENT_FUNDS + if (spendableAmount < amount) + return tecINSUFFICIENT_FUNDS; + + return tesSUCCESS; +} + TER EscrowCreate::preclaim(PreclaimContext const& ctx) { - auto const sled = ctx.view.read(keylet::account(ctx.tx[sfDestination])); + STAmount const amount{ctx.tx[sfAmount]}; + AccountID const account{ctx.tx[sfAccount]}; + AccountID const dest{ctx.tx[sfDestination]}; + + auto const sled = ctx.view.read(keylet::account(dest)); if (!sled) return tecNO_DST; @@ -156,6 +377,77 @@ EscrowCreate::preclaim(PreclaimContext const& ctx) if (isPseudoAccount(sled)) return tecNO_PERMISSION; + if (!isXRP(amount)) + { + if (!ctx.view.rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + if (auto const ret = std::visit( + [&](T const&) { + return escrowCreatePreclaimHelper( + ctx, account, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + return tesSUCCESS; +} + +template +static TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal); + +template <> +TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create an escrow + // LCOV_EXCL_START + if (issuer == sender) + return tecINTERNAL; + // LCOV_EXCL_STOP + + auto const ter = rippleCredit( + view, + sender, + issuer, + amount, + amount.holds() ? false : true, + journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE + return tesSUCCESS; +} + +template <> +TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create an escrow + // LCOV_EXCL_START + if (issuer == sender) + return tecINTERNAL; + // LCOV_EXCL_STOP + + auto const ter = rippleLockEscrowMPT(view, sender, amount, journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE return tesSUCCESS; } @@ -196,21 +488,23 @@ EscrowCreate::doApply() } } - auto const account = ctx_.tx[sfAccount]; - auto const sle = ctx_.view().peek(keylet::account(account)); + auto const sle = ctx_.view().peek(keylet::account(account_)); if (!sle) - return tefINTERNAL; + return tefINTERNAL; // LCOV_EXCL_LINE // Check reserve and funds availability + STAmount const amount{ctx_.tx[sfAmount]}; + + auto const reserve = + ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); + + if (mSourceBalance < reserve) + return tecINSUFFICIENT_RESERVE; + + // Check reserve and funds availability + if (isXRP(amount)) { - auto const balance = STAmount((*sle)[sfBalance]).xrp(); - auto const reserve = - ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); - - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; - - if (balance < reserve + STAmount(ctx_.tx[sfAmount]).xrp()) + if (mSourceBalance < reserve + STAmount(amount).xrp()) return tecUNFUNDED; } @@ -233,10 +527,10 @@ EscrowCreate::doApply() // Create escrow in ledger. Note that we we use the value from the // sequence or ticket. For more explanation see comments in SeqProxy.h. - Keylet const escrowKeylet = keylet::escrow(account, ctx_.tx.getSeqValue()); + Keylet const escrowKeylet = keylet::escrow(account_, ctx_.tx.getSeqValue()); auto const slep = std::make_shared(escrowKeylet); - (*slep)[sfAmount] = ctx_.tx[sfAmount]; - (*slep)[sfAccount] = account; + (*slep)[sfAmount] = amount; + (*slep)[sfAccount] = account_; (*slep)[~sfCondition] = ctx_.tx[~sfCondition]; (*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag]; (*slep)[sfDestination] = ctx_.tx[sfDestination]; @@ -244,32 +538,69 @@ EscrowCreate::doApply() (*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter]; (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + if (ctx_.view().rules().enabled(featureTokenEscrow) && !isXRP(amount)) + { + auto const xferRate = transferRate(ctx_.view(), amount); + if (xferRate != parityRate) + (*slep)[sfTransferRate] = xferRate.value; + } + ctx_.view().insert(slep); // Add escrow to sender's owner directory { auto page = ctx_.view().dirInsert( - keylet::ownerDir(account), escrowKeylet, describeOwnerDir(account)); + keylet::ownerDir(account_), + escrowKeylet, + describeOwnerDir(account_)); if (!page) - return tecDIR_FULL; + return tecDIR_FULL; // LCOV_EXCL_LINE (*slep)[sfOwnerNode] = *page; } // If it's not a self-send, add escrow to recipient's owner directory. - if (auto const dest = ctx_.tx[sfDestination]; dest != ctx_.tx[sfAccount]) + AccountID const dest = ctx_.tx[sfDestination]; + if (dest != account_) { auto page = ctx_.view().dirInsert( keylet::ownerDir(dest), escrowKeylet, describeOwnerDir(dest)); if (!page) - return tecDIR_FULL; + return tecDIR_FULL; // LCOV_EXCL_LINE (*slep)[sfDestinationNode] = *page; } - // Deduct owner's balance, increment owner count - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + // IOU escrow objects are added to the issuer's owner directory to help + // track the total locked balance. For MPT, this isn't necessary because the + // locked balance is already stored directly in the MPTokenIssuance object. + AccountID const issuer = amount.getIssuer(); + if (!isXRP(amount) && issuer != account_ && issuer != dest && + !amount.holds()) + { + auto page = ctx_.view().dirInsert( + keylet::ownerDir(issuer), escrowKeylet, describeOwnerDir(issuer)); + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*slep)[sfIssuerNode] = *page; + } + + // Deduct owner's balance + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] - amount; + else + { + if (auto const ret = std::visit( + [&](T const&) { + return escrowLockApplyHelper( + ctx_.view(), issuer, account_, amount, j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + + // increment owner count adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); ctx_.view().update(sle); - return tesSUCCESS; } @@ -360,19 +691,324 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx) return Transactor::calculateBaseFee(view, tx) + extraFee; } +template +static TER +escrowFinishPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& dest, + STAmount const& amount); + +template <> +TER +escrowFinishPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tesSUCCESS + if (issuer == dest) + return tesSUCCESS; + + // If the issuer has requireAuth set, check if the destination is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); + ter != tesSUCCESS) + return ter; + + // If the issuer has deep frozen the destination, return tecFROZEN + if (isDeepFrozen(ctx.view, dest, amount.getCurrency(), amount.getIssuer())) + return tecFROZEN; + + return tesSUCCESS; +} + +template <> +TER +escrowFinishPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the dest, return tesSUCCESS + if (issuer == dest) + return tesSUCCESS; + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = + keylet::mptIssuance(amount.get().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the issuer has requireAuth set, check if the destination is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = + requireAuth(ctx.view, mptIssue, dest, MPTAuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + // If the issuer has frozen the destination, return tecLOCKED + if (isFrozen(ctx.view, dest, mptIssue)) + return tecLOCKED; + + return tesSUCCESS; +} + TER EscrowFinish::preclaim(PreclaimContext const& ctx) { - if (!ctx.view.rules().enabled(featureCredentials)) - return Transactor::preclaim(ctx); + if (ctx.view.rules().enabled(featureCredentials)) + { + if (auto const err = credentials::valid(ctx, ctx.tx[sfAccount]); + !isTesSuccess(err)) + return err; + } - if (auto const err = credentials::valid(ctx, ctx.tx[sfAccount]); - !isTesSuccess(err)) - return err; + auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]); + auto const slep = ctx.view.read(k); + if (!slep) + return tecNO_TARGET; + AccountID const dest = (*slep)[sfDestination]; + STAmount const amount = (*slep)[sfAmount]; + + if (!isXRP(amount)) + { + if (!ctx.view.rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + if (auto const ret = std::visit( + [&](T const&) { + return escrowFinishPreclaimHelper(ctx, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } return tesSUCCESS; } +template +static TER +escrowUnlockApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& xrpBalance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& sender, + AccountID const& receiver, + bool createAsset, + beast::Journal journal); + +template <> +TER +escrowUnlockApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& xrpBalance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& sender, + AccountID const& receiver, + bool createAsset, + beast::Journal journal) +{ + Keylet const trustLineKey = keylet::line(receiver, amount.issue()); + bool const recvLow = issuer > receiver; + bool const senderIssuer = issuer == sender; + bool const receiverIssuer = issuer == receiver; + bool const issuerHigh = issuer > receiver; + + // LCOV_EXCL_START + if (senderIssuer) + return tecINTERNAL; + // LCOV_EXCL_STOP + + if (receiverIssuer) + return tesSUCCESS; + + if (!view.exists(trustLineKey) && createAsset && !receiverIssuer) + { + // Can the account cover the trust line's reserve? + if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; + xrpBalance < view.fees().accountReserve(ownerCount + 1)) + { + JLOG(journal.trace()) << "Trust line does not exist. " + "Insufficent reserve to create line."; + + return tecNO_LINE_INSUF_RESERVE; + } + + Currency const currency = amount.getCurrency(); + STAmount initialBalance(amount.issue()); + initialBalance.setIssuer(noAccount()); + + // clang-format off + if (TER const ter = trustCreate( + view, // payment sandbox + recvLow, // is dest low? + issuer, // source + receiver, // destination + trustLineKey.key, // ledger index + sleDest, // Account to add to + false, // authorize account + (sleDest->getFlags() & lsfDefaultRipple) == 0, + false, // freeze trust line + false, // deep freeze trust line + initialBalance, // zero initial balance + Issue(currency, receiver), // limit of zero + 0, // quality in + 0, // quality out + journal); // journal + !isTesSuccess(ter)) + { + return ter; // LCOV_EXCL_LINE + } + // clang-format on + + view.update(sleDest); + } + + if (!view.exists(trustLineKey) && !receiverIssuer) + return tecNO_LINE; + + auto const xferRate = transferRate(view, amount); + // update if issuer rate is less than locked rate + if (xferRate < lockedRate) + lockedRate = xferRate; + + // Transfer Rate only applies when: + // 1. Issuer is not involved in the transfer (senderIssuer or + // receiverIssuer) + // 2. The locked rate is different from the parity rate + + // NOTE: Transfer fee in escrow works a bit differently from a normal + // payment. In escrow, the fee is deducted from the locked/sending amount, + // whereas in a normal payment, the transfer fee is taken on top of the + // sending amount. + auto finalAmt = amount; + if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate) + { + // compute transfer fee, if any + auto const xferFee = amount.value() - + divideRound(amount, lockedRate, amount.issue(), true); + // compute balance to transfer + finalAmt = amount.value() - xferFee; + } + + // validate the line limit if the account submitting txn is not the receiver + // of the funds + if (!createAsset) + { + auto const sleRippleState = view.peek(trustLineKey); + if (!sleRippleState) + return tecINTERNAL; // LCOV_EXCL_LINE + + // if the issuer is the high, then we use the low limit + // otherwise we use the high limit + STAmount const lineLimit = sleRippleState->getFieldAmount( + issuerHigh ? sfLowLimit : sfHighLimit); + + STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance); + + // flip the sign of the line balance if the issuer is not high + if (!issuerHigh) + lineBalance.negate(); + + // add the final amount to the line balance + lineBalance += finalAmt; + + // if the transfer would exceed the line limit return tecLIMIT_EXCEEDED + if (lineLimit < lineBalance) + return tecLIMIT_EXCEEDED; + } + + // if destination is not the issuer then transfer funds + if (!receiverIssuer) + { + auto const ter = + rippleCredit(view, issuer, receiver, finalAmt, true, journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE + } + return tesSUCCESS; +} + +template <> +TER +escrowUnlockApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& xrpBalance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& sender, + AccountID const& receiver, + bool createAsset, + beast::Journal journal) +{ + bool const senderIssuer = issuer == sender; + bool const receiverIssuer = issuer == receiver; + + auto const mptID = amount.get().getMptID(); + auto const issuanceKey = keylet::mptIssuance(mptID); + if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && + createAsset && !receiverIssuer) + { + if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; + xrpBalance < view.fees().accountReserve(ownerCount + 1)) + { + return tecINSUFFICIENT_RESERVE; + } + + if (auto const ter = + MPTokenAuthorize::createMPToken(view, mptID, receiver, 0); + !isTesSuccess(ter)) + { + return ter; // LCOV_EXCL_LINE + } + + // update owner count. + adjustOwnerCount(view, sleDest, 1, journal); + } + + if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && + !receiverIssuer) + return tecNO_PERMISSION; + + auto const xferRate = transferRate(view, amount); + // update if issuer rate is less than locked rate + if (xferRate < lockedRate) + lockedRate = xferRate; + + // Transfer Rate only applies when: + // 1. Issuer is not involved in the transfer (senderIssuer or + // receiverIssuer) + // 2. The locked rate is different from the parity rate + + // NOTE: Transfer fee in escrow works a bit differently from a normal + // payment. In escrow, the fee is deducted from the locked/sending amount, + // whereas in a normal payment, the transfer fee is taken on top of the + // sending amount. + auto finalAmt = amount; + if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate) + { + // compute transfer fee, if any + auto const xferFee = amount.value() - + divideRound(amount, lockedRate, amount.asset(), true); + // compute balance to transfer + finalAmt = amount.value() - xferFee; + } + + return rippleUnlockEscrowMPT(view, sender, receiver, finalAmt, journal); +} + TER EscrowFinish::doApply() { @@ -495,8 +1131,50 @@ EscrowFinish::doApply() } } + STAmount const amount = slep->getFieldAmount(sfAmount); // Transfer amount to destination - (*sled)[sfBalance] = (*sled)[sfBalance] + (*slep)[sfAmount]; + if (isXRP(amount)) + (*sled)[sfBalance] = (*sled)[sfBalance] + amount; + else + { + if (!ctx_.view().rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + Rate lockedRate = slep->isFieldPresent(sfTransferRate) + ? ripple::Rate(slep->getFieldU32(sfTransferRate)) + : parityRate; + auto const issuer = amount.getIssuer(); + bool const createAsset = destID == account_; + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockApplyHelper( + ctx_.view(), + lockedRate, + sled, + mPriorBalance, + amount, + issuer, + account, + destID, + createAsset, + j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + + // Remove escrow from issuers owner directory, if present. + if (auto const optPage = (*slep)[~sfIssuerNode]; optPage) + { + if (!ctx_.view().dirRemove( + keylet::ownerDir(issuer), *optPage, k.key, true)) + { + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; + return tefBAD_LEDGER; // LCOV_EXCL_LINE + } + } + } + ctx_.view().update(sled); // Adjust source owner count @@ -506,7 +1184,6 @@ EscrowFinish::doApply() // Remove escrow from ledger ctx_.view().erase(slep); - return tesSUCCESS; } @@ -524,6 +1201,90 @@ EscrowCancel::preflight(PreflightContext const& ctx) return preflight2(ctx); } +template +static TER +escrowCancelPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + STAmount const& amount); + +template <> +TER +escrowCancelPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecINTERNAL + if (issuer == account) + return tecINTERNAL; // LCOV_EXCL_LINE + + // If the issuer has requireAuth set, check if the account is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), account); + ter != tesSUCCESS) + return ter; + + return tesSUCCESS; +} + +template <> +TER +escrowCancelPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecINTERNAL + if (issuer == account) + return tecINTERNAL; // LCOV_EXCL_LINE + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = + keylet::mptIssuance(amount.get().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + + // If the issuer has requireAuth set, check if the account is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = + requireAuth(ctx.view, mptIssue, account, MPTAuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + return tesSUCCESS; +} + +TER +EscrowCancel::preclaim(PreclaimContext const& ctx) +{ + auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]); + auto const slep = ctx.view.read(k); + if (!slep) + return tecNO_TARGET; + + AccountID const account = (*slep)[sfAccount]; + STAmount const amount = (*slep)[sfAmount]; + + if (!isXRP(amount)) + { + if (!ctx.view.rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + if (auto const ret = std::visit( + [&](T const&) { + return escrowCancelPreclaimHelper(ctx, account, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + return tesSUCCESS; +} + TER EscrowCancel::doApply() { @@ -580,9 +1341,49 @@ EscrowCancel::doApply() } } - // Transfer amount back to owner, decrement owner count auto const sle = ctx_.view().peek(keylet::account(account)); - (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount]; + STAmount const amount = slep->getFieldAmount(sfAmount); + + // Transfer amount back to the owner + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] + amount; + else + { + if (!ctx_.view().rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + auto const issuer = amount.getIssuer(); + bool const createAsset = account == account_; + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockApplyHelper( + ctx_.view(), + parityRate, + slep, + mPriorBalance, + amount, + issuer, + account, // sender and receiver are the same + account, + createAsset, + j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; // LCOV_EXCL_LINE + + // Remove escrow from issuers owner directory, if present. + if (auto const optPage = (*slep)[~sfIssuerNode]; optPage) + { + if (!ctx_.view().dirRemove( + keylet::ownerDir(issuer), *optPage, k.key, true)) + { + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; + return tefBAD_LEDGER; // LCOV_EXCL_LINE + } + } + } + adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); ctx_.view().update(sle); diff --git a/src/xrpld/app/tx/detail/Escrow.h b/src/xrpld/app/tx/detail/Escrow.h index 78acdbee00..2225c94f16 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -84,6 +84,9 @@ public: static NotTEC preflight(PreflightContext const& ctx); + static TER + preclaim(PreclaimContext const& ctx); + TER doApply() override; }; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 05c2a5d620..31b8fe3cc1 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -109,7 +109,8 @@ XRPNotCreated::visitEntry( ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops(); break; case ltESCROW: - drops_ -= (*before)[sfAmount].xrp().drops(); + if (isXRP((*before)[sfAmount])) + drops_ -= (*before)[sfAmount].xrp().drops(); break; default: break; @@ -130,7 +131,7 @@ XRPNotCreated::visitEntry( .drops(); break; case ltESCROW: - if (!isDelete) + if (!isDelete && isXRP((*after)[sfAmount])) drops_ += (*after)[sfAmount].xrp().drops(); break; default: @@ -270,14 +271,35 @@ NoZeroEscrow::visitEntry( std::shared_ptr const& after) { auto isBad = [](STAmount const& amount) { - if (!amount.native()) - return true; + // IOU case + if (amount.holds()) + { + if (amount <= beast::zero) + return true; - if (amount.xrp() <= XRPAmount{0}) - return true; + if (badCurrency() == amount.getCurrency()) + return true; + } - if (amount.xrp() >= INITIAL_XRP) - return true; + // MPT case + if (amount.holds()) + { + if (amount <= beast::zero) + return true; + + if (amount.mpt() > MPTAmount{maxMPTokenAmount}) + return true; + } + + // XRP case + if (amount.native()) + { + if (amount.xrp() <= XRPAmount{0}) + return true; + + if (amount.xrp() >= INITIAL_XRP) + return true; + } return false; }; @@ -287,14 +309,40 @@ NoZeroEscrow::visitEntry( if (after && after->getType() == ltESCROW) bad_ |= isBad((*after)[sfAmount]); + + auto checkAmount = [this](std::int64_t amount) { + if (amount > maxMPTokenAmount || amount < 0) + bad_ = true; + }; + + if (after && after->getType() == ltMPTOKEN_ISSUANCE) + { + auto const outstanding = (*after)[sfOutstandingAmount]; + checkAmount(outstanding); + if (auto const locked = (*after)[~sfLockedAmount]) + { + checkAmount(*locked); + bad_ = outstanding < *locked; + } + } + + if (after && after->getType() == ltMPTOKEN) + { + auto const mptAmount = (*after)[sfMPTAmount]; + checkAmount(mptAmount); + if (auto const locked = (*after)[~sfLockedAmount]) + { + checkAmount(*locked); + } + } } bool NoZeroEscrow::finalize( - STTx const&, + STTx const& txn, TER const, XRPAmount const, - ReadView const&, + ReadView const& rv, beast::Journal const& j) { if (bad_) @@ -1458,6 +1506,9 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensCreated_ == 0 && mptokensDeleted_ == 0; } + + if (tx.getTxnType() == ttESCROW_FINISH) + return true; } if (mptIssuancesCreated_ != 0) diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index 748c05869f..77b21b65f3 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -83,6 +83,15 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) return tecHAS_OBLIGATIONS; } + if ((*sleMpt)[~sfLockedAmount].value_or(0) != 0) + { + auto const sleMptIssuance = ctx.view.read( + keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + if (!sleMptIssuance) + return tefINTERNAL; // LCOV_EXCL_LINE + + return tecHAS_OBLIGATIONS; + } if (ctx.view.rules().enabled(featureSingleAssetVault) && sleMpt->isFlag(lsfMPTLocked)) return tecNO_PERMISSION; @@ -140,6 +149,32 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +TER +MPTokenAuthorize::createMPToken( + ApplyView& view, + MPTID const& mptIssuanceID, + AccountID const& account, + std::uint32_t const flags) +{ + auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); + + auto const ownerNode = view.dirInsert( + keylet::ownerDir(account), mptokenKey, describeOwnerDir(account)); + + if (!ownerNode) + return tecDIR_FULL; // LCOV_EXCL_LINE + + auto mptoken = std::make_shared(mptokenKey); + (*mptoken)[sfAccount] = account; + (*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID; + (*mptoken)[sfFlags] = flags; + (*mptoken)[sfOwnerNode] = *ownerNode; + + view.insert(mptoken); + + return tesSUCCESS; +} + TER MPTokenAuthorize::authorize( ApplyView& view, diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index e2b135a22a..a81dc7dea2 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.h +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -54,6 +54,13 @@ public: beast::Journal journal, MPTAuthorizeArgs const& args); + static TER + createMPToken( + ApplyView& view, + MPTID const& mptIssuanceID, + AccountID const& account, + std::uint32_t const flags); + TER doApply() override; }; diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp index d06ea3473e..a2e1f33b94 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp @@ -58,6 +58,9 @@ MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx) if ((*sleMPT)[sfOutstandingAmount] != 0) return tecHAS_OBLIGATIONS; + if ((*sleMPT)[~sfLockedAmount].value_or(0) != 0) + return tecHAS_OBLIGATIONS; + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index 0c16182ed8..6e19c4ae86 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -650,6 +650,15 @@ SetAccount::doApply() uFlagsOut &= ~lsfDisallowIncomingTrustline; } + // Set or clear flags for disallowing escrow + if (ctx_.view().rules().enabled(featureTokenEscrow)) + { + if (uSetFlag == asfAllowTrustLineLocking) + uFlagsOut |= lsfAllowTrustLineLocking; + else if (uClearFlag == asfAllowTrustLineLocking) + uFlagsOut &= ~lsfAllowTrustLineLocking; + } + // Set flag for clawback if (ctx_.view().rules().enabled(featureClawback) && uSetFlag == asfAllowTrustLineClawback) diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 387aedecfc..8c391499b6 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -357,6 +357,13 @@ transferRate(ReadView const& view, AccountID const& issuer); [[nodiscard]] Rate transferRate(ReadView const& view, MPTID const& issuanceID); +/** Returns the transfer fee as Rate based on the type of token + * @param view The ledger view + * @param amount The amount to transfer + */ +[[nodiscard]] Rate +transferRate(ReadView const& view, STAmount const& amount); + /** Returns `true` if the directory is empty @param key The key of the directory */ @@ -667,6 +674,21 @@ rippleCredit( bool bCheckIssuer, beast::Journal j); +TER +rippleLockEscrowMPT( + ApplyView& view, + AccountID const& uGrantorID, + STAmount const& saAmount, + beast::Journal j); + +TER +rippleUnlockEscrowMPT( + ApplyView& view, + AccountID const& uGrantorID, + AccountID const& uGranteeID, + STAmount const& saAmount, + beast::Journal j); + /** Calls static accountSendIOU if saAmount represents Issue. * Calls static accountSendMPT if saAmount represents MPTIssue. */ diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 3978d26e56..d3161dccae 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -781,6 +781,19 @@ transferRate(ReadView const& view, MPTID const& issuanceID) return parityRate; } +Rate +transferRate(ReadView const& view, STAmount const& amount) +{ + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + return transferRate(view, issue.getIssuer()); + else + return transferRate(view, issue.getMptID()); + }, + amount.asset().value()); +} + bool areCompatible( ReadView const& validLedger, @@ -2723,6 +2736,249 @@ sharesToAssetsWithdraw( return assets; } +TER +rippleLockEscrowMPT( + ApplyView& view, + AccountID const& sender, + STAmount const& amount, + beast::Journal j) +{ + auto const mptIssue = amount.get(); + auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); + auto sleIssuance = view.peek(mptID); + if (!sleIssuance) + { + JLOG(j.error()) << "rippleLockEscrowMPT: MPT issuance not found for " + << mptIssue.getMptID(); + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + } + + if (amount.getIssuer() == sender) + { + JLOG(j.error()) + << "rippleLockEscrowMPT: sender is the issuer, cannot lock MPTs."; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + // 1. Decrease the MPT Holder MPTAmount + // 2. Increase the MPT Holder EscrowedAmount + { + auto const mptokenID = keylet::mptoken(mptID.key, sender); + auto sle = view.peek(mptokenID); + if (!sle) + { + JLOG(j.error()) + << "rippleLockEscrowMPT: MPToken not found for " << sender; + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + } + + auto const amt = sle->getFieldU64(sfMPTAmount); + auto const pay = amount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract(STAmount(mptIssue, amt), STAmount(mptIssue, pay))) + { + JLOG(j.error()) + << "rippleLockEscrowMPT: insufficient MPTAmount for " + << to_string(sender) << ": " << amt << " < " << pay; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + (*sle)[sfMPTAmount] = amt - pay; + + // Overflow check for addition + uint64_t const locked = (*sle)[~sfLockedAmount].value_or(0); + + if (!canAdd(STAmount(mptIssue, locked), STAmount(mptIssue, pay))) + { + JLOG(j.error()) + << "rippleLockEscrowMPT: overflow on locked amount for " + << to_string(sender) << ": " << locked << " + " << pay; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + if (sle->isFieldPresent(sfLockedAmount)) + (*sle)[sfLockedAmount] += pay; + else + sle->setFieldU64(sfLockedAmount, pay); + + view.update(sle); + } + + // 1. Increase the Issuance EscrowedAmount + // 2. DO NOT change the Issuance OutstandingAmount + { + uint64_t const issuanceEscrowed = + (*sleIssuance)[~sfLockedAmount].value_or(0); + auto const pay = amount.mpt().value(); + + // Overflow check for addition + if (!canAdd( + STAmount(mptIssue, issuanceEscrowed), STAmount(mptIssue, pay))) + { + JLOG(j.error()) << "rippleLockEscrowMPT: overflow on issuance " + "locked amount for " + << mptIssue.getMptID() << ": " << issuanceEscrowed + << " + " << pay; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + if (sleIssuance->isFieldPresent(sfLockedAmount)) + (*sleIssuance)[sfLockedAmount] += pay; + else + sleIssuance->setFieldU64(sfLockedAmount, pay); + + view.update(sleIssuance); + } + return tesSUCCESS; +} + +TER +rippleUnlockEscrowMPT( + ApplyView& view, + AccountID const& sender, + AccountID const& receiver, + STAmount const& amount, + beast::Journal j) +{ + auto const issuer = amount.getIssuer(); + auto const mptIssue = amount.get(); + auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); + auto sleIssuance = view.peek(mptID); + if (!sleIssuance) + { + JLOG(j.error()) << "rippleUnlockEscrowMPT: MPT issuance not found for " + << mptIssue.getMptID(); + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + } + + // Decrease the Issuance EscrowedAmount + { + if (!sleIssuance->isFieldPresent(sfLockedAmount)) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: no locked amount in issuance for " + << mptIssue.getMptID(); + return tecINTERNAL; // LCOV_EXCL_LINE + } + + auto const locked = sleIssuance->getFieldU64(sfLockedAmount); + auto const redeem = amount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract( + STAmount(mptIssue, locked), STAmount(mptIssue, redeem))) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: insufficient locked amount for " + << mptIssue.getMptID() << ": " << locked << " < " << redeem; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + auto const newLocked = locked - redeem; + if (newLocked == 0) + sleIssuance->makeFieldAbsent(sfLockedAmount); + else + sleIssuance->setFieldU64(sfLockedAmount, newLocked); + view.update(sleIssuance); + } + + if (issuer != receiver) + { + // Increase the MPT Holder MPTAmount + auto const mptokenID = keylet::mptoken(mptID.key, receiver); + auto sle = view.peek(mptokenID); + if (!sle) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: MPToken not found for " << receiver; + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + } + + auto current = sle->getFieldU64(sfMPTAmount); + auto delta = amount.mpt().value(); + + // Overflow check for addition + if (!canAdd(STAmount(mptIssue, current), STAmount(mptIssue, delta))) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: overflow on MPTAmount for " + << to_string(receiver) << ": " << current << " + " << delta; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + (*sle)[sfMPTAmount] += delta; + view.update(sle); + } + else + { + // Decrease the Issuance OutstandingAmount + auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount); + auto const redeem = amount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract( + STAmount(mptIssue, outstanding), STAmount(mptIssue, redeem))) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: insufficient outstanding amount for " + << mptIssue.getMptID() << ": " << outstanding << " < " + << redeem; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem); + view.update(sleIssuance); + } + + if (issuer == sender) + { + JLOG(j.error()) << "rippleUnlockEscrowMPT: sender is the issuer, " + "cannot unlock MPTs."; + return tecINTERNAL; // LCOV_EXCL_LINE + } + else + { + // Decrease the MPT Holder EscrowedAmount + auto const mptokenID = keylet::mptoken(mptID.key, sender); + auto sle = view.peek(mptokenID); + if (!sle) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: MPToken not found for " << sender; + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + } + + if (!sle->isFieldPresent(sfLockedAmount)) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: no locked amount in MPToken for " + << to_string(sender); + return tecINTERNAL; // LCOV_EXCL_LINE + } + + auto const locked = sle->getFieldU64(sfLockedAmount); + auto const delta = amount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract(STAmount(mptIssue, locked), STAmount(mptIssue, delta))) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: insufficient locked amount for " + << to_string(sender) << ": " << locked << " < " << delta; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + auto const newLocked = locked - delta; + if (newLocked == 0) + sle->makeFieldAbsent(sfLockedAmount); + else + sle->setFieldU64(sfLockedAmount, newLocked); + view.update(sle); + } + return tesSUCCESS; +} + bool after(NetClock::time_point now, std::uint32_t mark) { diff --git a/src/xrpld/rpc/handlers/GatewayBalances.cpp b/src/xrpld/rpc/handlers/GatewayBalances.cpp index e8b95bd75c..ca9e370c81 100644 --- a/src/xrpld/rpc/handlers/GatewayBalances.cpp +++ b/src/xrpld/rpc/handlers/GatewayBalances.cpp @@ -142,11 +142,41 @@ doGatewayBalances(RPC::JsonContext& context) std::map> hotBalances; std::map> assets; std::map> frozenBalances; + std::map locked; // Traverse the cold wallet's trust lines { forEachItem( *ledger, accountID, [&](std::shared_ptr const& sle) { + if (sle->getType() == ltESCROW) + { + auto const& escrow = sle->getFieldAmount(sfAmount); + auto& bal = locked[escrow.getCurrency()]; + if (bal == beast::zero) + { + // This is needed to set the currency code correctly + bal = escrow; + } + else + { + try + { + bal += escrow; + } + catch (std::runtime_error const&) + { + // Presumably the exception was caused by overflow. + // On overflow return the largest valid STAmount. + // Very large sums of STAmount are approximations + // anyway. + bal = STAmount( + bal.issue(), + STAmount::cMaxValue, + STAmount::cMaxOffset); + } + } + } + auto rs = PathFindTrustLine::makeItem(accountID, sle); if (!rs) @@ -246,6 +276,17 @@ doGatewayBalances(RPC::JsonContext& context) populateResult(frozenBalances, jss::frozen_balances); populateResult(assets, jss::assets); + // Add total escrow to the result + if (!locked.empty()) + { + Json::Value j; + for (auto const& [k, v] : locked) + { + j[to_string(k)] = v.getText(); + } + result[jss::locked] = std::move(j); + } + return result; }