#include #include #include #include #include #include #include #include #include #include // IWYU pragma: keep #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl::test { enum class TrustFlag { Freeze, Auth, Noripple }; /*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; case TrustFlag::Noripple: if (useHigh) return lsfHighNoRipple; return lsfLowNoRipple; } 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 } class ElementComboIter { enum class SB /*state bit*/ : std::uint16_t { Acc, Iss, Cur, RootAcc, RootIss, Xrp, SameAccIss, ExistingAcc, ExistingCur, ExistingIss, PrevAcc, PrevCur, PrevIss, Boundary, Last }; std::uint16_t state_ = 0; static_assert(safeCast(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; [[nodiscard]] bool has(SB s) const { return (state_ & (1 << safeCast(s))) != 0; } [[nodiscard]] bool hasAny(std::initializer_list sb) const { for (auto const s : sb) { if (has(s)) return true; } return false; } [[nodiscard]] 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) { } [[nodiscard]] bool valid() const { return (allowCompound_ || !(has(SB::Acc) && hasAny({SB::Cur, SB::Iss}))) && (!hasAny({SB::PrevAcc, SB::PrevCur, SB::PrevIss}) || (prev_ != nullptr)) && (!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 void emplaceInto( Col& col, AccFactory&& accF, IssFactory&& issF, CurrencyFactory&& currencyF, std::optional const& existingAcc, std::optional const& existingCur, std::optional const& existingIss) { assert(!has(SB::Last)); auto const acc = [&]() -> std::optional { if (!has(SB::Acc)) return std::nullopt; if (has(SB::RootAcc)) return xrpAccount(); if (has(SB::ExistingAcc) && existingAcc) return existingAcc; return accF().id(); }(); auto const iss = [&]() -> std::optional { if (!has(SB::Iss)) return std::nullopt; 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 = [&]() -> std::optional { if (!has(SB::Cur)) return std::nullopt; 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]; } xrpl::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; [[nodiscard]] 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; explicit 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, std::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 kBUF_SIZE = 32; char buf[kBUF_SIZE]; for (size_t id = 0; id < numAct; ++id) { snprintf(buf, kBUF_SIZE, "A%zu", id); accounts.emplace_back(buf); } for (size_t id = 0; id < numCur; ++id) { if (id < 10) { snprintf(buf, kBUF_SIZE, "CC%zu", id); } else if (id < 100) { snprintf(buf, kBUF_SIZE, "C%zu", id); } else { snprintf(buf, kBUF_SIZE, "%zu", id); } currencies.emplace_back(toCurrency(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, xrpl::Keylet const& k) { auto const sle = v.read(k); if (!sle) return STAmount{}; return (*sle)[sfBalance]; }; auto lineBalance = [](ReadView const& v, xrpl::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++); } xrpl::Currency getAvailCurrency() { return getCurrency(nextAvailCurrency++); } template void forEachElementPair( STAmount const& sendMax, STAmount const& deliver, std::vector const& prefix, std::vector const& suffix, std::optional const& existingAcc, std::optional const& existingCur, std::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 const og{*this}; outerResult = prefix; outer.emplaceInto( outerResult, accF, issF, currencyF, existingAcc, existingCur, existingIss); STPathElement const* prevInner = &outerResult.back(); ElementComboIter inner(prevInner); while (inner.next()) { StateGuard const ig{*this}; result = outerResult; inner.emplaceInto( 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 { void testToStrand(FeatureBitset features) { 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 = xrpl::Book; using XRPS = XRPEndpointStepInfo; AMMContext ammContext(alice, false); auto test = [&, this]( jtx::Env& env, Issue const& deliver, std::optional const& sendMaxIssue, STPath const& path, TER expTer, auto&&... expSteps) { auto [ter, strand] = toStrand( *env.current(), alice, bob, deliver, std::nullopt, sendMaxIssue, path, true, OfferCrossing::No, ammContext, std::nullopt, env.app().getJournal("Flow")); BEAST_EXPECT(ter == expTer); if (sizeof...(expSteps) != 0) BEAST_EXPECT(equal(strand, std::forward(expSteps)...)); }; { Env env(*this, features); 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 [ter, _] = toStrand( *env.current(), alice, alice, /*deliver*/ xrpIssue(), /*limitQuality*/ std::nullopt, /*sendMaxIssue*/ eur, path, true, OfferCrossing::No, ammContext, std::nullopt, env.app().getJournal("Flow")); (void)_; BEAST_EXPECT(isTesSuccess(ter)); } { STPath const path = STPath({ipe(usd), cpe(xrpCurrency())}); auto [ter, _] = toStrand( *env.current(), alice, alice, /*deliver*/ xrpIssue(), /*limitQuality*/ std::nullopt, /*sendMaxIssue*/ eur, path, true, OfferCrossing::No, ammContext, std::nullopt, env.app().getJournal("Flow")); (void)_; BEAST_EXPECT(isTesSuccess(ter)); } } { Env env(*this, features); env.fund(XRP(10000), alice, bob, carol, gw); test(env, usd, std::nullopt, STPath(), terNO_LINE); env.trust(usd(1000), alice, bob, carol); test(env, usd, std::nullopt, STPath(), tecPATH_DRY); env(pay(gw, alice, usd(100))); env(pay(gw, carol, usd(100))); // Insert implied account test( env, usd, std::nullopt, STPath(), tesSUCCESS, D{alice, gw, usdC}, D{gw, bob, usdC}); env.trust(eur(1000), alice, bob); // Insert implied offer test( env, eur, usd, STPath(), tesSUCCESS, D{alice, gw, usdC}, B{usd, eur, std::nullopt}, D{gw, bob, eurC}); // Path with explicit offer test( env, eur, usd, STPath({ipe(eur)}), tesSUCCESS, D{alice, gw, usdC}, B{usd, eur, std::nullopt}, D{gw, bob, eurC}); // Path with offer that changes issuer only env.trust(carol["USD"](1000), bob); test( env, carol["USD"], usd, STPath({iape(carol)}), tesSUCCESS, D{alice, gw, usdC}, B{usd, carol["USD"], std::nullopt}, D{carol, bob, usdC}); // Path with XRP src currency test( env, usd, xrpIssue(), STPath({ipe(usd)}), tesSUCCESS, XRPS{alice}, B{XRP, usd, std::nullopt}, D{gw, bob, usdC}); // Path with XRP dst currency. test( env, xrpIssue(), usd, STPath({STPathElement{ STPathElement::TypeCurrency, xrpAccount(), xrpCurrency(), xrpAccount()}}), tesSUCCESS, D{alice, gw, usdC}, B{usd, XRP, std::nullopt}, XRPS{bob}); // Path with XRP cross currency bridged payment test( env, eur, usd, STPath({cpe(xrpCurrency())}), tesSUCCESS, D{alice, gw, usdC}, B{usd, XRP, std::nullopt}, B{XRP, eur, std::nullopt}, D{gw, bob, eurC}); // XRP -> XRP transaction can't include a path test(env, XRP, std::nullopt, STPath({ape(carol)}), temBAD_PATH); { // The root account can't be the src or dst auto flowJournal = env.app().getJournal("Flow"); { // The root account can't be the dst auto r = toStrand( *env.current(), alice, xrpAccount(), XRP, std::nullopt, usd, STPath(), true, OfferCrossing::No, ammContext, std::nullopt, flowJournal); BEAST_EXPECT(r.first == temBAD_PATH); } { // The root account can't be the src auto r = toStrand( *env.current(), xrpAccount(), alice, XRP, std::nullopt, std::nullopt, STPath(), true, OfferCrossing::No, ammContext, std::nullopt, flowJournal); BEAST_EXPECT(r.first == temBAD_PATH); } { // The root account can't be the src. auto r = toStrand( *env.current(), noAccount(), bob, usd, std::nullopt, std::nullopt, STPath(), true, OfferCrossing::No, ammContext, std::nullopt, flowJournal); BEAST_EXPECT(r.first == temBAD_PATH); } } // Create an offer with the same in/out issue test(env, eur, usd, STPath({ipe(usd), ipe(eur)}), temBAD_PATH); // Path element with type zero test( env, usd, std::nullopt, 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, std::nullopt, STPath({ape(gw), ape(carol)}), temBAD_PATH_LOOP); // The same offer can't appear more than once on a path test(env, eur, usd, 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); 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); env.fund(XRP(10000), alice, bob, noripple(gw)); env.trust(usd(1000), alice, bob); env(pay(gw, alice, usd(100))); test(env, usd, std::nullopt, STPath(), terNO_RIPPLE); } { // check global freeze Env env(*this, features); 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, std::nullopt, STPath(), tesSUCCESS); env(fclear(alice, asfGlobalFreeze)); test(env, usd, std::nullopt, STPath(), tesSUCCESS); // Account can not issue funds env(fset(gw, asfGlobalFreeze)); test(env, usd, std::nullopt, STPath(), terNO_LINE); env(fclear(gw, asfGlobalFreeze)); test(env, usd, std::nullopt, STPath(), tesSUCCESS); // Account can not receive funds env(fset(bob, asfGlobalFreeze)); test(env, usd, std::nullopt, STPath(), terNO_LINE); env(fclear(bob, asfGlobalFreeze)); test(env, usd, std::nullopt, STPath(), tesSUCCESS); } { // Freeze between gw and alice Env env(*this, features); env.fund(XRP(10000), alice, bob, gw); env.trust(usd(1000), alice, bob); env(pay(gw, alice, usd(100))); test(env, usd, std::nullopt, STPath(), tesSUCCESS); env(trust(gw, alice["USD"](0), tfSetFreeze)); BEAST_EXPECT(getTrustFlag(env, gw, alice, usdC, TrustFlag::Freeze)); test(env, usd, std::nullopt, STPath(), terNO_LINE); } { // check no auth // An account may require authorization to receive IOUs from an // issuer Env env(*this, features); 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, std::nullopt, STPath(), terNO_AUTH); // Check pure issue redeem still works auto [ter, strand] = toStrand( *env.current(), alice, gw, usd, std::nullopt, std::nullopt, STPath(), true, OfferCrossing::No, ammContext, std::nullopt, env.app().getJournal("Flow")); BEAST_EXPECT(isTesSuccess(ter)); BEAST_EXPECT(equal(strand, D{alice, gw, usdC})); } { // last step xrp from offer Env env(*this, features); 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.emplaceBack(std::nullopt, xrpCurrency(), std::nullopt); auto [ter, strand] = toStrand( *env.current(), alice, bob, XRP, std::nullopt, usd, path, false, OfferCrossing::No, ammContext, std::nullopt, env.app().getJournal("Flow")); BEAST_EXPECT(isTesSuccess(ter)); BEAST_EXPECT( equal(strand, D{alice, gw, usdC}, B{usd, xrpIssue(), std::nullopt}, XRPS{bob})); } } void testRIPD1373(FeatureBitset features) { 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"]; { Env env(*this, features); 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)); TestPath const p = [&] { TestPath result; result.pushBack(allPathElements(gw, bob["USD"])); result.pushBack(cpe(eur.currency)); return result; }(); PathSet const paths(p); env(pay(alice, alice, eur(1)), Json(paths.json()), Sendmax(XRP(10)), Txflags(tfNoRippleDirect | tfPartialPayment), Ter(temBAD_PATH)); } { Env env(*this, features); 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); 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(FeatureBitset features) { 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); 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)); // payment path: USD -> USD/XRP -> XRP/USD env(pay(alice, carol, usd(100)), Sendmax(usd(100)), Path(~XRP, ~usd), Txflags(tfNoRippleDirect), Ter(temBAD_PATH_LOOP)); } { Env env(*this, features); 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 testNoAccount(FeatureBitset features) { testcase("test no account"); using namespace jtx; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const gw = Account("gw"); auto const usd = gw["USD"]; Env env(*this, features); env.fund(XRP(10000), alice, bob, gw); STAmount const sendMax{usd, 100, 1}; STAmount const noAccountAmount{Issue{usd.currency, noAccount()}, 100, 1}; STAmount const deliver; AccountID const srcAcc = alice.id(); AccountID const dstAcc = bob.id(); STPathSet const pathSet; ::xrpl::path::RippleCalc::Input inputs; inputs.defaultPathsAllowed = true; try { PaymentSandbox sb{env.current().get(), TapNone}; { auto const r = ::xrpl::path::RippleCalc::rippleCalculate( sb, sendMax, deliver, dstAcc, noAccount(), pathSet, std::nullopt, env.app(), &inputs); BEAST_EXPECT(r.result() == temBAD_PATH); } { auto const r = ::xrpl::path::RippleCalc::rippleCalculate( sb, sendMax, deliver, noAccount(), srcAcc, pathSet, std::nullopt, env.app(), &inputs); BEAST_EXPECT(r.result() == temBAD_PATH); } { auto const r = ::xrpl::path::RippleCalc::rippleCalculate( sb, noAccountAmount, deliver, dstAcc, srcAcc, pathSet, std::nullopt, env.app(), &inputs); BEAST_EXPECT(r.result() == temBAD_PATH); } { auto const r = ::xrpl::path::RippleCalc::rippleCalculate( sb, sendMax, noAccountAmount, dstAcc, srcAcc, pathSet, std::nullopt, env.app(), &inputs); BEAST_EXPECT(r.result() == temBAD_PATH); } } catch (...) { this->fail(); } } void run() override { using namespace jtx; auto const sa = testableAmendments(); testToStrand(sa - featurePermissionedDEX); testToStrand(sa); testRIPD1373(sa - featurePermissionedDEX); testRIPD1373(sa); testLoop(sa - featurePermissionedDEX); testLoop(sa); testNoAccount(sa); } }; BEAST_DEFINE_TESTSUITE(PayStrand, app, xrpl); } // namespace xrpl::test