diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index e979d82a33..53ea1e70fb 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -4229,6 +4229,10 @@ True True + + True + True + True True diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index 7a877baabd..fdd736f6a8 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -5010,6 +5010,9 @@ test\app + + test\app + test\app diff --git a/src/ripple/app/main/Amendments.cpp b/src/ripple/app/main/Amendments.cpp index e48133f3ba..d470eaf61d 100644 --- a/src/ripple/app/main/Amendments.cpp +++ b/src/ripple/app/main/Amendments.cpp @@ -51,7 +51,8 @@ supportedAmendments () { "532651B4FD58DF8922A49BA101AB3E996E5BFBF95A913B3E392504863E63B164 TickSize" }, { "E2E6F2866106419B88C50045ACE96368558C345566AC8F2BDF5A5B5587F0E6FA fix1368" }, { "07D43DCE529B15A10827E5E04943B496762F9A88E3268269D69C44BE49E21104 Escrow" }, - { "86E83A7D2ECE3AD5FA87AB2195AE015C950469ABF0B72EAACED318F74886AE90 CryptoConditionsSuite" } + { "86E83A7D2ECE3AD5FA87AB2195AE015C950469ABF0B72EAACED318F74886AE90 CryptoConditionsSuite" }, + { "48C4451D6C6A138453F056EB6793AFF4B5C57457A37BA63EF3541FF8CE873DC2 ToStrandV2"} }; } diff --git a/src/ripple/app/paths/impl/BookStep.cpp b/src/ripple/app/paths/impl/BookStep.cpp index 634496b84d..ea71497945 100644 --- a/src/ripple/app/paths/impl/BookStep.cpp +++ b/src/ripple/app/paths/impl/BookStep.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -684,6 +685,13 @@ BookStep::check(StrandContext const& ctx) const return temBAD_PATH_LOOP; } + if (ctx.view.rules().enabled(featureToStrandV2) && + ctx.seenDirectIssues[1].count(book_.out)) + { + JLOG(j_.debug()) << "BookStep: loop detected: " << *this; + return temBAD_PATH_LOOP; + } + if (amendmentRIPD1443(ctx.view.info().parentCloseTime)) { if (ctx.prevStep) diff --git a/src/ripple/app/paths/impl/DirectStep.cpp b/src/ripple/app/paths/impl/DirectStep.cpp index 9144050094..b96fa06e36 100644 --- a/src/ripple/app/paths/impl/DirectStep.cpp +++ b/src/ripple/app/paths/impl/DirectStep.cpp @@ -124,6 +124,12 @@ class DirectStepI : public StepImp return src_; } + boost::optional> + directStepAccts () const override + { + return std::make_pair(src_, dst_); + } + bool redeems (ReadView const& sb, bool fwd) const override; diff --git a/src/ripple/app/paths/impl/PaySteps.cpp b/src/ripple/app/paths/impl/PaySteps.cpp index 28c2a59ab1..2d50568c06 100644 --- a/src/ripple/app/paths/impl/PaySteps.cpp +++ b/src/ripple/app/paths/impl/PaySteps.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include #include @@ -102,6 +104,8 @@ toStep ( JLOG (j.warn()) << "Found offer/account payment step. Aborting payment strand."; assert (0); + if (ctx.view.rules().enabled(featureToStrandV2)) + return {temBAD_PATH, std::unique_ptr{}}; Throw (tefEXCEPTION, "Found offer/account payment step."); } @@ -132,7 +136,7 @@ toStep ( } std::pair -toStrand ( +toStrandV1 ( ReadView const& view, AccountID const& src, AccountID const& dst, @@ -370,6 +374,308 @@ toStrand ( return {tesSUCCESS, std::move (result)}; } + +std::pair +toStrandV2 ( + ReadView const& view, + AccountID const& src, + AccountID const& dst, + Issue const& deliver, + boost::optional const& sendMaxIssue, + STPath const& path, + bool ownerPaysTransferFee, + beast::Journal j) +{ + if (isXRP(src) || isXRP(dst) || + !isConsistent(deliver) || (sendMaxIssue && !isConsistent(*sendMaxIssue))) + return {temBAD_PATH, Strand{}}; + + for (auto const& pe : path) + { + auto const t = pe.getNodeType(); + + if ((t & ~STPathElement::typeAll) || !t) + return {temBAD_PATH, Strand{}}; + + bool const hasAccount = t & STPathElement::typeAccount; + bool const hasIssuer = t & STPathElement::typeIssuer; + bool const hasCurrency = t & STPathElement::typeCurrency; + + if (hasAccount && (hasIssuer || hasCurrency)) + return {temBAD_PATH, Strand{}}; + + if (hasIssuer && isXRP(pe.getIssuerID())) + return {temBAD_PATH, Strand{}}; + + if (hasAccount && isXRP(pe.getAccountID())) + return {temBAD_PATH, Strand{}}; + + if (hasCurrency && hasIssuer && + isXRP(pe.getCurrency()) != isXRP(pe.getIssuerID())) + return {temBAD_PATH, Strand{}}; + } + + Issue curIssue = [&] + { + auto const& currency = + sendMaxIssue ? sendMaxIssue->currency : deliver.currency; + if (isXRP (currency)) + return xrpIssue (); + return Issue{currency, src}; + }(); + + auto hasCurrency = [](STPathElement const pe) + { + return pe.getNodeType () & STPathElement::typeCurrency; + }; + + std::vector normPath; + // reserve enough for the path, the implied source, destination, + // sendmax and deliver. + normPath.reserve(4 + path.size()); + { + normPath.emplace_back( + STPathElement::typeAll, src, curIssue.currency, curIssue.account); + + if (sendMaxIssue && sendMaxIssue->account != src && + (path.empty() || !path[0].isAccount() || + path[0].getAccountID() != sendMaxIssue->account)) + { + normPath.emplace_back(sendMaxIssue->account, boost::none, boost::none); + } + + for (auto const& i : path) + normPath.push_back(i); + + auto const lastCurrency = + (*boost::find_if(boost::adaptors::reverse(normPath), hasCurrency)) + .getCurrency(); + if (lastCurrency != deliver.currency) + normPath.emplace_back( + boost::none, deliver.currency, deliver.account); + + if (!((normPath.back().isAccount() && + normPath.back().getAccountID() == deliver.account) || + (dst == deliver.account))) + { + normPath.emplace_back(deliver.account, boost::none, boost::none); + } + + if (!normPath.back().isAccount() || + normPath.back().getAccountID() != dst) + { + normPath.emplace_back(dst, boost::none, boost::none); + } + } + + auto const strandSrc = normPath.front().getAccountID (); + auto const strandDst = normPath.back().getAccountID (); + + Strand result; + result.reserve (2 * normPath.size ()); + + /* A strand may not include the same account node more than once + in the same currency. In a direct step, an account will show up + at most twice: once as a src and once as a dst (hence the two element array). + The strandSrc and strandDst will only show up once each. + */ + std::array, 2> seenDirectIssues; + // A strand may not include the same offer book more than once + boost::container::flat_set seenBookOuts; + seenDirectIssues[0].reserve (normPath.size()); + seenDirectIssues[1].reserve (normPath.size()); + seenBookOuts.reserve (normPath.size()); + auto ctx = [&](bool isLast = false) + { + return StrandContext{view, result, strandSrc, strandDst, isLast, + ownerPaysTransferFee, seenDirectIssues, seenBookOuts, j}; + }; + + for (std::size_t i = 0; i < normPath.size () - 1; ++i) + { + /* Iterate through the path elements considering them in pairs. + The first element of the pair is `cur` and the second element is + `next`. When an offer is one of the pairs, the step created will be for + `next`. This means when `cur` is an offer and `next` is an + account then no step is created, as a step has already been created for + that offer. + */ + boost::optional impliedPE; + auto cur = &normPath[i]; + auto const next = &normPath[i + 1]; + + if (cur->isAccount()) + curIssue.account = cur->getAccountID (); + else if (cur->hasIssuer()) + curIssue.account = cur->getIssuerID (); + + if (cur->hasCurrency()) + { + curIssue.currency = cur->getCurrency (); + if (isXRP(curIssue.currency)) + curIssue.account = xrpAccount(); + } + + if (cur->isAccount() && next->isAccount()) + { + if (!isXRP (curIssue.currency) && + curIssue.account != cur->getAccountID () && + curIssue.account != next->getAccountID ()) + { + JLOG (j.trace()) << "Inserting implied account"; + auto msr = make_DirectStepI (ctx(), cur->getAccountID (), + curIssue.account, curIssue.currency); + if (msr.first != tesSUCCESS) + return {msr.first, Strand{}}; + result.push_back (std::move (msr.second)); + impliedPE.emplace(STPathElement::typeAccount, + curIssue.account, xrpCurrency(), xrpAccount()); + cur = &*impliedPE; + } + } + else if (cur->isAccount() && next->isOffer()) + { + if (curIssue.account != cur->getAccountID ()) + { + JLOG (j.trace()) << "Inserting implied account before offer"; + auto msr = make_DirectStepI (ctx(), cur->getAccountID (), + curIssue.account, curIssue.currency); + if (msr.first != tesSUCCESS) + return {msr.first, Strand{}}; + result.push_back (std::move (msr.second)); + impliedPE.emplace(STPathElement::typeAccount, + curIssue.account, xrpCurrency(), xrpAccount()); + cur = &*impliedPE; + } + } + else if (cur->isOffer() && next->isAccount()) + { + if (curIssue.account != next->getAccountID () && + !isXRP (next->getAccountID ())) + { + if (isXRP(curIssue)) + { + if (i != normPath.size() - 2) + return {temBAD_PATH, Strand{}}; + else + { + // Last step. insert xrp endpoint step + auto msr = make_XRPEndpointStep (ctx(), next->getAccountID()); + if (msr.first != tesSUCCESS) + return {msr.first, Strand{}}; + result.push_back(std::move(msr.second)); + } + } + else + { + JLOG(j.trace()) << "Inserting implied account after offer"; + auto msr = make_DirectStepI(ctx(), + curIssue.account, next->getAccountID(), curIssue.currency); + if (msr.first != tesSUCCESS) + return {msr.first, Strand{}}; + result.push_back(std::move(msr.second)); + } + } + continue; + } + + if (!next->isOffer() && + next->hasCurrency() && next->getCurrency () != curIssue.currency) + { + // Should never happen + assert(0); + return {temBAD_PATH, Strand{}}; + } + + auto s = + toStep (ctx (/*isLast*/ i == normPath.size () - 2), cur, next, curIssue); + if (s.first == tesSUCCESS) + result.emplace_back (std::move (s.second)); + else + { + JLOG (j.debug()) << "toStep failed: " << s.first; + return {s.first, Strand{}}; + } + } + + auto checkStrand = [&]() -> bool { + auto stepAccts = [](Step const& s) -> std::pair { + if (auto r = s.directStepAccts()) + return *r; + if (auto const r = s.bookStepBook()) + return std::make_pair(r->in.account, r->out.account); + Throw( + tefEXCEPTION, "Step should be either a direct or book step"); + return std::make_pair(xrpAccount(), xrpAccount()); + }; + + auto curAccount = src; + auto curIssue = [&] { + auto& currency = + sendMaxIssue ? sendMaxIssue->currency : deliver.currency; + if (isXRP(currency)) + return xrpIssue(); + return Issue{currency, src}; + }(); + + for (auto const& s : result) + { + auto const accts = stepAccts(*s); + if (accts.first != curAccount) + return false; + + if (auto const b = s->bookStepBook()) + { + if (curIssue != b->in) + return false; + curIssue = b->out; + } + else + { + curIssue.account = accts.second; + } + + curAccount = accts.second; + } + if (curAccount != dst) + return false; + if (curIssue.currency != deliver.currency) + return false; + if (curIssue.account != deliver.account && + curIssue.account != dst) + return false; + return true; + }; + + if (!checkStrand()) + { + JLOG (j.warn()) << "Flow check strand failed"; + assert(0); + return {temBAD_PATH, Strand{}}; + } + + return {tesSUCCESS, std::move (result)}; +} + +std::pair +toStrand ( + ReadView const& view, + AccountID const& src, + AccountID const& dst, + Issue const& deliver, + boost::optional const& sendMaxIssue, + STPath const& path, + bool ownerPaysTransferFee, + beast::Journal j) +{ + if (view.rules().enabled(featureToStrandV2)) + return toStrandV2( + view, src, dst, deliver, sendMaxIssue, path, ownerPaysTransferFee, j); + else + return toStrandV1( + view, src, dst, deliver, sendMaxIssue, path, ownerPaysTransferFee, j); +} + std::pair> toStrands ( ReadView const& view, diff --git a/src/ripple/app/paths/impl/Steps.h b/src/ripple/app/paths/impl/Steps.h index 6a212d5d87..e37b093d2c 100644 --- a/src/ripple/app/paths/impl/Steps.h +++ b/src/ripple/app/paths/impl/Steps.h @@ -121,6 +121,14 @@ public: return boost::none; } + // for debugging. Return the src and dst accounts for a direct step + // For XRP endpoints, one of src or dst will be the root account + virtual boost::optional> + directStepAccts () const + { + return boost::none; + } + /** If this step is a DirectStepI and the src redeems to the dst, return true, otherwise return false. @@ -223,6 +231,26 @@ bool operator==(Strand const& lhs, Strand const& rhs) return true; } +/* + Normalize a path by inserting implied accounts and offers + + @param src Account that is sending assets + @param dst Account that is receiving assets + @param deliver Asset the dst account will receive + (if issuer of deliver == dst, then accept any issuer) + @param sendMax Optional asset to send. + @param path Liquidity sources to use for this strand of the payment. The path + contains an ordered collection of the offer books to use and + accounts to ripple through. + @return error code and normalized path +*/ +std::pair +normalizePath(AccountID const& src, + AccountID const& dst, + Issue const& deliver, + boost::optional const& sendMaxIssue, + STPath const& path); + /* Create a strand for the specified path @@ -235,7 +263,6 @@ bool operator==(Strand const& lhs, Strand const& rhs) @param path Liquidity sources to use for this strand of the payment. The path contains an ordered collection of the offer books to use and accounts to ripple through. - @param addDefaultPath Determines if the default path should be considered @param l logs to write journal messages to @return error code and collection of strands */ diff --git a/src/ripple/app/paths/impl/XRPEndpointStep.cpp b/src/ripple/app/paths/impl/XRPEndpointStep.cpp index f6c11a7df1..3fd49dc833 100644 --- a/src/ripple/app/paths/impl/XRPEndpointStep.cpp +++ b/src/ripple/app/paths/impl/XRPEndpointStep.cpp @@ -68,6 +68,14 @@ class XRPEndpointStep : public StepImp return acc_; }; + boost::optional> + directStepAccts () const override + { + if (isLast_) + return std::make_pair(xrpAccount(), acc_); + return std::make_pair(acc_, xrpAccount()); + } + boost::optional cachedIn () const override { diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 3d8d9312c9..352040295e 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -48,6 +48,7 @@ extern uint256 const featureTickSize; extern uint256 const fix1368; extern uint256 const featureEscrow; extern uint256 const featureCryptoConditionsSuite; +extern uint256 const featureToStrandV2; } // ripple diff --git a/src/ripple/protocol/STPathSet.h b/src/ripple/protocol/STPathSet.h index c67714acef..b84a74c1b5 100644 --- a/src/ripple/protocol/STPathSet.h +++ b/src/ripple/protocol/STPathSet.h @@ -117,6 +117,8 @@ public: hash_value_ = get_hash (*this); } + STPathElement(STPathElement const&) = default; + int getNodeType () const { @@ -203,8 +205,8 @@ class STPath public: STPath () = default; - STPath (std::vector const& p) - : mPath (p) + STPath (std::vector p) + : mPath (std::move(p)) { } std::vector::size_type @@ -279,6 +281,10 @@ public: return mPath[i]; } + void reserve(size_t s) + { + mPath.reserve(s); + } private: std::vector mPath; }; diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index ef8a84c9b1..e485daf3fb 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -59,5 +59,6 @@ uint256 const featureTickSize = feature("TickSize"); uint256 const fix1368 = feature("fix1368"); uint256 const featureEscrow = feature("Escrow"); uint256 const featureCryptoConditionsSuite = feature("CryptoConditionsSuite"); +uint256 const featureToStrandV2 = feature("ToStrandV2"); } // ripple diff --git a/src/test/app/CrossingLimits_test.cpp b/src/test/app/CrossingLimits_test.cpp index 547882d749..e3d24a5df2 100644 --- a/src/test/app/CrossingLimits_test.cpp +++ b/src/test/app/CrossingLimits_test.cpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace ripple { namespace test { @@ -41,11 +42,12 @@ private: } public: + void - testStepLimit() + testStepLimit(std::initializer_list fs) { using namespace jtx; - Env env(*this); + Env env(*this, features(fs)); auto const xrpMax = XRP(100000000000); auto const gw = Account("gateway"); auto const USD = gw["USD"]; @@ -74,12 +76,12 @@ public: balance("bob", USD(0)), owners("bob", 1), balance("dan", USD(1)), owners("dan", 2))); } - + void - testCrossingLimit() + testCrossingLimit(std::initializer_list fs) { using namespace jtx; - Env env(*this); + Env env(*this, features(fs)); auto const xrpMax = XRP(100000000000); auto const gw = Account("gateway"); auto const USD = gw["USD"]; @@ -105,10 +107,10 @@ public: } void - testStepAndCrossingLimit() + testStepAndCrossingLimit(std::initializer_list fs) { using namespace jtx; - Env env(*this); + Env env(*this, features(fs)); auto const xrpMax = XRP(100000000000); auto const gw = Account("gateway"); auto const USD = gw["USD"]; @@ -150,9 +152,14 @@ public: void run() { - testStepLimit(); - testCrossingLimit(); - testStepAndCrossingLimit(); + auto testAll = [this](std::initializer_list fs) { + testStepLimit(fs); + testCrossingLimit(fs); + testStepAndCrossingLimit(fs); + }; + testAll({}); + testAll({featureFlow}); + testAll({featureFlow, featureToStrandV2}); } }; diff --git a/src/test/app/DeliverMin_test.cpp b/src/test/app/DeliverMin_test.cpp index c9173c5512..f961e94e9c 100644 --- a/src/test/app/DeliverMin_test.cpp +++ b/src/test/app/DeliverMin_test.cpp @@ -20,6 +20,7 @@ #include #include #include +#include namespace ripple { namespace test { @@ -28,7 +29,7 @@ class DeliverMin_test : public beast::unit_test::suite { public: void - test_convert_all_of_an_asset() + test_convert_all_of_an_asset(std::initializer_list fs) { testcase("Convert all of an asset using DeliverMin"); @@ -37,7 +38,7 @@ public: auto const USD = gw["USD"]; { - Env env(*this); + Env env(*this, features(fs)); env.fund(XRP(10000), "alice", "bob", "carol", gw); env.trust(USD(100), "alice", "bob", "carol"); env(pay("alice", "bob", USD(10)), delivermin(USD(10)), ter(temBAD_AMOUNT)); @@ -60,7 +61,7 @@ public: } { - Env env(*this); + Env env(*this, features(fs)); env.fund(XRP(10000), "alice", "bob", gw); env.trust(USD(1000), "alice", "bob"); env(pay(gw, "bob", USD(100))); @@ -72,7 +73,7 @@ public: } { - Env env(*this); + Env env(*this, features(fs)); env.fund(XRP(10000), "alice", "bob", "carol", gw); env.trust(USD(1000), "bob", "carol"); env(pay(gw, "bob", USD(200))); @@ -90,7 +91,7 @@ public: } { - Env env(*this); + Env env(*this, features(fs)); env.fund(XRP(10000), "alice", "bob", "carol", "dan", gw); env.trust(USD(1000), "bob", "carol", "dan"); env(pay(gw, "bob", USD(100))); @@ -110,7 +111,9 @@ public: void run() { - test_convert_all_of_an_asset(); + test_convert_all_of_an_asset({}); + test_convert_all_of_an_asset({featureFlow}); + test_convert_all_of_an_asset({featureFlow, featureToStrandV2}); } }; diff --git a/src/test/app/Discrepancy_test.cpp b/src/test/app/Discrepancy_test.cpp index 0a8e5573e8..52ffb98a2a 100644 --- a/src/test/app/Discrepancy_test.cpp +++ b/src/test/app/Discrepancy_test.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include namespace ripple { @@ -36,11 +37,11 @@ class Discrepancy_test : public beast::unit_test::suite // A payment with path and sendmax is made and the transaction is queried // to verify that the net of balance changes match the fee charged. void - testXRPDiscrepancy () + testXRPDiscrepancy (std::initializer_list fs) { testcase ("Discrepancy test : XRP Discrepancy"); using namespace test::jtx; - Env env {*this}; + Env env {*this, features(fs)}; Account A1 {"A1"}; Account A2 {"A2"}; @@ -143,7 +144,9 @@ class Discrepancy_test : public beast::unit_test::suite public: void run () { - testXRPDiscrepancy (); + testXRPDiscrepancy ({}); + testXRPDiscrepancy ({featureFlow}); + testXRPDiscrepancy ({featureFlow, featureToStrandV2}); } }; diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index de2121b106..1a29dba593 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -21,61 +21,16 @@ #include #include #include +#include #include #include #include #include #include + namespace ripple { namespace test { -struct Flow_test; - -struct DirectStepInfo -{ - AccountID src; - AccountID dst; - Currency currency; -}; - -struct XRPEndpointStepInfo -{ - AccountID acc; -}; - -enum class TrustFlag {freeze, auth}; - -/*constexpr*/ std::uint32_t trustFlag (TrustFlag f, bool useHigh) -{ - switch(f) - { - case TrustFlag::freeze: - if (useHigh) - return lsfHighFreeze; - return lsfLowFreeze; - case TrustFlag::auth: - if (useHigh) - return lsfHighAuth; - return lsfLowAuth; - } - return 0; // Silence warning about end of non-void function -} - -bool getTrustFlag (jtx::Env const& env, - jtx::Account const& src, - jtx::Account const& dst, - Currency const& cur, - TrustFlag flag) -{ - if (auto sle = env.le (keylet::line (src, dst, cur))) - { - auto const useHigh = src.id() > dst.id(); - return sle->isFlag (trustFlag (flag, useHigh)); - } - Throw ("No line in getTrustFlag"); - return false; // silence warning -} - jtx::PrettyAmount xrpMinusFee (jtx::Env const& env, std::int64_t xrpAmount) { @@ -85,311 +40,17 @@ xrpMinusFee (jtx::Env const& env, std::int64_t xrpAmount) dropsPerXRP::value * xrpAmount - feeDrops); }; -bool equal (std::unique_ptr const& s1, - DirectStepInfo const& dsi) -{ - if (!s1) - return false; - return test::directStepEqual (*s1, dsi.src, dsi.dst, dsi.currency); -} - -bool equal (std::unique_ptr const& s1, - XRPEndpointStepInfo const& xrpsi) -{ - if (!s1) - return false; - return test::xrpEndpointStepEqual (*s1, xrpsi.acc); -} - -bool equal (std::unique_ptr const& s1, ripple::Book const& bsi) -{ - if (!s1) - return false; - return bookStepEqual (*s1, bsi); -} - -template -bool strandEqualHelper (Iter i) -{ - // base case. all args processed and found equal. - return true; -} - -template -bool strandEqualHelper (Iter i, StepInfo&& si, Args&&... args) -{ - if (!equal (*i, std::forward (si))) - return false; - return strandEqualHelper (++i, std::forward (args)...); -} - -template -bool equal (Strand const& strand, Args&&... args) -{ - if (strand.size () != sizeof...(Args)) - return false; - if (strand.empty ()) - return true; - return strandEqualHelper (strand.begin (), std::forward (args)...); -} - struct Flow_test : public beast::unit_test::suite { - // Account path element - static auto APE(AccountID const& a) + static bool hasFeature(uint256 const& feat, std::initializer_list args) { - return STPathElement ( - STPathElement::typeAccount, a, xrpCurrency (), xrpAccount ()); - }; - - // Issue path element - static auto IPE(Issue const& iss) - { - return STPathElement ( - STPathElement::typeCurrency | STPathElement::typeIssuer, - xrpAccount (), iss.currency, iss.account); - }; - - // Issuer path element - static auto IAPE(AccountID const& account) - { - return STPathElement ( - STPathElement::typeIssuer, - xrpAccount (), xrpCurrency (), account); - }; - - // Currency path element - static auto CPE(Currency const& c) - { - return STPathElement ( - STPathElement::typeCurrency, xrpAccount (), c, xrpAccount ()); - }; - - void testToStrand () - { - testcase ("To Strand"); - - using namespace jtx; - auto const alice = Account ("alice"); - auto const bob = Account ("bob"); - auto const carol = Account ("carol"); - auto const gw = Account ("gw"); - - auto const USD = gw["USD"]; - auto const EUR = gw["EUR"]; - - auto const eurC = EUR.currency; - auto const usdC = USD.currency; - - using D = DirectStepInfo; - using B = ripple::Book; - using XRPS = XRPEndpointStepInfo; - - auto test = [&, this](jtx::Env& env, Issue const& deliver, - boost::optional const& sendMaxIssue, STPath const& path, - TER expTer, auto&&... expSteps) - { - auto r = toStrand (*env.current (), alice, bob, - deliver, sendMaxIssue, path, true, env.app ().logs ().journal ("Flow")); - BEAST_EXPECT(r.first == expTer); - if (sizeof...(expSteps)) - BEAST_EXPECT(equal ( - r.second, std::forward (expSteps)...)); - }; - - { - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); - env.fund (XRP (10000), alice, bob, carol, gw); - - test (env, USD, boost::none, STPath(), terNO_LINE); - - env.trust (USD (1000), alice, bob, carol); - test (env, USD, boost::none, STPath(), tecPATH_DRY); - - env (pay (gw, alice, USD (100))); - env (pay (gw, carol, USD (100))); - - // Insert implied account - test (env, USD, boost::none, STPath(), tesSUCCESS, - D{alice, gw, usdC}, D{gw, bob, usdC}); - env.trust (EUR (1000), alice, bob); - - // Insert implied offer - test (env, EUR, USD.issue (), STPath(), tesSUCCESS, - D{alice, gw, usdC}, B{USD, EUR}, D{gw, bob, eurC}); - - // Path with explicit offer - test (env, EUR, USD.issue (), STPath ({IPE (EUR)}), - tesSUCCESS, D{alice, gw, usdC}, B{USD, EUR}, D{gw, bob, eurC}); - - // Path with offer that changes issuer only - env.trust (carol["USD"] (1000), bob); - test (env, carol["USD"], USD.issue (), STPath ({IAPE (carol)}), - tesSUCCESS, - D{alice, gw, usdC}, B{USD, carol["USD"]}, D{carol, bob, usdC}); - - // Path with XRP src currency - test (env, USD, xrpIssue (), STPath ({IPE (USD)}), tesSUCCESS, - XRPS{alice}, B{XRP, USD}, D{gw, bob, usdC}); - - // Path with XRP dst currency - test (env, xrpIssue(), USD.issue (), STPath ({IPE (XRP)}), - tesSUCCESS, D{alice, gw, usdC}, B{USD, XRP}, XRPS{bob}); - - // Path with XRP cross currency bridged payment - test (env, EUR, USD.issue (), STPath ({CPE (xrpCurrency ())}), - tesSUCCESS, - D{alice, gw, usdC}, B{USD, XRP}, B{XRP, EUR}, D{gw, bob, eurC}); - - // XRP -> XRP transaction can't include a path - test (env, XRP, boost::none, STPath ({APE (carol)}), temBAD_PATH); - - { - // The root account can't be the src or dst - auto flowJournal = env.app ().logs ().journal ("Flow"); - { - // The root account can't be the dst - auto r = toStrand (*env.current (), alice, - xrpAccount (), XRP, USD.issue (), STPath (), true, flowJournal); - BEAST_EXPECT(r.first == temBAD_PATH); - } - { - // The root account can't be the src - auto r = - toStrand (*env.current (), xrpAccount (), - alice, XRP, boost::none, STPath (), true, flowJournal); - BEAST_EXPECT(r.first == temBAD_PATH); - } - { - // The root account can't be the src - auto r = toStrand (*env.current (), - noAccount (), bob, USD, boost::none, STPath (), true, flowJournal); - BEAST_EXPECT(r.first == terNO_ACCOUNT); - } - } - - // Create an offer with the same in/out issue - test (env, EUR, USD.issue (), STPath ({IPE (USD), IPE (EUR)}), - temBAD_PATH); - - // Path element with type zero - test (env, USD, boost::none, - STPath ({STPathElement ( - 0, xrpAccount (), xrpCurrency (), xrpAccount ())}), - temBAD_PATH); - - // The same account can't appear more than once on a path - // `gw` will be used from alice->carol and implied between carol - // and bob - test (env, USD, boost::none, STPath ({APE (gw), APE (carol)}), - temBAD_PATH_LOOP); - - // The same offer can't appear more than once on a path - test (env, EUR, USD.issue (), STPath ({IPE (EUR), IPE (USD), IPE (EUR)}), - temBAD_PATH_LOOP); - } - - { - // cannot have more than one offer with the same output issue - - using namespace jtx; - Env env (*this, features (featureFlow), features(featureOwnerPaysFee)); - - env.fund (XRP (10000), alice, bob, carol, gw); - env.trust (USD (10000), alice, bob, carol); - env.trust (EUR (10000), alice, bob, carol); - - env (pay (gw, bob, USD (100))); - env (pay (gw, bob, EUR (100))); - - env (offer (bob, XRP (100), USD (100))); - env (offer (bob, USD (100), EUR (100)), txflags (tfPassive)); - env (offer (bob, EUR (100), USD (100)), txflags (tfPassive)); - - // payment path: XRP -> XRP/USD -> USD/EUR -> EUR/USD - env (pay (alice, carol, USD (100)), path (~USD, ~EUR, ~USD), - sendmax (XRP (200)), txflags (tfNoRippleDirect), - ter (temBAD_PATH_LOOP)); - } - - { - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); - env.fund (XRP (10000), alice, bob, noripple (gw)); - env.trust (USD (1000), alice, bob); - env (pay (gw, alice, USD (100))); - test (env, USD, boost::none, STPath (), terNO_RIPPLE); - } - - { - // check global freeze - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); - env.fund (XRP (10000), alice, bob, gw); - env.trust (USD (1000), alice, bob); - env (pay (gw, alice, USD (100))); - - // Account can still issue payments - env(fset(alice, asfGlobalFreeze)); - test (env, USD, boost::none, STPath (), tesSUCCESS); - env(fclear(alice, asfGlobalFreeze)); - test (env, USD, boost::none, STPath (), tesSUCCESS); - - // Account can not issue funds - env(fset(gw, asfGlobalFreeze)); - test (env, USD, boost::none, STPath (), terNO_LINE); - env(fclear(gw, asfGlobalFreeze)); - test (env, USD, boost::none, STPath (), tesSUCCESS); - - // Account can not receive funds - env(fset(bob, asfGlobalFreeze)); - test (env, USD, boost::none, STPath (), terNO_LINE); - env(fclear(bob, asfGlobalFreeze)); - test (env, USD, boost::none, STPath (), tesSUCCESS); - } - { - // Freeze between gw and alice - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); - env.fund (XRP (10000), alice, bob, gw); - env.trust (USD (1000), alice, bob); - env (pay (gw, alice, USD (100))); - test (env, USD, boost::none, STPath (), tesSUCCESS); - env (trust (gw, alice["USD"] (0), tfSetFreeze)); - BEAST_EXPECT(getTrustFlag (env, gw, alice, usdC, TrustFlag::freeze)); - test (env, USD, boost::none, STPath (), terNO_LINE); - } - { - // check no auth - // An account may require authorization to receive IOUs from an - // issuer - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); - env.fund (XRP (10000), alice, bob, gw); - env (fset (gw, asfRequireAuth)); - env.trust (USD (1000), alice, bob); - // Authorize alice but not bob - env (trust (gw, alice ["USD"] (1000), tfSetfAuth)); - BEAST_EXPECT(getTrustFlag (env, gw, alice, usdC, TrustFlag::auth)); - env (pay (gw, alice, USD (100))); - env.require (balance (alice, USD (100))); - test (env, USD, boost::none, STPath (), terNO_AUTH); - - // Check pure issue redeem still works - auto r = toStrand (*env.current (), alice, gw, USD, - boost::none, STPath (), true, env.app ().logs ().journal ("Flow")); - BEAST_EXPECT(r.first == tesSUCCESS); - BEAST_EXPECT(equal (r.second, D{alice, gw, usdC})); - } - { - // Check path with sendMax and node with correct sendMax already set - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); - env.fund (XRP (10000), alice, bob, gw); - env.trust (USD (1000), alice, bob); - env.trust (EUR (1000), alice, bob); - env (pay (gw, alice, EUR (100))); - auto const path = STPath ({STPathElement (STPathElement::typeAll, - EUR.account, EUR.currency, EUR.account)}); - test (env, USD, EUR.issue(), path, tesSUCCESS); - } + for(auto const& f : args) + if (f == feat) + return true; + return false; } - void testDirectStep () + + void testDirectStep (std::initializer_list fs) { testcase ("Direct Step"); @@ -407,7 +68,7 @@ struct Flow_test : public beast::unit_test::suite auto const USD = gw["USD"]; { // Pay USD, trivial path - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, gw); env.trust (USD (1000), alice, bob); @@ -417,7 +78,7 @@ struct Flow_test : public beast::unit_test::suite } { // XRP transfer - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob); env (pay (alice, bob, XRP (100))); @@ -426,7 +87,7 @@ struct Flow_test : public beast::unit_test::suite } { // Partial payments - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, gw); env.trust (USD (1000), alice, bob); @@ -440,7 +101,7 @@ struct Flow_test : public beast::unit_test::suite } { // Pay by rippling through accounts, use path finder - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, dan); env.trust (USDA (10), bob); @@ -455,7 +116,7 @@ struct Flow_test : public beast::unit_test::suite { // Pay by rippling through accounts, specify path // and charge a transfer fee - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, dan); env.trust (USDA (10), bob); @@ -473,7 +134,7 @@ struct Flow_test : public beast::unit_test::suite { // Pay by rippling through accounts, specify path and transfer fee // Test that the transfer fee is not charged when alice issues - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, dan); env.trust (USDA (10), bob); @@ -489,7 +150,7 @@ struct Flow_test : public beast::unit_test::suite { // test best quality path is taken // Paths: A->B->D->E ; A->C->D->E - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, dan, erin); env.trust (USDA (10), bob, carol); @@ -510,7 +171,7 @@ struct Flow_test : public beast::unit_test::suite } { // Limit quality - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol); env.trust (USDA (10), bob); @@ -526,7 +187,7 @@ struct Flow_test : public beast::unit_test::suite } } - void testLineQuality () + void testLineQuality (std::initializer_list fs) { testcase ("Line Quality"); @@ -543,66 +204,63 @@ struct Flow_test : public beast::unit_test::suite // Dan -> Bob -> Alice -> Carol; vary bobDanQIn and bobAliceQOut for (auto bobDanQIn : {80, 100, 120}) for (auto bobAliceQOut : {80, 100, 120}) - for (auto const& f : {feature ("nullFeature"), featureFlow}) - { - if (f != featureFlow && bobDanQIn < 100 && bobAliceQOut < 100) - continue; // Bug in flow v1 - Env env (*this, features (f)); - env.fund (XRP (10000), alice, bob, carol, dan); - env (trust (bob, USDD (100)), qualityInPercent (bobDanQIn)); - env (trust (bob, USDA (100)), - qualityOutPercent (bobAliceQOut)); - env (trust (carol, USDA (100))); + { + if (!hasFeature(featureFlow, fs) && bobDanQIn < 100 && + bobAliceQOut < 100) + continue; // Bug in flow v1 + Env env(*this, features(fs)); + env.fund(XRP(10000), alice, bob, carol, dan); + env(trust(bob, USDD(100)), qualityInPercent(bobDanQIn)); + env(trust(bob, USDA(100)), qualityOutPercent(bobAliceQOut)); + env(trust(carol, USDA(100))); - env (pay (alice, bob, USDA (100))); - env.require (balance (bob, USDA (100))); - env (pay (dan, carol, USDA (10)), path (bob), - sendmax (USDD (100)), txflags (tfNoRippleDirect)); - env.require (balance (bob, USDA (90))); - if (bobAliceQOut > bobDanQIn) - env.require ( - balance (bob, USDD (10.0 * double(bobAliceQOut) / - double(bobDanQIn)))); - else - env.require (balance (bob, USDD (10))); - env.require (balance (carol, USDA (10))); - } + env(pay(alice, bob, USDA(100))); + env.require(balance(bob, USDA(100))); + env(pay(dan, carol, USDA(10)), + path(bob), sendmax(USDD(100)), txflags(tfNoRippleDirect)); + env.require(balance(bob, USDA(90))); + if (bobAliceQOut > bobDanQIn) + env.require(balance( + bob, + USDD(10.0 * double(bobAliceQOut) / double(bobDanQIn)))); + else + env.require(balance(bob, USDD(10))); + env.require(balance(carol, USDA(10))); + } // bob -> alice -> carol; vary carolAliceQIn for (auto carolAliceQIn : {80, 100, 120}) - for (auto const& f : {feature ("nullFeature"), featureFlow}) - { - Env env (*this, features (f)); - env.fund (XRP (10000), alice, bob, carol); - env (trust (bob, USDA (10))); - env (trust (carol, USDA (10)), qualityInPercent (carolAliceQIn)); + { + Env env(*this, features(fs)); + env.fund(XRP(10000), alice, bob, carol); + env(trust(bob, USDA(10))); + env(trust(carol, USDA(10)), qualityInPercent(carolAliceQIn)); - env (pay (alice, bob, USDA (10))); - env.require (balance (bob, USDA (10))); - env (pay (bob, carol, USDA (5)), sendmax (USDA (10))); - auto const effectiveQ = - carolAliceQIn > 100 ? 1.0 : carolAliceQIn / 100.0; - env.require (balance (bob, USDA (10.0 - 5.0 / effectiveQ))); - } + env(pay(alice, bob, USDA(10))); + env.require(balance(bob, USDA(10))); + env(pay(bob, carol, USDA(5)), sendmax(USDA(10))); + auto const effectiveQ = + carolAliceQIn > 100 ? 1.0 : carolAliceQIn / 100.0; + env.require(balance(bob, USDA(10.0 - 5.0 / effectiveQ))); + } // bob -> alice -> carol; bobAliceQOut varies. for (auto bobAliceQOut : {80, 100, 120}) - for (auto const& f : {feature ("nullFeature"), featureFlow}) - { - Env env (*this, features (f)); - env.fund (XRP (10000), alice, bob, carol); - env (trust (bob, USDA (10)), qualityOutPercent (bobAliceQOut)); - env (trust (carol, USDA (10))); + { + Env env(*this, features(fs)); + env.fund(XRP(10000), alice, bob, carol); + env(trust(bob, USDA(10)), qualityOutPercent(bobAliceQOut)); + env(trust(carol, USDA(10))); - env (pay (alice, bob, USDA (10))); - env.require (balance (bob, USDA (10))); - env (pay (bob, carol, USDA (5)), sendmax (USDA (5))); - env.require (balance (carol, USDA (5))); - env.require (balance (bob, USDA (10-5))); - } + env(pay(alice, bob, USDA(10))); + env.require(balance(bob, USDA(10))); + env(pay(bob, carol, USDA(5)), sendmax(USDA(5))); + env.require(balance(carol, USDA(5))); + env.require(balance(bob, USDA(10 - 5))); + } } - void testBookStep () + void testBookStep (std::initializer_list fs) { testcase ("Book Step"); @@ -618,7 +276,7 @@ struct Flow_test : public beast::unit_test::suite { // simple IOU/IOU offer - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -639,7 +297,7 @@ struct Flow_test : public beast::unit_test::suite } { // simple IOU/XRP XRP/IOU offer - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -663,7 +321,7 @@ struct Flow_test : public beast::unit_test::suite } { // simple XRP -> USD through offer and sendmax - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -684,7 +342,7 @@ struct Flow_test : public beast::unit_test::suite } { // simple USD -> XRP through offer and sendmax - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -705,7 +363,7 @@ struct Flow_test : public beast::unit_test::suite } { // test unfunded offers are removed when payment succeeds - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -751,7 +409,7 @@ struct Flow_test : public beast::unit_test::suite // offer. When the payment fails `flow` should return the unfunded // offer. This test is intentionally similar to the one that removes // unfunded offers when the payment succeeds. - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -778,7 +436,15 @@ struct Flow_test : public beast::unit_test::suite STAmount smax (BTC (61)); PaymentSandbox sb (env.current ().get (), tapNONE); STPathSet paths; + auto IPE = [](Issue const& iss) { + return STPathElement( + STPathElement::typeCurrency | STPathElement::typeIssuer, + xrpAccount(), + iss.currency, + iss.account); + }; { + // BTC -> USD STPath p1 ({IPE (USD.issue ())}); paths.push_back (p1); @@ -818,8 +484,7 @@ struct Flow_test : public beast::unit_test::suite // Without limits, the 0.4 USD would produce 1000 EUR in the forward // pass. This test checks that the payment produces 1 EUR, as expected. - Env env (*this, features (featureFlow), - features (featureOwnerPaysFee)); + Env env (*this, features (fs)); auto const closeTime = STAmountSO::soTime2 + 100 * env.closed ()->info ().closeTimeResolution; @@ -844,7 +509,7 @@ struct Flow_test : public beast::unit_test::suite } } - void testTransferRate () + void testTransferRate (std::initializer_list fs) { testcase ("Transfer Rate"); @@ -862,7 +527,7 @@ struct Flow_test : public beast::unit_test::suite { // Simple payment through a gateway with a // transfer rate - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, gw); env(rate(gw, 1.25)); @@ -874,7 +539,7 @@ struct Flow_test : public beast::unit_test::suite } { // transfer rate is not charged when issuer is src or dst - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, gw); env(rate(gw, 1.25)); @@ -886,7 +551,7 @@ struct Flow_test : public beast::unit_test::suite } { // transfer fee on an offer - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, gw); env(rate(gw, 1.25)); @@ -904,7 +569,7 @@ struct Flow_test : public beast::unit_test::suite { // Transfer fee two consecutive offers - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, carol, gw); env(rate(gw, 1.25)); @@ -927,7 +592,7 @@ struct Flow_test : public beast::unit_test::suite { // First pass through a strand redeems, second pass issues, no offers // limiting step is not an endpoint - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); auto const USDA = alice["USD"]; auto const USDB = bob["USD"]; @@ -947,7 +612,7 @@ struct Flow_test : public beast::unit_test::suite { // First pass through a strand redeems, second pass issues, through an offer // limiting step is not an endpoint - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); auto const USDA = alice["USD"]; auto const USDB = bob["USD"]; Account const dan ("dan"); @@ -974,7 +639,7 @@ struct Flow_test : public beast::unit_test::suite { // Offer where the owner is also the issuer, owner pays fee - Env env (*this, features(featureFlow), features(featureOwnerPaysFee)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, gw); env(rate(gw, 1.25)); @@ -986,9 +651,10 @@ struct Flow_test : public beast::unit_test::suite balance (alice, xrpMinusFee(env, 10000-100)), balance (bob, USD (100))); } + if (!hasFeature(featureOwnerPaysFee, fs)) { // Offer where the owner is also the issuer, sender pays fee - Env env (*this, features(featureFlow)); + Env env (*this, features(fs)); env.fund (XRP (10000), alice, bob, gw); env(rate(gw, 1.25)); @@ -1003,8 +669,10 @@ struct Flow_test : public beast::unit_test::suite } void - testFalseDryHelper (jtx::Env& env) + testFalseDry(std::initializer_list fs) { + testcase ("falseDryChanges"); + using namespace jtx; auto const gw = Account ("gateway"); @@ -1014,6 +682,8 @@ struct Flow_test : public beast::unit_test::suite Account const bob ("bob"); Account const carol ("carol"); + Env env (*this, features (fs)); + auto const closeTime = amendmentRIPD1141SoTime() + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); @@ -1045,21 +715,6 @@ struct Flow_test : public beast::unit_test::suite BEAST_EXPECT(carolUSD > USD (0) && carolUSD < USD (50)); } - void - testFalseDry () - { - testcase ("falseDryChanges"); - using namespace jtx; - { - Env env (*this, features (featureFlow)); - testFalseDryHelper (env); - } - { - Env env (*this); - testFalseDryHelper (env); - } - } - void testLimitQuality () { @@ -1123,7 +778,7 @@ struct Flow_test : public beast::unit_test::suite } void - testSelfPayment1() + testSelfPayment1(std::initializer_list fs) { testcase ("Self-payment 1"); @@ -1140,7 +795,7 @@ struct Flow_test : public beast::unit_test::suite auto const USD = gw1["USD"]; auto const EUR = gw2["EUR"]; - Env env (*this, features (featureFlow)); + Env env (*this, features (fs)); auto const closeTime = amendmentRIPD1141SoTime () + 100 * env.closed ()->info ().closeTimeResolution; @@ -1199,7 +854,7 @@ struct Flow_test : public beast::unit_test::suite } void - testSelfPayment2() + testSelfPayment2(std::initializer_list fs) { testcase ("Self-payment 2"); @@ -1214,7 +869,7 @@ struct Flow_test : public beast::unit_test::suite auto const USD = gw1["USD"]; auto const EUR = gw2["EUR"]; - Env env (*this, features (featureFlow)); + Env env (*this, features (fs)); auto const closeTime = amendmentRIPD1141SoTime () + 100 * env.closed ()->info ().closeTimeResolution; @@ -1271,7 +926,7 @@ struct Flow_test : public beast::unit_test::suite BEAST_EXPECT(offer[sfTakerPays] == USD (495)); } } - void testSelfFundedXRPEndpoint (bool consumeOffer) + void testSelfFundedXRPEndpoint (bool consumeOffer, std::initializer_list fs) { // Test that the deferred credit table is not bypassed for // XRPEndpointSteps. If the account in the first step is sending XRP and @@ -1282,7 +937,7 @@ struct Flow_test : public beast::unit_test::suite using namespace jtx; - Env env(*this, features(featureFlow)); + Env env(*this, features(fs)); // Need new behavior from `accountHolds` auto const closeTime = amendmentRIPD1141SoTime() + @@ -1305,7 +960,7 @@ struct Flow_test : public beast::unit_test::suite txflags(tfPartialPayment | tfNoRippleDirect)); } - void testUnfundedOffer (bool withFix) + void testUnfundedOffer (bool withFix, std::initializer_list fs) { testcase(std::string("Unfunded Offer ") + (withFix ? "with fix" : "without fix")); @@ -1313,7 +968,7 @@ struct Flow_test : public beast::unit_test::suite using namespace jtx; { // Test reverse - Env env(*this, features(featureFlow)); + Env env(*this, features(fs)); auto closeTime = amendmentRIPD1298SoTime(); if (withFix) closeTime += env.closed()->info().closeTimeResolution; @@ -1345,7 +1000,7 @@ struct Flow_test : public beast::unit_test::suite } { // Test forward - Env env(*this, features(featureFlow)); + Env env(*this, features(fs)); auto closeTime = amendmentRIPD1298SoTime(); if (withFix) closeTime += env.closed()->info().closeTimeResolution; @@ -1379,14 +1034,13 @@ struct Flow_test : public beast::unit_test::suite } } - template void - testReexecuteDirectStep(Features&&... fs) + testReexecuteDirectStep(std::initializer_list fs) { testcase("ReexecuteDirectStep"); using namespace jtx; - Env env(*this, features(fs)...); + Env env(*this, features(fs)); auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1488,22 +1142,32 @@ struct Flow_test : public beast::unit_test::suite void run() override { - testDirectStep (); - testLineQuality(); - testBookStep (); - testTransferRate (); - testToStrand (); - testFalseDry(); testLimitQuality(); - testSelfPayment1(); - testSelfPayment2(); - testSelfFundedXRPEndpoint(false); - testSelfFundedXRPEndpoint(true); - testUnfundedOffer(true); - testUnfundedOffer(false); - testReexecuteDirectStep(featureFlow, fix1368); testRIPD1443(true); testRIPD1443(false); + + auto testWithFeats = [this](auto&&... fs) + { + testLineQuality({fs...}); + testFalseDry({fs...}); + if (!sizeof...(fs)) + return; + testDirectStep({fs...}); + testBookStep({fs...}); + testDirectStep({featureOwnerPaysFee, fs...}); + testBookStep({featureOwnerPaysFee, fs...}); + testTransferRate({featureOwnerPaysFee, fs...}); + testSelfPayment1({fs...}); + testSelfPayment2({fs...}); + testSelfFundedXRPEndpoint(false, {fs...}); + testSelfFundedXRPEndpoint(true, {fs...}); + testUnfundedOffer(true, {fs...}); + testUnfundedOffer(false, {fs...}); + testReexecuteDirectStep({fix1368, fs...}); + }; + testWithFeats(); + testWithFeats(featureFlow); + testWithFeats(featureFlow, featureToStrandV2); } }; diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index db7fe38de6..9856175b4c 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -17,6 +17,7 @@ */ //============================================================================== #include +#include #include #include #include @@ -52,12 +53,12 @@ class Freeze_test : public beast::unit_test::suite return val.isArray() && val.size() == size; } - void testRippleState() + void testRippleState(std::initializer_list fs) { testcase("RippleState Freeze"); using namespace test::jtx; - Env env(*this); + Env env(*this, features(fs)); Account G1 {"G1"}; Account alice {"alice"}; @@ -206,12 +207,12 @@ class Freeze_test : public beast::unit_test::suite } void - testGlobalFreeze() + testGlobalFreeze(std::initializer_list fs) { testcase("Global Freeze"); using namespace test::jtx; - Env env(*this); + Env env(*this, features(fs)); Account G1 {"G1"}; Account A1 {"A1"}; @@ -364,12 +365,12 @@ class Freeze_test : public beast::unit_test::suite } void - testNoFreeze() + testNoFreeze(std::initializer_list fs) { testcase("No Freeze"); using namespace test::jtx; - Env env(*this); + Env env(*this, features(fs)); Account G1 {"G1"}; Account A1 {"A1"}; @@ -418,12 +419,12 @@ class Freeze_test : public beast::unit_test::suite } void - testOffersWhenFrozen() + testOffersWhenFrozen(std::initializer_list fs) { testcase("Offers for Frozen Trust Lines"); using namespace test::jtx; - Env env(*this); + Env env(*this, features(fs)); Account G1 {"G1"}; Account A2 {"A2"}; @@ -522,10 +523,16 @@ public: void run() { - testRippleState(); - testGlobalFreeze(); - testNoFreeze(); - testOffersWhenFrozen(); + auto testAll = [this](std::initializer_list fs) + { + testRippleState(fs); + testGlobalFreeze(fs); + testNoFreeze(fs); + testOffersWhenFrozen(fs); + }; + testAll({}); + testAll({featureFlow}); + testAll({featureFlow, featureToStrandV2}); } }; diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 22ac5cd561..2701302d0a 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -92,7 +92,7 @@ class Offer_test : public beast::unit_test::suite } public: - void testRmFundedOffer () + void testRmFundedOffer (std::initializer_list fs) { testcase ("Incorrect Removal of Funded Offers"); @@ -105,7 +105,7 @@ public: // still funded and not used for the payment. using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; // ledger close times have a dynamic resolution depending on network // conditions it appears the resolution in test is 10 seconds @@ -151,12 +151,12 @@ public: isOffer (env, carol, BTC (49), XRP (49))); } - void testCanceledOffer () + void testCanceledOffer (std::initializer_list fs) { testcase ("Removing Canceled Offers"); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const USD = gw["USD"]; @@ -206,7 +206,7 @@ public: BEAST_EXPECT(!isOffer (env, alice, XRP (222), USD (111))); } - void testTinyPayment () + void testTinyPayment (std::initializer_list fs) { testcase ("Tiny payments"); @@ -221,7 +221,7 @@ public: auto const USD = gw["USD"]; auto const EUR = gw["EUR"]; - Env env {*this}; + Env env {*this, features(fs)}; env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -248,7 +248,7 @@ public: } } - void testXRPTinyPayment () + void testXRPTinyPayment (std::initializer_list fs) { testcase ("XRP Tiny payments"); @@ -279,7 +279,10 @@ public: for (auto withFix : {false, true}) { - Env env {*this}; + if (!withFix && fs.size()) + continue; + + Env env {*this, features(fs)}; auto closeTime = [&] { @@ -352,7 +355,7 @@ public: } } - void testEnforceNoRipple () + void testEnforceNoRipple (std::initializer_list fs) { testcase ("Enforce No Ripple"); @@ -369,7 +372,7 @@ public: { // No ripple with an implied account step after an offer - Env env {*this}; + Env env {*this, features(fs)}; auto const gw1 = Account {"gw1"}; auto const USD1 = gw1["USD"]; auto const gw2 = Account {"gw2"}; @@ -419,7 +422,7 @@ public: } void - testInsufficientReserve () + testInsufficientReserve (std::initializer_list fs) { testcase ("Insufficient Reserve"); @@ -442,7 +445,7 @@ public: // No crossing: { - Env env {*this}; + Env env {*this, features(fs)}; env.fund (XRP (1000000), gw); auto const f = env.current ()->fees ().base; @@ -490,7 +493,7 @@ public: // if an offer were added. Attempt to sell IOUs to // buy XRP. If it fully crosses, we succeed. { - Env env {*this}; + Env env {*this, features(fs)}; env.fund (XRP (1000000), gw); auto const f = env.current ()->fees ().base; @@ -522,7 +525,7 @@ public: } void - testFillModes () + testFillModes (std::initializer_list fs) { testcase ("Fill Modes"); @@ -537,7 +540,7 @@ public: // Fill or Kill - unless we fully cross, just charge // a fee and not place the offer on the books: { - Env env {*this}; + Env env {*this, features(fs)}; env.fund (startBalance, gw); auto const f = env.current ()->fees ().base; @@ -579,7 +582,7 @@ public: // Immediate or Cancel - cross as much as possible // and add nothing on the books: { - Env env {*this}; + Env env {*this, features(fs)}; env.fund (startBalance, gw); auto const f = env.current ()->fees ().base; @@ -632,7 +635,7 @@ public: } void - testMalformed() + testMalformed(std::initializer_list fs) { testcase ("Malformed Detection"); @@ -643,7 +646,7 @@ public: auto const alice = Account {"alice"}; auto const USD = gw["USD"]; - Env env {*this}; + Env env {*this, features(fs)}; env.fund (startBalance, gw); env.fund (startBalance, alice); @@ -726,7 +729,7 @@ public: } void - testExpiration() + testExpiration(std::initializer_list fs) { testcase ("Offer Expiration"); @@ -799,7 +802,7 @@ public: } void - testUnfundedCross() + testUnfundedCross(std::initializer_list fs) { testcase ("Unfunded Crossing"); @@ -860,7 +863,7 @@ public: } void - testSelfCross(bool use_partner) + testSelfCross(bool use_partner, std::initializer_list fs) { testcase (std::string("Self-crossing") + (use_partner ? ", with partner account" : "")); @@ -872,7 +875,7 @@ public: auto const USD = gw["USD"]; auto const BTC = gw["BTC"]; - Env env {*this}; + Env env {*this, features(fs)}; env.fund (XRP (10000), gw); if (use_partner) { @@ -964,7 +967,7 @@ public: } void - testNegativeBalance() + testNegativeBalance(std::initializer_list fs) { // This test creates an offer test for negative balance // with transfer fees and miniscule funds. @@ -972,7 +975,7 @@ public: using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const bob = Account {"bob"}; @@ -1041,7 +1044,7 @@ public: } void - testOfferCrossWithXRP(bool reverse_order) + testOfferCrossWithXRP(bool reverse_order, std::initializer_list fs) { testcase (std::string("Offer Crossing with XRP, ") + (reverse_order ? "Reverse" : "Normal") + @@ -1049,7 +1052,7 @@ public: using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const bob = Account {"bob"}; @@ -1098,13 +1101,13 @@ public: } void - testOfferCrossWithLimitOverride() + testOfferCrossWithLimitOverride(std::initializer_list fs) { testcase ("Offer Crossing with Limit Override"); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const bob = Account {"bob"}; @@ -1143,13 +1146,13 @@ public: } void - testOfferAcceptThenCancel() + testOfferAcceptThenCancel(std::initializer_list fs) { testcase ("Offer Accept then Cancel."); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const USD = env.master["USD"]; auto const nextOfferSeq = env.seq (env.master); @@ -1170,14 +1173,14 @@ public: } void - testOfferCancelPastAndFuture() + testOfferCancelPastAndFuture(std::initializer_list fs) { testcase ("Offer Cancel Past and Future Sequence."); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const alice = Account {"alice"}; auto const nextOfferSeq = env.seq (env.master); @@ -1200,13 +1203,13 @@ public: } void - testCurrencyConversionEntire() + testCurrencyConversionEntire(std::initializer_list fs) { testcase ("Currency Conversion: Entire Offer"); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const bob = Account {"bob"}; @@ -1260,13 +1263,13 @@ public: } void - testCurrencyConversionIntoDebt() + testCurrencyConversionIntoDebt(std::initializer_list fs) { testcase ("Currency Conversion: Offerer Into Debt"); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const alice = Account {"alice"}; auto const bob = Account {"bob"}; auto const carol = Account {"carol"}; @@ -1288,13 +1291,13 @@ public: } void - testCurrencyConversionInParts() + testCurrencyConversionInParts(std::initializer_list fs) { testcase ("Currency Conversion: In Parts"); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const bob = Account {"bob"}; @@ -1375,13 +1378,13 @@ public: } void - testCrossCurrencyStartXRP() + testCrossCurrencyStartXRP(std::initializer_list fs) { testcase ("Cross Currency Payment: Start with XRP"); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const bob = Account {"bob"}; @@ -1414,13 +1417,13 @@ public: } void - testCrossCurrencyEndXRP() + testCrossCurrencyEndXRP(std::initializer_list fs) { testcase ("Cross Currency Payment: End with XRP"); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const bob = Account {"bob"}; @@ -1461,13 +1464,13 @@ public: } void - testCrossCurrencyBridged() + testCrossCurrencyBridged(std::initializer_list fs) { testcase ("Cross Currency Payment: Bridged"); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw1 = Account {"gateway_1"}; auto const gw2 = Account {"gateway_2"}; auto const alice = Account {"alice"}; @@ -1525,13 +1528,13 @@ public: } void - testOfferFeesConsumeFunds() + testOfferFeesConsumeFunds(std::initializer_list fs) { testcase ("Offer Fees Consume Funds"); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw1 = Account {"gateway_1"}; auto const gw2 = Account {"gateway_2"}; auto const gw3 = Account {"gateway_3"}; @@ -1578,13 +1581,13 @@ public: } void - testOfferCreateThenCross() + testOfferCreateThenCross(std::initializer_list fs) { testcase ("Offer Create, then Cross"); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const bob = Account {"bob"}; @@ -1613,13 +1616,13 @@ public: } void - testSellFlagBasic() + testSellFlagBasic(std::initializer_list fs) { testcase ("Offer tfSell: Basic Sell"); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const bob = Account {"bob"}; @@ -1654,13 +1657,13 @@ public: } void - testSellFlagExceedLimit() + testSellFlagExceedLimit(std::initializer_list fs) { testcase ("Offer tfSell: 2x Sell Exceed Limit"); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const bob = Account {"bob"}; @@ -1697,13 +1700,13 @@ public: } void - testGatewayCrossCurrency() + testGatewayCrossCurrency(std::initializer_list fs) { testcase ("Client Issue #535: Gateway Cross Currency"); using namespace jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const bob = Account {"bob"}; @@ -1764,7 +1767,7 @@ public: BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-101"); } - void testTickSize () + void testTickSize (std::initializer_list fs) { testcase ("Tick Size"); @@ -1772,7 +1775,7 @@ public: // Try to set tick size without enabling feature { - Env env {*this}; + Env env {*this, features(fs)}; auto const gw = Account {"gateway"}; env.fund (XRP(10000), gw); @@ -1783,7 +1786,7 @@ public: // Try to set tick size out of range { - Env env {*this, features (featureTickSize)}; + Env env {*this, features(fs), features (featureTickSize)}; auto const gw = Account {"gateway"}; env.fund (XRP(10000), gw); @@ -1816,7 +1819,7 @@ public: BEAST_EXPECT (! env.le(gw)->isFieldPresent (sfTickSize)); } - Env env {*this, features (featureTickSize)}; + Env env {*this, features(fs), features (featureTickSize)}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const XTS = gw["XTS"]; @@ -1886,36 +1889,41 @@ public: void run () { - testCanceledOffer (); - testRmFundedOffer (); - testTinyPayment (); - testXRPTinyPayment (); - testEnforceNoRipple (); - testInsufficientReserve (); - testFillModes (); - testMalformed (); - testExpiration (); - testUnfundedCross (); - testSelfCross (false); - testSelfCross (true); - testNegativeBalance (); - testOfferCrossWithXRP (true); - testOfferCrossWithXRP (false); - testOfferCrossWithLimitOverride (); - testOfferAcceptThenCancel (); - testOfferCancelPastAndFuture (); - testCurrencyConversionEntire (); - testCurrencyConversionIntoDebt (); - testCurrencyConversionInParts (); - testCrossCurrencyStartXRP (); - testCrossCurrencyEndXRP (); - testCrossCurrencyBridged (); - testOfferFeesConsumeFunds (); - testOfferCreateThenCross (); - testSellFlagBasic (); - testSellFlagExceedLimit (); - testGatewayCrossCurrency (); - testTickSize (); + auto testAll = [this](std::initializer_list fs) { + testCanceledOffer(fs); + testRmFundedOffer(fs); + testTinyPayment(fs); + testXRPTinyPayment(fs); + testEnforceNoRipple(fs); + testInsufficientReserve(fs); + testFillModes(fs); + testMalformed(fs); + testExpiration(fs); + testUnfundedCross(fs); + testSelfCross(false, fs); + testSelfCross(true, fs); + testNegativeBalance(fs); + testOfferCrossWithXRP(true, fs); + testOfferCrossWithXRP(false, fs); + testOfferCrossWithLimitOverride(fs); + testOfferAcceptThenCancel(fs); + testOfferCancelPastAndFuture(fs); + testCurrencyConversionEntire(fs); + testCurrencyConversionIntoDebt(fs); + testCurrencyConversionInParts(fs); + testCrossCurrencyStartXRP(fs); + testCrossCurrencyEndXRP(fs); + testCrossCurrencyBridged(fs); + testOfferFeesConsumeFunds(fs); + testOfferCreateThenCross(fs); + testSellFlagBasic(fs); + testSellFlagExceedLimit(fs); + testGatewayCrossCurrency(fs); + testTickSize(fs); + }; + testAll({}); + testAll({featureFlow}); + testAll({featureFlow, featureToStrandV2}); } }; diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp new file mode 100644 index 0000000000..c16f9d5cfa --- /dev/null +++ b/src/test/app/PayStrand_test.cpp @@ -0,0 +1,1416 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 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 +#include +#include +#include +#include + +namespace ripple { +namespace test { + +struct DirectStepInfo +{ + AccountID src; + AccountID dst; + Currency currency; +}; + +struct XRPEndpointStepInfo +{ + AccountID acc; +}; + +enum class TrustFlag { freeze, auth }; + +std::uint32_t +trustFlag(TrustFlag f, bool useHigh) +{ + switch (f) + { + case TrustFlag::freeze: + if (useHigh) + return lsfHighFreeze; + return lsfLowFreeze; + case TrustFlag::auth: + if (useHigh) + return lsfHighAuth; + return lsfLowAuth; + } + return 0; // Silence warning about end of non-void function +} + +bool +getTrustFlag( + jtx::Env const& env, + jtx::Account const& src, + jtx::Account const& dst, + Currency const& cur, + TrustFlag flag) +{ + if (auto sle = env.le(keylet::line(src, dst, cur))) + { + auto const useHigh = src.id() > dst.id(); + return sle->isFlag(trustFlag(flag, useHigh)); + } + Throw("No line in getTrustFlag"); + return false; // silence warning +} + +bool +equal(std::unique_ptr const& s1, DirectStepInfo const& dsi) +{ + if (!s1) + return false; + return test::directStepEqual(*s1, dsi.src, dsi.dst, dsi.currency); +} + +bool +equal(std::unique_ptr const& s1, XRPEndpointStepInfo const& xrpsi) +{ + if (!s1) + return false; + return test::xrpEndpointStepEqual(*s1, xrpsi.acc); +} + +bool +equal(std::unique_ptr const& s1, ripple::Book const& bsi) +{ + if (!s1) + return false; + return bookStepEqual(*s1, bsi); +} + +template +bool +strandEqualHelper(Iter i) +{ + // base case. all args processed and found equal. + return true; +} + +template +bool +strandEqualHelper(Iter i, StepInfo&& si, Args&&... args) +{ + if (!equal(*i, std::forward(si))) + return false; + return strandEqualHelper(++i, std::forward(args)...); +} + +template +bool +equal(Strand const& strand, Args&&... args) +{ + if (strand.size() != sizeof...(Args)) + return false; + if (strand.empty()) + return true; + return strandEqualHelper(strand.begin(), std::forward(args)...); +} + +STPathElement +ape(AccountID const& a) +{ + return STPathElement( + STPathElement::typeAccount, a, xrpCurrency(), xrpAccount()); +}; + +// Issue path element +STPathElement +ipe(Issue const& iss) +{ + return STPathElement( + STPathElement::typeCurrency | STPathElement::typeIssuer, + xrpAccount(), + iss.currency, + iss.account); +}; + +// Issuer path element +STPathElement +iape(AccountID const& account) +{ + return STPathElement( + STPathElement::typeIssuer, xrpAccount(), xrpCurrency(), account); +}; + +// Currency path element +STPathElement +cpe(Currency const& c) +{ + return STPathElement( + STPathElement::typeCurrency, xrpAccount(), c, xrpAccount()); +}; + +// All path element +STPathElement +allpe(AccountID const& a, Issue const& iss) +{ + return STPathElement( + STPathElement::typeAccount | STPathElement::typeCurrency | + STPathElement::typeIssuer, + a, + iss.currency, + iss.account); +}; + +class ElementComboIter +{ + enum class SB /*state bit*/ + { acc, + iss, + cur, + rootAcc, + rootIss, + xrp, + sameAccIss, + existingAcc, + existingCur, + existingIss, + prevAcc, + prevCur, + prevIss, + boundary, + last }; + + std::uint16_t state_ = 0; + static_assert(static_cast(SB::last) <= sizeof(decltype(state_)) * 8, ""); + STPathElement const* prev_ = nullptr; + // disallow iss and cur to be specified with acc is specified (simplifies some tests) + bool const allowCompound_ = false; + + bool + has(SB s) const + { + return state_ & (1 << static_cast(s)); + } + + bool + hasAny(std::initializer_list sb) const + { + for (auto const s : sb) + if (has(s)) + return true; + return false; + } + + size_t + count(std::initializer_list sb) const + { + size_t result=0; + + for (auto const s : sb) + if (has(s)) + result++; + return result; + } + +public: + explicit ElementComboIter(STPathElement const* prev = nullptr) : prev_(prev) + { + } + + bool + valid() const + { + return + (allowCompound_ || !(has(SB::acc) && hasAny({SB::cur, SB::iss}))) && + (!hasAny({SB::prevAcc, SB::prevCur, SB::prevIss}) || prev_) && + (!hasAny({SB::rootAcc, SB::sameAccIss, SB::existingAcc, SB::prevAcc}) || has(SB::acc)) && + (!hasAny({SB::rootIss, SB::sameAccIss, SB::existingIss, SB::prevIss}) || has(SB::iss)) && + (!hasAny({SB::xrp, SB::existingCur, SB::prevCur}) || has(SB::cur)) && + // These will be duplicates + (count({SB::xrp, SB::existingCur, SB::prevCur}) <= 1) && + (count({SB::rootAcc, SB::existingAcc, SB::prevAcc}) <= 1) && + (count({SB::rootIss, SB::existingIss, SB::rootIss}) <= 1); + } + bool + next() + { + if (!(has(SB::last))) + { + do + { + ++state_; + } while (!valid()); + } + return !has(SB::last); + } + + template < + class Col, + class AccFactory, + class IssFactory, + class CurrencyFactory> + void + emplace_into( + Col& col, + AccFactory&& accF, + IssFactory&& issF, + CurrencyFactory&& currencyF, + boost::optional const& existingAcc, + boost::optional const& existingCur, + boost::optional const& existingIss) + { + assert(!has(SB::last)); + + auto const acc = [&]() -> boost::optional { + if (!has(SB::acc)) + return boost::none; + if (has(SB::rootAcc)) + return xrpAccount(); + if (has(SB::existingAcc) && existingAcc) + return existingAcc; + return accF().id(); + }(); + auto const iss = [&]() -> boost::optional { + if (!has(SB::iss)) + return boost::none; + if (has(SB::rootIss)) + return xrpAccount(); + if (has(SB::sameAccIss)) + return acc; + if (has(SB::existingIss) && existingIss) + return *existingIss; + return issF().id(); + }(); + auto const cur = [&]() -> boost::optional { + if (!has(SB::cur)) + return boost::none; + if (has(SB::xrp)) + return xrpCurrency(); + if (has(SB::existingCur) && existingCur) + return *existingCur; + return currencyF(); + }(); + if (!has(SB::boundary)) + col.emplace_back(acc, cur, iss); + else + col.emplace_back( + STPathElement::Type::typeBoundary, + acc.value_or(AccountID{}), + cur.value_or(Currency{}), + iss.value_or(AccountID{})); + } +}; + +struct ExistingElementPool +{ + std::vector accounts; + std::vector currencies; + std::vector currencyNames; + + jtx::Account + getAccount(size_t id) + { + assert(id < accounts.size()); + return accounts[id]; + } + + ripple::Currency + getCurrency(size_t id) + { + assert(id < currencies.size()); + return currencies[id]; + } + + // ids from 0 through (nextAvail -1) have already been used in the + // path + size_t nextAvailAccount = 0; + size_t nextAvailCurrency = 0; + + using ResetState = std::tuple; + ResetState + getResetState() const + { + return std::make_tuple(nextAvailAccount, nextAvailCurrency); + } + + void + resetTo(ResetState const& s) + { + std::tie(nextAvailAccount, nextAvailCurrency) = s; + } + + struct StateGuard + { + ExistingElementPool& p_; + ResetState state_; + + StateGuard(ExistingElementPool& p) : p_{p}, state_{p.getResetState()} + { + } + ~StateGuard() + { + p_.resetTo(state_); + } + }; + + // Create the given number of accounts, and add trust lines so every + // account trusts every other with every currency + // Create an offer from every currency/account to every other + // currency/account; the offer owner is either the specified + // account or the issuer of the "taker gets" account + void + setupEnv( + jtx::Env& env, + size_t numAct, + size_t numCur, + boost::optional const& offererIndex) + { + using namespace jtx; + + assert(!offererIndex || offererIndex < numAct); + + accounts.clear(); + accounts.reserve(numAct); + currencies.clear(); + currencies.reserve(numCur); + currencyNames.clear(); + currencyNames.reserve(numCur); + + constexpr size_t bufSize = 8; + char buf[bufSize]; + + for (size_t id = 0; id < numAct; ++id) + { + snprintf(buf, bufSize, "A%zu", id); + accounts.emplace_back(buf); + } + + for (size_t id = 0; id < numCur; ++id) + { + if (id < 10) + snprintf(buf, bufSize, "CC%zu", id); + else if (id < 100) + snprintf(buf, bufSize, "C%zu", id); + else + snprintf(buf, bufSize, "%zu", id); + currencies.emplace_back(to_currency(buf)); + currencyNames.emplace_back(buf); + } + + for (auto const& a : accounts) + env.fund(XRP(100000), a); + + // Every account trusts every other account with every currency + for (auto ai1 = accounts.begin(), aie = accounts.end(); ai1 != aie; + ++ai1) + { + for (auto ai2 = accounts.begin(); ai2 != aie; ++ai2) + { + if (ai1 == ai2) + continue; + for (auto const& cn : currencyNames) + { + env.trust((*ai1)[cn](1'000'000), *ai2); + if (ai1 > ai2) + { + // accounts with lower indexes hold balances from + // accounts + // with higher indexes + auto const& src = *ai1; + auto const& dst = *ai2; + env(pay(src, dst, src[cn](500000))); + } + } + env.close(); + } + } + + std::vector ious; + ious.reserve(numAct * numCur); + for (auto const& a : accounts) + for (auto const& cn : currencyNames) + ious.emplace_back(a[cn]); + + // create offers from every currency to every other currency + for (auto takerPays = ious.begin(), ie = ious.end(); takerPays != ie; + ++takerPays) + { + for (auto takerGets = ious.begin(); takerGets != ie; ++takerGets) + { + if (takerPays == takerGets) + continue; + auto const owner = + offererIndex ? accounts[*offererIndex] : takerGets->account; + if (owner.id() != takerGets->account.id()) + env(pay(takerGets->account, owner, (*takerGets)(1000))); + + env(offer(owner, (*takerPays)(1000), (*takerGets)(1000)), + txflags(tfPassive)); + } + env.close(); + } + + // create offers to/from xrp to every other ious + for (auto const& iou : ious) + { + auto const owner = + offererIndex ? accounts[*offererIndex] : iou.account; + env(offer(owner, iou(1000), XRP(1000)), txflags(tfPassive)); + env(offer(owner, XRP(1000), iou(1000)), txflags(tfPassive)); + env.close(); + } + } + + std::int64_t + totalXRP(ReadView const& v, bool incRoot) + { + std::uint64_t totalXRP = 0; + auto add = [&](auto const& a) { + // XRP balance + auto const sle = v.read(keylet::account(a)); + if (!sle) + return; + auto const b = (*sle)[sfBalance]; + totalXRP += b.mantissa(); + }; + for (auto const& a : accounts) + add(a); + if (incRoot) + add(xrpAccount()); + return totalXRP; + } + + // Check that the balances for all accounts for all currencies & XRP are the + // same + bool + checkBalances(ReadView const& v1, ReadView const& v2) + { + std::vector> diffs; + + auto xrpBalance = [](ReadView const& v, ripple::Keylet const& k) { + auto const sle = v.read(k); + if (!sle) + return STAmount{}; + return (*sle)[sfBalance]; + }; + auto lineBalance = [](ReadView const& v, ripple::Keylet const& k) { + auto const sle = v.read(k); + if (!sle) + return STAmount{}; + return (*sle)[sfBalance]; + }; + std::uint64_t totalXRP[2]; + for (auto ai1 = accounts.begin(), aie = accounts.end(); ai1 != aie; + ++ai1) + { + { + // XRP balance + auto const ak = keylet::account(*ai1); + auto const b1 = xrpBalance(v1, ak); + auto const b2 = xrpBalance(v2, ak); + totalXRP[0] += b1.mantissa(); + totalXRP[1] += b2.mantissa(); + if (b1 != b2) + diffs.emplace_back(b1, b2, xrpAccount(), *ai1); + } + for (auto ai2 = accounts.begin(); ai2 != aie; ++ai2) + { + if (ai1 >= ai2) + continue; + for (auto const& c : currencies) + { + // Line balance + auto const lk = keylet::line(*ai1, *ai2, c); + auto const b1 = lineBalance(v1, lk); + auto const b2 = lineBalance(v2, lk); + if (b1 != b2) + diffs.emplace_back(b1, b2, *ai1, *ai2); + } + } + } + return diffs.empty(); + } + + jtx::Account + getAvailAccount() + { + return getAccount(nextAvailAccount++); + } + + ripple::Currency + getAvailCurrency() + { + return getCurrency(nextAvailCurrency++); + } + + template + void + for_each_element_pair( + STAmount const& sendMax, + STAmount const& deliver, + std::vector const& prefix, + std::vector const& suffix, + boost::optional const& existingAcc, + boost::optional const& existingCur, + boost::optional const& existingIss, + F&& f) + { + auto accF = [&] { return this->getAvailAccount(); }; + auto issF = [&] { return this->getAvailAccount(); }; + auto currencyF = [&] { return this->getAvailCurrency(); }; + + STPathElement const* prevOuter = + prefix.empty() ? nullptr : &prefix.back(); + ElementComboIter outer(prevOuter); + + std::vector outerResult; + std::vector result; + auto const resultSize = prefix.size() + suffix.size() + 2; + outerResult.reserve(resultSize); + result.reserve(resultSize); + while (outer.next()) + { + StateGuard og{*this}; + outerResult = prefix; + outer.emplace_into( + outerResult, + accF, + issF, + currencyF, + existingAcc, + existingCur, + existingIss); + STPathElement const* prevInner = &outerResult.back(); + ElementComboIter inner(prevInner); + while (inner.next()) + { + StateGuard ig{*this}; + result = outerResult; + inner.emplace_into( + result, + accF, + issF, + currencyF, + existingAcc, + existingCur, + existingIss); + result.insert(result.end(), suffix.begin(), suffix.end()); + f(sendMax, deliver, result); + } + }; + } +}; + +struct PayStrand_test : public beast::unit_test::suite +{ + static bool hasFeature(uint256 const& feat) + { + return false; + } + + static bool hasFeature(uint256 const& feat, std::initializer_list args) + { + for(auto const& f : args) + if (f == feat) + return true; + return false; + } + + // Test every combination of element type pairs on a path + void + testAllPairs() + { + testcase("All pairs"); + using namespace jtx; + using RippleCalc = ::ripple::path::RippleCalc; + + ExistingElementPool eep; + Env env(*this, features(featureToStrandV2)); + + auto const closeTime = amendmentRIPD1298SoTime() + + 100 * env.closed()->info().closeTimeResolution; + env.close(closeTime); + eep.setupEnv(env, /*numAcc*/ 9, /*numCur*/ 6, boost::none); + env.close(); + + auto const src = eep.getAvailAccount(); + auto const dst = eep.getAvailAccount(); + + RippleCalc::Input inputs; + inputs.defaultPathsAllowed = false; + + auto callback = [&]( + STAmount const& sendMax, + STAmount const& deliver, + std::vector const& p) { + std::array sbs{ + {PaymentSandbox{env.current().get(), tapNONE}, + PaymentSandbox{env.current().get(), tapNONE}}}; + std::array rcOutputs; + // pay with both env1 and env2 + // check all result and account balances match + // save results so can see if run out of funds or somesuch + STPathSet paths; + paths.emplace_back(p); + for (auto i = 0; i < 2; ++i) + { + if (i == 0) + env.app().config().features.insert(featureFlow); + else + env.app().config().features.erase(featureFlow); + + try + { + rcOutputs[i] = RippleCalc::rippleCalculate( + sbs[i], + sendMax, + deliver, + dst, + src, + paths, + env.app().logs(), + &inputs); + } + catch (...) + { + this->fail(); + } + } + + // check combinations of src and dst currencies (inc xrp) + // Check the results + auto const terMatch = [&] { + if (rcOutputs[0].result() == rcOutputs[1].result()) + return true; + + // handle some know error code mismatches + if (p.empty() || + !(rcOutputs[0].result() == temBAD_PATH || + rcOutputs[0].result() == temBAD_PATH_LOOP)) + return false; + + if (rcOutputs[1].result() == temBAD_PATH) + return true; + + if (rcOutputs[1].result() == terNO_LINE) + return true; + + for (auto const& pe : p) + { + auto const t = pe.getNodeType(); + if ((t & STPathElement::typeAccount) && + t != STPathElement::typeAccount) + { + return true; + } + } + + // xrp followed by offer that doesn't specify both currency and + // issuer (and currency is not xrp, if specifyed) + if (isXRP(sendMax) && + !(p[0].hasCurrency() && isXRP(p[0].getCurrency())) && + !(p[0].hasCurrency() && p[0].hasIssuer())) + { + return true; + } + + for (size_t i = 0; i < p.size() - 1; ++i) + { + auto const tCur = p[i].getNodeType(); + auto const tNext = p[i + 1].getNodeType(); + if ((tCur & STPathElement::typeCurrency) && + isXRP(p[i].getCurrency()) && + (tNext & STPathElement::typeAccount) && + !isXRP(p[i + 1].getAccountID())) + { + return true; + } + } + return false; + }(); + + this->BEAST_EXPECT( + terMatch && (rcOutputs[0].result() == tesSUCCESS || + rcOutputs[0].result() == temBAD_PATH || + rcOutputs[0].result() == temBAD_PATH_LOOP)); + if (terMatch && rcOutputs[0].result() == tesSUCCESS) + this->BEAST_EXPECT(eep.checkBalances(sbs[0], sbs[1])); + }; + + std::vector prefix; + std::vector suffix; + + for (auto srcAmtIsXRP : {false, true}) + { + for (auto dstAmtIsXRP : {false, true}) + { + for (auto hasPrefix : {false, true}) + { + ExistingElementPool::StateGuard esg{eep}; + prefix.clear(); + suffix.clear(); + + STAmount const sendMax{ + srcAmtIsXRP ? xrpIssue() : Issue{eep.getAvailCurrency(), + eep.getAvailAccount()}, + -1, // (-1 == no limit) + 0}; + + STAmount const deliver{ + dstAmtIsXRP ? xrpIssue() : Issue{eep.getAvailCurrency(), + eep.getAvailAccount()}, + 1, + 0}; + + if (hasPrefix) + { + for(auto e0IsAccount : {false, true}) + { + for (auto e1IsAccount : {false, true}) + { + ExistingElementPool::StateGuard presg{eep}; + prefix.clear(); + auto pushElement = + [&prefix, &eep](bool isAccount) mutable { + if (isAccount) + prefix.emplace_back( + eep.getAvailAccount().id(), + boost::none, + boost::none); + else + prefix.emplace_back( + boost::none, + eep.getAvailCurrency(), + eep.getAvailAccount().id()); + }; + pushElement(e0IsAccount); + pushElement(e1IsAccount); + boost::optional existingAcc; + boost::optional existingCur; + boost::optional existingIss; + if (e0IsAccount) + { + existingAcc = prefix[0].getAccountID(); + } + else + { + existingIss = prefix[0].getIssuerID(); + existingCur = prefix[0].getCurrency(); + } + if (e1IsAccount) + { + if (!existingAcc) + existingAcc = prefix[1].getAccountID(); + } + else + { + if (!existingIss) + existingIss = prefix[1].getIssuerID(); + if (!existingCur) + existingCur = prefix[1].getCurrency(); + } + eep.for_each_element_pair( + sendMax, + deliver, + prefix, + suffix, + existingAcc, + existingCur, + existingIss, + callback); + } + } + } + else + { + eep.for_each_element_pair( + sendMax, + deliver, + prefix, + suffix, + /*existingAcc*/ boost::none, + /*existingCur*/ boost::none, + /*existingIss*/ boost::none, + callback); + } + } + } + } + } + + void + testToStrand(std::initializer_list fs) + { + testcase("To Strand"); + + using namespace jtx; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + + auto const USD = gw["USD"]; + auto const EUR = gw["EUR"]; + + auto const eurC = EUR.currency; + auto const usdC = USD.currency; + + using D = DirectStepInfo; + using B = ripple::Book; + using XRPS = XRPEndpointStepInfo; + + auto test = [&, this]( + jtx::Env& env, + Issue const& deliver, + boost::optional const& sendMaxIssue, + STPath const& path, + TER expTer, + auto&&... expSteps) { + auto r = toStrand( + *env.current(), + alice, + bob, + deliver, + sendMaxIssue, + path, + true, + env.app().logs().journal("Flow")); + BEAST_EXPECT(r.first == expTer); + if (sizeof...(expSteps)) + BEAST_EXPECT(equal( + r.second, std::forward(expSteps)...)); + }; + + { + Env env(*this, features(fs)); + env.fund(XRP(10000), alice, bob, gw); + env.trust(USD(1000), alice, bob); + env.trust(EUR(1000), alice, bob); + env(pay(gw, alice, EUR(100))); + + { + STPath const path = + STPath({ipe(bob["USD"]), cpe(EUR.currency)}); + auto r = toStrand( + *env.current(), + alice, + alice, + /*deliver*/ xrpIssue(), + /*sendMaxIssue*/ EUR.issue(), + path, + true, + env.app().logs().journal("Flow")); + BEAST_EXPECT(r.first == tesSUCCESS); + } + { + STPath const path = STPath({ipe(USD), cpe(xrpCurrency())}); + auto r = toStrand( + *env.current(), + alice, + alice, + /*deliver*/ xrpIssue(), + /*sendMaxIssue*/ xrpIssue(), + path, + true, + env.app().logs().journal("Flow")); + BEAST_EXPECT(r.first == tesSUCCESS); + } + return; + }; + + { + Env env(*this, features(fs)); + env.fund(XRP(10000), alice, bob, carol, gw); + + test(env, USD, boost::none, STPath(), terNO_LINE); + + env.trust(USD(1000), alice, bob, carol); + test(env, USD, boost::none, STPath(), tecPATH_DRY); + + env(pay(gw, alice, USD(100))); + env(pay(gw, carol, USD(100))); + + // Insert implied account + test( + env, + USD, + boost::none, + STPath(), + tesSUCCESS, + D{alice, gw, usdC}, + D{gw, bob, usdC}); + env.trust(EUR(1000), alice, bob); + + // Insert implied offer + test( + env, + EUR, + USD.issue(), + STPath(), + tesSUCCESS, + D{alice, gw, usdC}, + B{USD, EUR}, + D{gw, bob, eurC}); + + // Path with explicit offer + test( + env, + EUR, + USD.issue(), + STPath({ipe(EUR)}), + tesSUCCESS, + D{alice, gw, usdC}, + B{USD, EUR}, + D{gw, bob, eurC}); + + // Path with offer that changes issuer only + env.trust(carol["USD"](1000), bob); + test( + env, + carol["USD"], + USD.issue(), + STPath({iape(carol)}), + tesSUCCESS, + D{alice, gw, usdC}, + B{USD, carol["USD"]}, + D{carol, bob, usdC}); + + // Path with XRP src currency + test( + env, + USD, + xrpIssue(), + STPath({ipe(USD)}), + tesSUCCESS, + XRPS{alice}, + B{XRP, USD}, + D{gw, bob, usdC}); + + // Path with XRP dst currency + test( + env, + xrpIssue(), + USD.issue(), + STPath({ipe(XRP)}), + tesSUCCESS, + D{alice, gw, usdC}, + B{USD, XRP}, + XRPS{bob}); + + // Path with XRP cross currency bridged payment + test( + env, + EUR, + USD.issue(), + STPath({cpe(xrpCurrency())}), + tesSUCCESS, + D{alice, gw, usdC}, + B{USD, XRP}, + B{XRP, EUR}, + D{gw, bob, eurC}); + + // XRP -> XRP transaction can't include a path + test(env, XRP, boost::none, STPath({ape(carol)}), temBAD_PATH); + + { + // The root account can't be the src or dst + auto flowJournal = env.app().logs().journal("Flow"); + { + // The root account can't be the dst + auto r = toStrand( + *env.current(), + alice, + xrpAccount(), + XRP, + USD.issue(), + STPath(), + true, + flowJournal); + BEAST_EXPECT(r.first == temBAD_PATH); + } + { + // The root account can't be the src + auto r = toStrand( + *env.current(), + xrpAccount(), + alice, + XRP, + boost::none, + STPath(), + true, + flowJournal); + BEAST_EXPECT(r.first == temBAD_PATH); + } + { + // The root account can't be the src + auto r = toStrand( + *env.current(), + noAccount(), + bob, + USD, + boost::none, + STPath(), + true, + flowJournal); + BEAST_EXPECT(r.first == terNO_ACCOUNT); + } + } + + // Create an offer with the same in/out issue + test( + env, + EUR, + USD.issue(), + STPath({ipe(USD), ipe(EUR)}), + temBAD_PATH); + + // Path element with type zero + test( + env, + USD, + boost::none, + STPath({STPathElement( + 0, xrpAccount(), xrpCurrency(), xrpAccount())}), + temBAD_PATH); + + // The same account can't appear more than once on a path + // `gw` will be used from alice->carol and implied between carol + // and bob + test( + env, + USD, + boost::none, + STPath({ape(gw), ape(carol)}), + temBAD_PATH_LOOP); + + // The same offer can't appear more than once on a path + test( + env, + EUR, + USD.issue(), + STPath({ipe(EUR), ipe(USD), ipe(EUR)}), + temBAD_PATH_LOOP); + } + + { + // cannot have more than one offer with the same output issue + + using namespace jtx; + Env env(*this, features(fs)); + + env.fund(XRP(10000), alice, bob, carol, gw); + env.trust(USD(10000), alice, bob, carol); + env.trust(EUR(10000), alice, bob, carol); + + env(pay(gw, bob, USD(100))); + env(pay(gw, bob, EUR(100))); + + env(offer(bob, XRP(100), USD(100))); + env(offer(bob, USD(100), EUR(100)), txflags(tfPassive)); + env(offer(bob, EUR(100), USD(100)), txflags(tfPassive)); + + // payment path: XRP -> XRP/USD -> USD/EUR -> EUR/USD + env(pay(alice, carol, USD(100)), + path(~USD, ~EUR, ~USD), + sendmax(XRP(200)), + txflags(tfNoRippleDirect), + ter(temBAD_PATH_LOOP)); + } + + { + Env env(*this, features(fs)); + env.fund(XRP(10000), alice, bob, noripple(gw)); + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + test(env, USD, boost::none, STPath(), terNO_RIPPLE); + } + + { + // check global freeze + Env env(*this, features(fs)); + env.fund(XRP(10000), alice, bob, gw); + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + + // Account can still issue payments + env(fset(alice, asfGlobalFreeze)); + test(env, USD, boost::none, STPath(), tesSUCCESS); + env(fclear(alice, asfGlobalFreeze)); + test(env, USD, boost::none, STPath(), tesSUCCESS); + + // Account can not issue funds + env(fset(gw, asfGlobalFreeze)); + test(env, USD, boost::none, STPath(), terNO_LINE); + env(fclear(gw, asfGlobalFreeze)); + test(env, USD, boost::none, STPath(), tesSUCCESS); + + // Account can not receive funds + env(fset(bob, asfGlobalFreeze)); + test(env, USD, boost::none, STPath(), terNO_LINE); + env(fclear(bob, asfGlobalFreeze)); + test(env, USD, boost::none, STPath(), tesSUCCESS); + } + { + // Freeze between gw and alice + Env env(*this, features(fs)); + env.fund(XRP(10000), alice, bob, gw); + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + test(env, USD, boost::none, STPath(), tesSUCCESS); + env(trust(gw, alice["USD"](0), tfSetFreeze)); + BEAST_EXPECT(getTrustFlag(env, gw, alice, usdC, TrustFlag::freeze)); + test(env, USD, boost::none, STPath(), terNO_LINE); + } + { + // check no auth + // An account may require authorization to receive IOUs from an + // issuer + Env env(*this, features(fs)); + env.fund(XRP(10000), alice, bob, gw); + env(fset(gw, asfRequireAuth)); + env.trust(USD(1000), alice, bob); + // Authorize alice but not bob + env(trust(gw, alice["USD"](1000), tfSetfAuth)); + BEAST_EXPECT(getTrustFlag(env, gw, alice, usdC, TrustFlag::auth)); + env(pay(gw, alice, USD(100))); + env.require(balance(alice, USD(100))); + test(env, USD, boost::none, STPath(), terNO_AUTH); + + // Check pure issue redeem still works + auto r = toStrand( + *env.current(), + alice, + gw, + USD, + boost::none, + STPath(), + true, + env.app().logs().journal("Flow")); + BEAST_EXPECT(r.first == tesSUCCESS); + BEAST_EXPECT(equal(r.second, D{alice, gw, usdC})); + } + { + // Check path with sendMax and node with correct sendMax already set + Env env(*this, features(fs)); + env.fund(XRP(10000), alice, bob, gw); + env.trust(USD(1000), alice, bob); + env.trust(EUR(1000), alice, bob); + env(pay(gw, alice, EUR(100))); + auto const path = STPath({STPathElement( + STPathElement::typeAll, + EUR.account, + EUR.currency, + EUR.account)}); + test(env, USD, EUR.issue(), path, tesSUCCESS); + } + + { + // last step xrp from offer + Env env(*this, features(fs)); + env.fund(XRP(10000), alice, bob, gw); + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + + // alice -> USD/XRP -> bob + STPath path; + path.emplace_back(boost::none, USD.currency, USD.account.id()); + path.emplace_back(boost::none, xrpCurrency(), boost::none); + + auto r = toStrand( + *env.current(), + alice, + bob, + XRP, + USD.issue(), + path, + false, + env.app().logs().journal("Flow")); + BEAST_EXPECT(r.first == tesSUCCESS); + BEAST_EXPECT(equal(r.second, D{alice, gw, usdC}, B{USD.issue(), xrpIssue()}, XRPS{bob})); + } + } + + void + testRIPD1373(std::initializer_list fs) + { + using namespace jtx; + testcase("RIPD1373"); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + auto const EUR = gw["EUR"]; + + if (hasFeature(featureToStrandV2, fs)) + { + Env env(*this, features(fs)); + env.fund(XRP(10000), alice, bob, gw); + + env.trust(USD(1000), alice, bob); + env.trust(EUR(1000), alice, bob); + env.trust(bob["USD"](1000), alice, gw); + env.trust(bob["EUR"](1000), alice, gw); + + env(offer(bob, XRP(100), bob["USD"](100)), txflags(tfPassive)); + env(offer(gw, XRP(100), USD(100)), txflags(tfPassive)); + + env(offer(bob, bob["USD"](100), bob["EUR"](100)), + txflags(tfPassive)); + env(offer(gw, USD(100), EUR(100)), txflags(tfPassive)); + + Path const p = [&] { + Path result; + result.push_back(allpe(gw, bob["USD"])); + result.push_back(cpe(EUR.currency)); + return result; + }(); + + PathSet paths(p); + + env(pay(alice, alice, EUR(1)), + json(paths.json()), + sendmax(XRP(10)), + txflags(tfNoRippleDirect | tfPartialPayment), + ter(temBAD_PATH)); + } + + { + Env env(*this, features(fs)); + + env.fund(XRP(10000), alice, bob, carol, gw); + env.trust(USD(10000), alice, bob, carol); + + env(pay(gw, bob, USD(100))); + + env(offer(bob, XRP(100), USD(100)), txflags(tfPassive)); + env(offer(bob, USD(100), XRP(100)), txflags(tfPassive)); + + // payment path: XRP -> XRP/USD -> USD/XRP + env(pay(alice, carol, XRP(100)), + path(~USD, ~XRP), + txflags(tfNoRippleDirect), + ter(temBAD_SEND_XRP_PATHS)); + } + + { + Env env(*this, features(fs)); + + env.fund(XRP(10000), alice, bob, carol, gw); + env.trust(USD(10000), alice, bob, carol); + + env(pay(gw, bob, USD(100))); + + env(offer(bob, XRP(100), USD(100)), txflags(tfPassive)); + env(offer(bob, USD(100), XRP(100)), txflags(tfPassive)); + + // payment path: XRP -> XRP/USD -> USD/XRP + env(pay(alice, carol, XRP(100)), + path(~USD, ~XRP), + sendmax(XRP(200)), + txflags(tfNoRippleDirect), + ter(temBAD_SEND_XRP_MAX)); + } + } + + void + testLoop(std::initializer_list fs) + { + testcase("test loop"); + using namespace jtx; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + auto const EUR = gw["EUR"]; + auto const CNY = gw["CNY"]; + + { + Env env(*this, features(fs)); + + env.fund(XRP(10000), alice, bob, carol, gw); + env.trust(USD(10000), alice, bob, carol); + + env(pay(gw, bob, USD(100))); + env(pay(gw, alice, USD(100))); + + env(offer(bob, XRP(100), USD(100)), txflags(tfPassive)); + env(offer(bob, USD(100), XRP(100)), txflags(tfPassive)); + + auto const expectedResult = [&] { + if (hasFeature(featureFlow, fs) && + !hasFeature(featureToStrandV2, fs)) + return tesSUCCESS; + return temBAD_PATH_LOOP; + }(); + // payment path: USD -> USD/XRP -> XRP/USD + env(pay(alice, carol, USD(100)), + sendmax(USD(100)), + path(~XRP, ~USD), + txflags(tfNoRippleDirect), + ter(expectedResult)); + } + { + Env env(*this, features(fs)); + + env.fund(XRP(10000), alice, bob, carol, gw); + env.trust(USD(10000), alice, bob, carol); + env.trust(EUR(10000), alice, bob, carol); + env.trust(CNY(10000), alice, bob, carol); + + env(pay(gw, bob, USD(100))); + env(pay(gw, bob, EUR(100))); + env(pay(gw, bob, CNY(100))); + + env(offer(bob, XRP(100), USD(100)), txflags(tfPassive)); + env(offer(bob, USD(100), EUR(100)), txflags(tfPassive)); + env(offer(bob, EUR(100), CNY(100)), txflags(tfPassive)); + + // payment path: XRP->XRP/USD->USD/EUR->USD/CNY + env(pay(alice, carol, CNY(100)), + sendmax(XRP(100)), + path(~USD, ~EUR, ~USD, ~CNY), + txflags(tfNoRippleDirect), + ter(temBAD_PATH_LOOP)); + } + } + + void + run() override + { + testAllPairs(); + testToStrand({featureFlow}); + testToStrand({featureFlow, featureToStrandV2}); + testRIPD1373({}); + testRIPD1373({featureFlow, featureToStrandV2}); + testLoop({}); + testLoop({featureFlow}); + testLoop({featureFlow, featureToStrandV2}); + } +}; + +BEAST_DEFINE_TESTSUITE_MANUAL(PayStrand, app, ripple); + +} // test +} // ripple diff --git a/src/test/app/SetAuth_test.cpp b/src/test/app/SetAuth_test.cpp index 0eb67d1a8a..a669731550 100644 --- a/src/test/app/SetAuth_test.cpp +++ b/src/test/app/SetAuth_test.cpp @@ -46,13 +46,13 @@ struct SetAuth_test : public beast::unit_test::suite return jv; } - void testAuth() + void testAuth(std::initializer_list fs) { using namespace jtx; auto const gw = Account("gw"); auto const USD = gw["USD"]; { - Env env(*this); + Env env(*this, features(fs)); env.fund(XRP(100000), "alice", gw); env(fset(gw, asfRequireAuth)); env(auth(gw, "alice", "USD"), ter(tecNO_LINE_REDUNDANT)); @@ -75,7 +75,9 @@ struct SetAuth_test : public beast::unit_test::suite void run() override { - testAuth(); + testAuth({}); + testAuth({featureFlow}); + testAuth({featureFlow, featureToStrandV2}); } }; diff --git a/src/test/app/TrustAndBalance_test.cpp b/src/test/app/TrustAndBalance_test.cpp index 7d10715da7..11ea0d659f 100644 --- a/src/test/app/TrustAndBalance_test.cpp +++ b/src/test/app/TrustAndBalance_test.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -45,12 +46,12 @@ class TrustAndBalance_test : public beast::unit_test::suite }; void - testPayNonexistent () + testPayNonexistent (std::initializer_list fs) { testcase ("Payment to Nonexistent Account"); using namespace test::jtx; - Env env {*this}; + Env env {*this, features(fs)}; env (pay (env.master, "alice", XRP(1)), ter(tecNO_DST_INSUF_XRP)); env.close(); } @@ -161,12 +162,12 @@ class TrustAndBalance_test : public beast::unit_test::suite } void - testDirectRipple () + testDirectRipple (std::initializer_list fs) { testcase ("Direct Payment, Ripple"); using namespace test::jtx; - Env env {*this}; + Env env {*this, features(fs)}; Account alice {"alice"}; Account bob {"bob"}; @@ -202,14 +203,14 @@ class TrustAndBalance_test : public beast::unit_test::suite } void - testWithTransferFee (bool subscribe, bool with_rate) + testWithTransferFee (bool subscribe, bool with_rate, std::initializer_list fs) { testcase(std::string("Direct Payment: ") + (with_rate ? "With " : "Without ") + " Xfer Fee, " + (subscribe ? "With " : "Without ") + " Subscribe"); using namespace test::jtx; - Env env {*this}; + Env env {*this, features(fs)}; auto wsc = test::makeWSClient(env.app().config()); Account gw {"gateway"}; Account alice {"alice"}; @@ -282,12 +283,12 @@ class TrustAndBalance_test : public beast::unit_test::suite } void - testWithPath () + testWithPath (std::initializer_list fs) { testcase ("Payments With Paths and Fees"); using namespace test::jtx; - Env env {*this}; + Env env {*this, features(fs)}; Account gw {"gateway"}; Account alice {"alice"}; Account bob {"bob"}; @@ -330,12 +331,12 @@ class TrustAndBalance_test : public beast::unit_test::suite } void - testIndirect () + testIndirect (std::initializer_list fs) { testcase ("Indirect Payment"); using namespace test::jtx; - Env env {*this}; + Env env {*this, features(fs)}; Account gw {"gateway"}; Account alice {"alice"}; Account bob {"bob"}; @@ -371,13 +372,13 @@ class TrustAndBalance_test : public beast::unit_test::suite } void - testIndirectMultiPath (bool with_rate) + testIndirectMultiPath (bool with_rate, std::initializer_list fs) { testcase (std::string("Indirect Payment, Multi Path, ") + (with_rate ? "With " : "Without ") + " Xfer Fee, "); using namespace test::jtx; - Env env {*this}; + Env env {*this, features(fs)}; Account gw {"gateway"}; Account amazon {"amazon"}; Account alice {"alice"}; @@ -437,12 +438,12 @@ class TrustAndBalance_test : public beast::unit_test::suite } void - testInvoiceID () + testInvoiceID (std::initializer_list fs) { testcase ("Set Invoice ID on Payment"); using namespace test::jtx; - Env env {*this}; + Env env {*this, features(fs)}; Account alice {"alice"}; auto wsc = test::makeWSClient(env.app().config()); @@ -489,19 +490,25 @@ class TrustAndBalance_test : public beast::unit_test::suite public: void run () { - testPayNonexistent (); testTrustNonexistent (); testCreditLimit (); - testDirectRipple (); - testWithTransferFee (false, false); - testWithTransferFee (false, true); - testWithTransferFee (true, false); - testWithTransferFee (true, true); - testWithPath (); - testIndirect (); - testIndirectMultiPath (true); - testIndirectMultiPath (false); - testInvoiceID (); + + auto testWithFeatures = [this](std::initializer_list fs) { + testPayNonexistent(fs); + testDirectRipple(fs); + testWithTransferFee(false, false, fs); + testWithTransferFee(false, true, fs); + testWithTransferFee(true, false, fs); + testWithTransferFee(true, true, fs); + testWithPath(fs); + testIndirect(fs); + testIndirectMultiPath(true, fs); + testIndirectMultiPath(false, fs); + testInvoiceID(fs); + }; + testWithFeatures({}); + testWithFeatures({featureFlow}); + testWithFeatures({featureFlow, featureToStrandV2}); } }; diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index cad23d1e7c..f7db2bbaf5 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -73,6 +73,14 @@ features (uint256 const& key, Args const&... args) return {{key, args...}}; } +/** Activate features in the Env ctor */ +inline +auto +features (std::initializer_list keys) +{ + return keys; +} + //------------------------------------------------------------------------------ /** A transaction testing environment. */ @@ -125,6 +133,14 @@ private: app().config().features.insert(key); } + void + construct_arg ( + std::initializer_list list) + { + for(auto const& key : list) + app().config().features.insert(key); + } + public: Env() = delete; Env (Env const&) = delete; diff --git a/src/test/jtx/PathSet.h b/src/test/jtx/PathSet.h index c2ab7f2578..d879487f7e 100644 --- a/src/test/jtx/PathSet.h +++ b/src/test/jtx/PathSet.h @@ -66,6 +66,7 @@ public: } Path& push_back (Issue const& iss); Path& push_back (jtx::Account const& acc); + Path& push_back (STPathElement const& pe); Json::Value json () const; private: Path& addHelper (){return *this;}; @@ -73,6 +74,12 @@ public: Path& addHelper (First&& first, Rest&&... rest); }; +inline Path& Path::push_back (STPathElement const& pe) +{ + path.emplace_back (pe); + return *this; +} + inline Path& Path::push_back (Issue const& iss) { path.emplace_back (STPathElement::typeCurrency | STPathElement::typeIssuer, diff --git a/src/test/ledger/BookDirs_test.cpp b/src/test/ledger/BookDirs_test.cpp index e841401a91..add4a03da2 100644 --- a/src/test/ledger/BookDirs_test.cpp +++ b/src/test/ledger/BookDirs_test.cpp @@ -18,16 +18,17 @@ #include #include #include +#include namespace ripple { namespace test { struct BookDirs_test : public beast::unit_test::suite { - void test_bookdir() + void test_bookdir(std::initializer_list fs) { using namespace jtx; - Env env(*this); + Env env(*this, features(fs)); auto gw = Account("gw"); auto USD = gw["USD"]; env.fund(XRP(1000000), "alice", "bob", "gw"); @@ -93,7 +94,8 @@ struct BookDirs_test : public beast::unit_test::suite void run() override { - test_bookdir(); + test_bookdir({}); + test_bookdir({featureFlow, featureToStrandV2}); } }; diff --git a/src/test/ledger/PaymentSandbox_test.cpp b/src/test/ledger/PaymentSandbox_test.cpp index 35c3d383bf..52146a73dc 100644 --- a/src/test/ledger/PaymentSandbox_test.cpp +++ b/src/test/ledger/PaymentSandbox_test.cpp @@ -23,6 +23,7 @@ #include #include #include +#include namespace ripple { namespace test { @@ -54,12 +55,12 @@ class PaymentSandbox_test : public beast::unit_test::suite 2) New code: Path is dry because sender does not have any GW1 to spend until the end of the transaction. */ - void testSelfFunding () + void testSelfFunding (std::initializer_list fs) { testcase ("selfFunding"); using namespace jtx; - Env env (*this); + Env env (*this, features(fs)); Account const gw1 ("gw1"); Account const gw2 ("gw2"); Account const snd ("snd"); @@ -95,12 +96,12 @@ class PaymentSandbox_test : public beast::unit_test::suite env.require (balance ("rcv", USD_gw2 (2))); } - void testSubtractCredits () + void testSubtractCredits (std::initializer_list fs) { testcase ("subtractCredits"); using namespace jtx; - Env env (*this); + Env env (*this, features(fs)); Account const gw1 ("gw1"); Account const gw2 ("gw2"); Account const alice ("alice"); @@ -255,7 +256,7 @@ class PaymentSandbox_test : public beast::unit_test::suite } } - void testTinyBalance () + void testTinyBalance (std::initializer_list fs) { testcase ("Tiny balance"); @@ -265,7 +266,7 @@ class PaymentSandbox_test : public beast::unit_test::suite using namespace jtx; - Env env (*this); + Env env (*this, features(fs)); Account const gw ("gw"); Account const alice ("alice"); @@ -291,7 +292,8 @@ class PaymentSandbox_test : public beast::unit_test::suite BEAST_EXPECT(pv.balanceHook (alice, gw, hugeAmt) != tinyAmt); } } - void testReserve() + + void testReserve(std::initializer_list fs) { testcase ("Reserve"); using namespace jtx; @@ -310,7 +312,7 @@ class PaymentSandbox_test : public beast::unit_test::suite return env.current ()->fees ().accountReserve (count); }; - Env env (*this); + Env env (*this, features(fs)); Account const alice ("alice"); env.fund (reserve(env, 1), alice); @@ -335,10 +337,14 @@ class PaymentSandbox_test : public beast::unit_test::suite public: void run () { - testSelfFunding (); - testSubtractCredits (); - testTinyBalance (); - testReserve(); + auto testAll = [this](std::initializer_list fs) { + testSelfFunding(fs); + testSubtractCredits(fs); + testTinyBalance(fs); + testReserve(fs); + }; + testAll({}); + testAll({featureFlow, featureToStrandV2}); } }; diff --git a/src/test/rpc/GatewayBalances_test.cpp b/src/test/rpc/GatewayBalances_test.cpp index 8931715bcf..7eabddf1f3 100644 --- a/src/test/rpc/GatewayBalances_test.cpp +++ b/src/test/rpc/GatewayBalances_test.cpp @@ -16,6 +16,7 @@ //============================================================================== #include +#include #include #include #include @@ -29,11 +30,11 @@ class GatewayBalances_test : public beast::unit_test::suite public: void - testGWB() + testGWB(std::initializer_list fs) { using namespace std::chrono_literals; using namespace jtx; - Env env(*this); + Env env(*this, features(fs)); // Gateway account and assets Account const alice {"alice"}; @@ -152,7 +153,8 @@ public: void run() override { - testGWB(); + testGWB({}); + testGWB({featureFlow, featureToStrandV2}); } }; diff --git a/src/test/rpc/NoRipple_test.cpp b/src/test/rpc/NoRipple_test.cpp index afabd778eb..34a351ee35 100644 --- a/src/test/rpc/NoRipple_test.cpp +++ b/src/test/rpc/NoRipple_test.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include @@ -66,13 +67,12 @@ public: } } - void - testNegativeBalance() + void testNegativeBalance(std::initializer_list fs) { testcase("Set noripple on a line with negative balance"); using namespace jtx; - Env env(*this); + Env env(*this, features(fs)); auto const gw = Account("gateway"); auto const alice = Account("alice"); @@ -113,13 +113,12 @@ public: BEAST_EXPECT(!lines[0u].isMember(jss::no_ripple)); } - void - testPairwise() + void testPairwise(std::initializer_list fs) { testcase("pairwise NoRipple"); using namespace jtx; - Env env(*this); + Env env(*this, features(fs)); auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -151,13 +150,12 @@ public: env(pay(alice, carol, bob["USD"](50)), ter(tecPATH_DRY)); } - void - testDefaultRipple() + void testDefaultRipple(std::initializer_list fs) { testcase("Set default ripple on an account and check new trustlines"); using namespace jtx; - Env env(*this); + Env env(*this, features(fs)); auto const gw = Account("gateway"); auto const alice = Account("alice"); @@ -213,9 +211,15 @@ public: void run () { testSetAndClear(); - testNegativeBalance(); - testPairwise(); - testDefaultRipple(); + + auto withFeatsTests = [this](std::initializer_list fs) { + testNegativeBalance(fs); + testPairwise(fs); + testDefaultRipple(fs); + }; + withFeatsTests({}); + withFeatsTests({featureFlow}); + withFeatsTests({featureFlow, featureToStrandV2}); } }; diff --git a/src/test/unity/app_test_unity.cpp b/src/test/unity/app_test_unity.cpp index 8b12846ff2..72c9d4b0c3 100644 --- a/src/test/unity/app_test_unity.cpp +++ b/src/test/unity/app_test_unity.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include