#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl { namespace test { //------------------------------------------------------------------------------ Json::Value rpf(jtx::Account const& src, jtx::Account const& dst, std::uint32_t num_src) { Json::Value jv = Json::objectValue; jv[jss::command] = "ripple_path_find"; jv[jss::source_account] = toBase58(src); if (num_src > 0) { auto& sc = (jv[jss::source_currencies] = Json::arrayValue); Json::Value j = Json::objectValue; while (num_src--) { j[jss::currency] = std::to_string(num_src + 100); sc.append(j); } } auto const d = toBase58(dst); jv[jss::destination_account] = d; Json::Value& j = (jv[jss::destination_amount] = Json::objectValue); j[jss::currency] = "USD"; j[jss::value] = "0.01"; j[jss::issuer] = d; return jv; } //------------------------------------------------------------------------------ class Path_test : public beast::unit_test::suite { jtx::Env pathTestEnv() { // These tests were originally written with search parameters that are // different from the current defaults. This function creates an env // with the search parameters that the tests were written for. using namespace jtx; return Env(*this, envconfig([](std::unique_ptr cfg) { cfg->PATH_SEARCH_OLD = 7; cfg->PATH_SEARCH = 7; cfg->PATH_SEARCH_MAX = 10; return cfg; })); } public: class gate { private: std::condition_variable cv_; std::mutex mutex_; bool signaled_ = false; public: // Thread safe, blocks until signaled or period expires. // Returns `true` if signaled. template bool wait_for(std::chrono::duration const& rel_time) { std::unique_lock lk(mutex_); auto b = cv_.wait_for(lk, rel_time, [this] { return signaled_; }); signaled_ = false; return b; } void signal() { std::lock_guard lk(mutex_); signaled_ = true; cv_.notify_all(); } }; auto find_paths_request( jtx::Env& env, jtx::Account const& src, jtx::Account const& dst, STAmount const& saDstAmount, std::optional const& saSendMax = std::nullopt, std::optional const& saSrcCurrency = std::nullopt, std::optional const& domain = std::nullopt) { using namespace jtx; auto& app = env.app(); Resource::Charge loadType = Resource::feeReferenceRPC; Resource::Consumer c; RPC::JsonContext context{ {env.journal, app, loadType, app.getOPs(), app.getLedgerMaster(), c, Role::USER, {}, RPC::apiVersionIfUnspecified}, {}, {}}; Json::Value params = Json::objectValue; params[jss::command] = "ripple_path_find"; params[jss::source_account] = toBase58(src); params[jss::destination_account] = toBase58(dst); params[jss::destination_amount] = saDstAmount.getJson(JsonOptions::none); if (saSendMax) params[jss::send_max] = saSendMax->getJson(JsonOptions::none); if (saSrcCurrency) { auto& sc = params[jss::source_currencies] = Json::arrayValue; Json::Value j = Json::objectValue; j[jss::currency] = to_string(saSrcCurrency.value()); sc.append(j); } if (domain) params[jss::domain] = to_string(*domain); Json::Value result; gate g; app.getJobQueue().postCoroTask(jtCLIENT, "RPC-Client", [&](auto) -> CoroTask { context.params = std::move(params); RPC::doCommand(context, result); g.signal(); co_return; }); using namespace std::chrono_literals; BEAST_EXPECT(g.wait_for(5s)); BEAST_EXPECT(!result.isMember(jss::error)); return result; } std::tuple find_paths( jtx::Env& env, jtx::Account const& src, jtx::Account const& dst, STAmount const& saDstAmount, std::optional const& saSendMax = std::nullopt, std::optional const& saSrcCurrency = std::nullopt, std::optional const& domain = std::nullopt) { Json::Value result = find_paths_request(env, src, dst, saDstAmount, saSendMax, saSrcCurrency, domain); BEAST_EXPECT(!result.isMember(jss::error)); STAmount da; if (result.isMember(jss::destination_amount)) da = amountFromJson(sfGeneric, result[jss::destination_amount]); STAmount sa; STPathSet paths; if (result.isMember(jss::alternatives)) { auto const& alts = result[jss::alternatives]; if (alts.size() > 0) { auto const& path = alts[0u]; if (path.isMember(jss::source_amount)) sa = amountFromJson(sfGeneric, path[jss::source_amount]); if (path.isMember(jss::destination_amount)) da = amountFromJson(sfGeneric, path[jss::destination_amount]); if (path.isMember(jss::paths_computed)) { Json::Value p; p["Paths"] = path[jss::paths_computed]; STParsedJSONObject po("generic", p); paths = po.object->getFieldPathSet(sfPaths); } } } return std::make_tuple(std::move(paths), std::move(sa), std::move(da)); } void source_currencies_limit() { testcase("source currency limits"); using namespace std::chrono_literals; using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); env.fund(XRP(10000), "alice", "bob", gw); env.close(); env.trust(gw["USD"](100), "alice", "bob"); env.close(); auto& app = env.app(); Resource::Charge loadType = Resource::feeReferenceRPC; Resource::Consumer c; RPC::JsonContext context{ {env.journal, app, loadType, app.getOPs(), app.getLedgerMaster(), c, Role::USER, {}, RPC::apiVersionIfUnspecified}, {}, {}}; Json::Value result; gate g; // Test RPC::Tuning::max_src_cur source currencies. app.getJobQueue().postCoroTask(jtCLIENT, "RPC-Client", [&](auto) -> CoroTask { context.params = rpf(Account("alice"), Account("bob"), RPC::Tuning::max_src_cur); RPC::doCommand(context, result); g.signal(); co_return; }); BEAST_EXPECT(g.wait_for(5s)); BEAST_EXPECT(!result.isMember(jss::error)); // Test more than RPC::Tuning::max_src_cur source currencies. app.getJobQueue().postCoroTask(jtCLIENT, "RPC-Client", [&](auto) -> CoroTask { context.params = rpf(Account("alice"), Account("bob"), RPC::Tuning::max_src_cur + 1); RPC::doCommand(context, result); g.signal(); co_return; }); BEAST_EXPECT(g.wait_for(5s)); BEAST_EXPECT(result.isMember(jss::error)); // Test RPC::Tuning::max_auto_src_cur source currencies. for (auto i = 0; i < (RPC::Tuning::max_auto_src_cur - 1); ++i) env.trust(Account("alice")[std::to_string(i + 100)](100), "bob"); app.getJobQueue().postCoroTask(jtCLIENT, "RPC-Client", [&](auto) -> CoroTask { context.params = rpf(Account("alice"), Account("bob"), 0); RPC::doCommand(context, result); g.signal(); co_return; }); BEAST_EXPECT(g.wait_for(5s)); BEAST_EXPECT(!result.isMember(jss::error)); // Test more than RPC::Tuning::max_auto_src_cur source currencies. env.trust(Account("alice")["AUD"](100), "bob"); app.getJobQueue().postCoroTask(jtCLIENT, "RPC-Client", [&](auto) -> CoroTask { context.params = rpf(Account("alice"), Account("bob"), 0); RPC::doCommand(context, result); g.signal(); co_return; }); BEAST_EXPECT(g.wait_for(5s)); BEAST_EXPECT(result.isMember(jss::error)); } void no_direct_path_no_intermediary_no_alternatives() { testcase("no direct path no intermediary no alternatives"); using namespace jtx; Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob"); env.close(); auto const result = find_paths(env, "alice", "bob", Account("bob")["USD"](5)); BEAST_EXPECT(std::get<0>(result).empty()); } void direct_path_no_intermediary() { testcase("direct path no intermediary"); using namespace jtx; Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob"); env.close(); env.trust(Account("alice")["USD"](700), "bob"); STPathSet st; STAmount sa; std::tie(st, sa, std::ignore) = find_paths(env, "alice", "bob", Account("bob")["USD"](5)); BEAST_EXPECT(st.empty()); BEAST_EXPECT(equal(sa, Account("alice")["USD"](5))); } void payment_auto_path_find() { testcase("payment auto path find"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); auto const USD = gw["USD"]; env.fund(XRP(10000), "alice", "bob", gw); env.close(); env.trust(USD(600), "alice"); env.trust(USD(700), "bob"); env(pay(gw, "alice", USD(70))); env(pay("alice", "bob", USD(24))); env.require(balance("alice", USD(46))); env.require(balance(gw, Account("alice")["USD"](-46))); env.require(balance("bob", USD(24))); env.require(balance(gw, Account("bob")["USD"](-24))); } void path_find(bool const domainEnabled) { testcase(std::string("path find") + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); auto const USD = gw["USD"]; env.fund(XRP(10000), "alice", "bob", gw); env.close(); env.trust(USD(600), "alice"); env.trust(USD(700), "bob"); env(pay(gw, "alice", USD(70))); env(pay(gw, "bob", USD(50))); std::optional domainID; if (domainEnabled) domainID = setupDomain(env, {"alice", "bob", gw}); STPathSet st; STAmount sa; std::tie(st, sa, std::ignore) = find_paths( env, "alice", "bob", Account("bob")["USD"](5), std::nullopt, std::nullopt, domainID); BEAST_EXPECT(same(st, stpath("gateway"))); BEAST_EXPECT(equal(sa, Account("alice")["USD"](5))); } void xrp_to_xrp(bool const domainEnabled) { using namespace jtx; testcase(std::string("XRP to XRP") + (domainEnabled ? " w/ " : " w/o ") + "domain"); Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob"); env.close(); std::optional domainID; if (domainEnabled) domainID = setupDomain(env, {"alice", "bob"}); auto const result = find_paths(env, "alice", "bob", XRP(5), std::nullopt, std::nullopt, domainID); BEAST_EXPECT(std::get<0>(result).empty()); } void path_find_consume_all(bool const domainEnabled) { testcase( std::string("path find consume all") + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; { Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob", "carol", "dan", "edward"); env.close(); env.trust(Account("alice")["USD"](10), "bob"); env.trust(Account("bob")["USD"](10), "carol"); env.trust(Account("carol")["USD"](10), "edward"); env.trust(Account("alice")["USD"](100), "dan"); env.trust(Account("dan")["USD"](100), "edward"); std::optional domainID; if (domainEnabled) domainID = setupDomain(env, {"alice", "bob", "carol", "dan", "edward"}); STPathSet st; STAmount sa; STAmount da; std::tie(st, sa, da) = find_paths( env, "alice", "edward", Account("edward")["USD"](-1), std::nullopt, std::nullopt, domainID); BEAST_EXPECT(same(st, stpath("dan"), stpath("bob", "carol"))); BEAST_EXPECT(equal(sa, Account("alice")["USD"](110))); BEAST_EXPECT(equal(da, Account("edward")["USD"](110))); } { Env env = pathTestEnv(); auto const gw = Account("gateway"); auto const USD = gw["USD"]; env.fund(XRP(10000), "alice", "bob", "carol", gw); env.close(); env.trust(USD(100), "bob", "carol"); env.close(); env(pay(gw, "carol", USD(100))); env.close(); std::optional domainID; if (domainEnabled) { domainID = setupDomain(env, {"alice", "bob", "carol", "gateway"}); env(offer("carol", XRP(100), USD(100)), domain(*domainID)); } else { env(offer("carol", XRP(100), USD(100))); } env.close(); STPathSet st; STAmount sa; STAmount da; std::tie(st, sa, da) = find_paths( env, "alice", "bob", Account("bob")["AUD"](-1), std::optional(XRP(1000000)), std::nullopt, domainID); BEAST_EXPECT(st.empty()); std::tie(st, sa, da) = find_paths( env, "alice", "bob", Account("bob")["USD"](-1), std::optional(XRP(1000000)), std::nullopt, domainID); BEAST_EXPECT(sa == XRP(100)); BEAST_EXPECT(equal(da, Account("bob")["USD"](100))); // if domain is used, finding path in the open offerbook will return // empty result if (domainEnabled) { std::tie(st, sa, da) = find_paths( env, "alice", "bob", Account("bob")["USD"](-1), std::optional(XRP(1000000)), std::nullopt, std::nullopt); // not specifying a domain BEAST_EXPECT(st.empty()); } } } void alternative_path_consume_both(bool const domainEnabled) { testcase( std::string("alternative path consume both") + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); auto const USD = gw["USD"]; auto const gw2 = Account("gateway2"); auto const gw2_USD = gw2["USD"]; env.fund(XRP(10000), "alice", "bob", gw, gw2); env.close(); env.trust(USD(600), "alice"); env.trust(gw2_USD(800), "alice"); env.trust(USD(700), "bob"); env.trust(gw2_USD(900), "bob"); std::optional domainID; if (domainEnabled) { domainID = setupDomain(env, {"alice", "bob", "gateway", "gateway2"}); env(pay(gw, "alice", USD(70)), domain(*domainID)); env(pay(gw2, "alice", gw2_USD(70)), domain(*domainID)); env(pay("alice", "bob", Account("bob")["USD"](140)), paths(Account("alice")["USD"]), domain(*domainID)); } else { env(pay(gw, "alice", USD(70))); env(pay(gw2, "alice", gw2_USD(70))); env(pay("alice", "bob", Account("bob")["USD"](140)), paths(Account("alice")["USD"])); } env.require(balance("alice", USD(0))); env.require(balance("alice", gw2_USD(0))); env.require(balance("bob", USD(70))); env.require(balance("bob", gw2_USD(70))); env.require(balance(gw, Account("alice")["USD"](0))); env.require(balance(gw, Account("bob")["USD"](-70))); env.require(balance(gw2, Account("alice")["USD"](0))); env.require(balance(gw2, Account("bob")["USD"](-70))); } void alternative_paths_consume_best_transfer(bool const domainEnabled) { testcase( std::string("alternative paths consume best transfer") + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); auto const USD = gw["USD"]; auto const gw2 = Account("gateway2"); auto const gw2_USD = gw2["USD"]; env.fund(XRP(10000), "alice", "bob", gw, gw2); env.close(); env(rate(gw2, 1.1)); env.trust(USD(600), "alice"); env.trust(gw2_USD(800), "alice"); env.trust(USD(700), "bob"); env.trust(gw2_USD(900), "bob"); std::optional domainID; if (domainEnabled) { domainID = setupDomain(env, {"alice", "bob", "gateway", "gateway2"}); env(pay(gw, "alice", USD(70)), domain(*domainID)); env(pay(gw2, "alice", gw2_USD(70)), domain(*domainID)); env(pay("alice", "bob", USD(70)), domain(*domainID)); } else { env(pay(gw, "alice", USD(70))); env(pay(gw2, "alice", gw2_USD(70))); env(pay("alice", "bob", USD(70))); } env.require(balance("alice", USD(0))); env.require(balance("alice", gw2_USD(70))); env.require(balance("bob", USD(70))); env.require(balance("bob", gw2_USD(0))); env.require(balance(gw, Account("alice")["USD"](0))); env.require(balance(gw, Account("bob")["USD"](-70))); env.require(balance(gw2, Account("alice")["USD"](-70))); env.require(balance(gw2, Account("bob")["USD"](0))); } void alternative_paths_consume_best_transfer_first() { testcase("alternative paths - consume best transfer first"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); auto const USD = gw["USD"]; auto const gw2 = Account("gateway2"); auto const gw2_USD = gw2["USD"]; env.fund(XRP(10000), "alice", "bob", gw, gw2); env.close(); env(rate(gw2, 1.1)); env.trust(USD(600), "alice"); env.trust(gw2_USD(800), "alice"); env.trust(USD(700), "bob"); env.trust(gw2_USD(900), "bob"); env(pay(gw, "alice", USD(70))); env(pay(gw2, "alice", gw2_USD(70))); env(pay("alice", "bob", Account("bob")["USD"](77)), sendmax(Account("alice")["USD"](100)), paths(Account("alice")["USD"])); env.require(balance("alice", USD(0))); env.require(balance("alice", gw2_USD(62.3))); env.require(balance("bob", USD(70))); env.require(balance("bob", gw2_USD(7))); env.require(balance(gw, Account("alice")["USD"](0))); env.require(balance(gw, Account("bob")["USD"](-70))); env.require(balance(gw2, Account("alice")["USD"](-62.3))); env.require(balance(gw2, Account("bob")["USD"](-7))); } void alternative_paths_limit_returned_paths_to_best_quality(bool const domainEnabled) { testcase( std::string("alternative paths - limit returned paths to best quality") + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); auto const USD = gw["USD"]; auto const gw2 = Account("gateway2"); auto const gw2_USD = gw2["USD"]; env.fund(XRP(10000), "alice", "bob", "carol", "dan", gw, gw2); env.close(); env(rate("carol", 1.1)); env.trust(Account("carol")["USD"](800), "alice", "bob"); env.trust(Account("dan")["USD"](800), "alice", "bob"); env.trust(USD(800), "alice", "bob"); env.trust(gw2_USD(800), "alice", "bob"); env.trust(Account("alice")["USD"](800), "dan"); env.trust(Account("bob")["USD"](800), "dan"); env.close(); env(pay(gw2, "alice", gw2_USD(100))); env.close(); env(pay("carol", "alice", Account("carol")["USD"](100))); env.close(); env(pay(gw, "alice", USD(100))); env.close(); std::optional domainID; if (domainEnabled) { domainID = setupDomain(env, {"alice", "bob", "carol", "dan", gw, gw2}); } STPathSet st; STAmount sa; std::tie(st, sa, std::ignore) = find_paths( env, "alice", "bob", Account("bob")["USD"](5), std::nullopt, std::nullopt, domainID); BEAST_EXPECT( same(st, stpath("gateway"), stpath("gateway2"), stpath("dan"), stpath("carol"))); BEAST_EXPECT(equal(sa, Account("alice")["USD"](5))); } void issues_path_negative_issue(bool const domainEnabled) { testcase( std::string("path negative: Issue #5") + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob", "carol", "dan"); env.close(); env.trust(Account("bob")["USD"](100), "alice", "carol", "dan"); env.trust(Account("alice")["USD"](100), "dan"); env.trust(Account("carol")["USD"](100), "dan"); env(pay("bob", "carol", Account("bob")["USD"](75))); env.require(balance("bob", Account("carol")["USD"](-75))); env.require(balance("carol", Account("bob")["USD"](75))); env.close(); std::optional domainID; if (domainEnabled) { domainID = setupDomain(env, {"alice", "bob", "carol", "dan"}); } auto result = find_paths( env, "alice", "bob", Account("bob")["USD"](25), std::nullopt, std::nullopt, domainID); BEAST_EXPECT(std::get<0>(result).empty()); env(pay("alice", "bob", Account("alice")["USD"](25)), ter(tecPATH_DRY)); env.close(); result = find_paths( env, "alice", "bob", Account("alice")["USD"](25), std::nullopt, std::nullopt, domainID); BEAST_EXPECT(std::get<0>(result).empty()); env.require(balance("alice", Account("bob")["USD"](0))); env.require(balance("alice", Account("dan")["USD"](0))); env.require(balance("bob", Account("alice")["USD"](0))); env.require(balance("bob", Account("carol")["USD"](-75))); env.require(balance("bob", Account("dan")["USD"](0))); env.require(balance("carol", Account("bob")["USD"](75))); env.require(balance("carol", Account("dan")["USD"](0))); env.require(balance("dan", Account("alice")["USD"](0))); env.require(balance("dan", Account("bob")["USD"](0))); env.require(balance("dan", Account("carol")["USD"](0))); } // alice -- limit 40 --> bob // alice --> carol --> dan --> bob // Balance of 100 USD Bob - Balance of 37 USD -> Rod void issues_path_negative_ripple_client_issue_23_smaller() { testcase("path negative: ripple-client issue #23: smaller"); using namespace jtx; Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob", "carol", "dan"); env.close(); env.trust(Account("alice")["USD"](40), "bob"); env.trust(Account("dan")["USD"](20), "bob"); env.trust(Account("alice")["USD"](20), "carol"); env.trust(Account("carol")["USD"](20), "dan"); env(pay("alice", "bob", Account("bob")["USD"](55)), paths(Account("alice")["USD"])); env.require(balance("bob", Account("alice")["USD"](40))); env.require(balance("bob", Account("dan")["USD"](15))); } // alice -120 USD-> edward -25 USD-> bob // alice -25 USD-> carol -75 USD -> dan -100 USD-> bob void issues_path_negative_ripple_client_issue_23_larger() { testcase("path negative: ripple-client issue #23: larger"); using namespace jtx; Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob", "carol", "dan", "edward"); env.close(); env.trust(Account("alice")["USD"](120), "edward"); env.trust(Account("edward")["USD"](25), "bob"); env.trust(Account("dan")["USD"](100), "bob"); env.trust(Account("alice")["USD"](25), "carol"); env.trust(Account("carol")["USD"](75), "dan"); env(pay("alice", "bob", Account("bob")["USD"](50)), paths(Account("alice")["USD"])); env.require(balance("alice", Account("edward")["USD"](-25))); env.require(balance("alice", Account("carol")["USD"](-25))); env.require(balance("bob", Account("edward")["USD"](25))); env.require(balance("bob", Account("dan")["USD"](25))); env.require(balance("carol", Account("alice")["USD"](25))); env.require(balance("carol", Account("dan")["USD"](-25))); env.require(balance("dan", Account("carol")["USD"](25))); env.require(balance("dan", Account("bob")["USD"](-25))); } // carol holds gateway AUD, sells gateway AUD for XRP // bob will hold gateway AUD // alice pays bob gateway AUD using XRP void via_offers_via_gateway(bool const domainEnabled) { testcase(std::string("via gateway") + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); auto const AUD = gw["AUD"]; env.fund(XRP(10000), "alice", "bob", "carol", gw); env.close(); env(rate(gw, 1.1)); env.close(); env.trust(AUD(100), "bob", "carol"); env.close(); env(pay(gw, "carol", AUD(50))); env.close(); std::optional domainID; if (domainEnabled) { domainID = setupDomain(env, {"alice", "bob", "carol", gw}); env(offer("carol", XRP(50), AUD(50)), domain(*domainID)); env.close(); env(pay("alice", "bob", AUD(10)), sendmax(XRP(100)), paths(XRP), domain(*domainID)); env.close(); } else { env(offer("carol", XRP(50), AUD(50))); env.close(); env(pay("alice", "bob", AUD(10)), sendmax(XRP(100)), paths(XRP)); env.close(); } env.require(balance("bob", AUD(10))); env.require(balance("carol", AUD(39))); auto const result = find_paths( env, "alice", "bob", Account("bob")["USD"](25), std::nullopt, std::nullopt, domainID); BEAST_EXPECT(std::get<0>(result).empty()); } void indirect_paths_path_find() { testcase("path find"); using namespace jtx; Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob", "carol"); env.close(); env.trust(Account("alice")["USD"](1000), "bob"); env.trust(Account("bob")["USD"](1000), "carol"); STPathSet st; STAmount sa; std::tie(st, sa, std::ignore) = find_paths(env, "alice", "carol", Account("carol")["USD"](5)); BEAST_EXPECT(same(st, stpath("bob"))); BEAST_EXPECT(equal(sa, Account("alice")["USD"](5))); } void quality_paths_quality_set_and_test() { testcase("quality set and test"); using namespace jtx; Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob"); env.close(); env(trust("bob", Account("alice")["USD"](1000)), json("{\"" + sfQualityIn.fieldName + "\": 2000}"), json("{\"" + sfQualityOut.fieldName + "\": 1400000000}")); Json::Value jv; Json::Reader().parse( R"({ "Balance" : { "currency" : "USD", "issuer" : "rrrrrrrrrrrrrrrrrrrrBZbvji", "value" : "0" }, "Flags" : 131072, "HighLimit" : { "currency" : "USD", "issuer" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", "value" : "1000" }, "HighNode" : "0", "HighQualityIn" : 2000, "HighQualityOut" : 1400000000, "LedgerEntryType" : "RippleState", "LowLimit" : { "currency" : "USD", "issuer" : "rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn", "value" : "0" }, "LowNode" : "0" })", jv); auto const jv_l = env.le(keylet::line(Account("bob").id(), Account("alice")["USD"].issue())) ->getJson(JsonOptions::none); for (auto it = jv.begin(); it != jv.end(); ++it) BEAST_EXPECT(*it == jv_l[it.memberName()]); } void trust_auto_clear_trust_normal_clear() { testcase("trust normal clear"); using namespace jtx; Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob"); env.close(); env.trust(Account("bob")["USD"](1000), "alice"); env.trust(Account("alice")["USD"](1000), "bob"); Json::Value jv; Json::Reader().parse( R"({ "Balance" : { "currency" : "USD", "issuer" : "rrrrrrrrrrrrrrrrrrrrBZbvji", "value" : "0" }, "Flags" : 196608, "HighLimit" : { "currency" : "USD", "issuer" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", "value" : "1000" }, "HighNode" : "0", "LedgerEntryType" : "RippleState", "LowLimit" : { "currency" : "USD", "issuer" : "rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn", "value" : "1000" }, "LowNode" : "0" })", jv); auto const jv_l = env.le(keylet::line(Account("bob").id(), Account("alice")["USD"].issue())) ->getJson(JsonOptions::none); for (auto it = jv.begin(); it != jv.end(); ++it) BEAST_EXPECT(*it == jv_l[it.memberName()]); env.trust(Account("bob")["USD"](0), "alice"); env.trust(Account("alice")["USD"](0), "bob"); BEAST_EXPECT( env.le(keylet::line(Account("bob").id(), Account("alice")["USD"].issue())) == nullptr); } void trust_auto_clear_trust_auto_clear() { testcase("trust auto clear"); using namespace jtx; Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob"); env.close(); env.trust(Account("bob")["USD"](1000), "alice"); env(pay("bob", "alice", Account("bob")["USD"](50))); env.trust(Account("bob")["USD"](0), "alice"); Json::Value jv; Json::Reader().parse( R"({ "Balance" : { "currency" : "USD", "issuer" : "rrrrrrrrrrrrrrrrrrrrBZbvji", "value" : "50" }, "Flags" : 65536, "HighLimit" : { "currency" : "USD", "issuer" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", "value" : "0" }, "HighNode" : "0", "LedgerEntryType" : "RippleState", "LowLimit" : { "currency" : "USD", "issuer" : "rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn", "value" : "0" }, "LowNode" : "0" })", jv); auto const jv_l = env.le(keylet::line(Account("alice").id(), Account("bob")["USD"].issue())) ->getJson(JsonOptions::none); for (auto it = jv.begin(); it != jv.end(); ++it) BEAST_EXPECT(*it == jv_l[it.memberName()]); env(pay("alice", "bob", Account("alice")["USD"](50))); BEAST_EXPECT( env.le(keylet::line(Account("alice").id(), Account("bob")["USD"].issue())) == nullptr); } void path_find_01(bool const domainEnabled) { testcase( std::string("Path Find: XRP -> XRP and XRP -> IOU") + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; Account A2{"A2"}; Account A3{"A3"}; Account G1{"G1"}; Account G2{"G2"}; Account G3{"G3"}; Account M1{"M1"}; env.fund(XRP(100000), A1); env.fund(XRP(10000), A2); env.fund(XRP(1000), A3, G1, G2, G3, M1); env.close(); env.trust(G1["XYZ"](5000), A1); env.trust(G3["ABC"](5000), A1); env.trust(G2["XYZ"](5000), A2); env.trust(G3["ABC"](5000), A2); env.trust(A2["ABC"](1000), A3); env.trust(G1["XYZ"](100000), M1); env.trust(G2["XYZ"](100000), M1); env.trust(G3["ABC"](100000), M1); env.close(); env(pay(G1, A1, G1["XYZ"](3500))); env(pay(G3, A1, G3["ABC"](1200))); env(pay(G2, M1, G2["XYZ"](25000))); env(pay(G3, M1, G3["ABC"](25000))); env.close(); std::optional domainID; if (domainEnabled) { domainID = setupDomain(env, {A1, A2, A3, G1, G2, G3, M1}); env(offer(M1, G1["XYZ"](1000), G2["XYZ"](1000)), domain(*domainID)); env(offer(M1, XRP(10000), G3["ABC"](1000)), domain(*domainID)); env.close(); } else { env(offer(M1, G1["XYZ"](1000), G2["XYZ"](1000))); env(offer(M1, XRP(10000), G3["ABC"](1000))); env.close(); } STPathSet st; STAmount sa, da; { auto const& send_amt = XRP(10); std::tie(st, sa, da) = find_paths(env, A1, A2, send_amt, std::nullopt, xrpCurrency(), domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(st.empty()); } { // no path should exist for this since dest account // does not exist. auto const& send_amt = XRP(200); std::tie(st, sa, da) = find_paths(env, A1, Account{"A0"}, send_amt, std::nullopt, xrpCurrency(), domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(st.empty()); } { auto const& send_amt = G3["ABC"](10); std::tie(st, sa, da) = find_paths(env, A2, G3, send_amt, std::nullopt, xrpCurrency(), domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, XRP(100))); BEAST_EXPECT(same(st, stpath(IPE(G3["ABC"])))); } { auto const& send_amt = A2["ABC"](1); std::tie(st, sa, da) = find_paths(env, A1, A2, send_amt, std::nullopt, xrpCurrency(), domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, XRP(10))); BEAST_EXPECT(same(st, stpath(IPE(G3["ABC"]), G3))); } { auto const& send_amt = A3["ABC"](1); std::tie(st, sa, da) = find_paths(env, A1, A3, send_amt, std::nullopt, xrpCurrency(), domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, XRP(10))); BEAST_EXPECT(same(st, stpath(IPE(G3["ABC"]), G3, A2))); } } void path_find_02(bool const domainEnabled) { testcase( std::string("Path Find: non-XRP -> XRP") + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; Account A2{"A2"}; Account G3{"G3"}; Account M1{"M1"}; env.fund(XRP(1000), A1, A2, G3); env.fund(XRP(11000), M1); env.close(); env.trust(G3["ABC"](1000), A1, A2); env.trust(G3["ABC"](100000), M1); env.close(); env(pay(G3, A1, G3["ABC"](1000))); env(pay(G3, A2, G3["ABC"](1000))); env(pay(G3, M1, G3["ABC"](1200))); env.close(); std::optional domainID; if (domainEnabled) { domainID = setupDomain(env, {A1, A2, G3, M1}); env(offer(M1, G3["ABC"](1000), XRP(10000)), domain(*domainID)); } else { env(offer(M1, G3["ABC"](1000), XRP(10000))); } STPathSet st; STAmount sa, da; auto const& send_amt = XRP(10); { std::tie(st, sa, da) = find_paths(env, A1, A2, send_amt, std::nullopt, A2["ABC"].currency, domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["ABC"](1))); BEAST_EXPECT(same(st, stpath(G3, IPE(xrpIssue())))); } // domain offer will not be considered in pathfinding for non-domain // paths if (domainEnabled) { std::tie(st, sa, da) = find_paths(env, A1, A2, send_amt, std::nullopt, A2["ABC"].currency); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(st.empty()); } } void path_find_04(bool const domainEnabled) { testcase( std::string("Path Find: Bitstamp and SnapSwap, liquidity with no offers") + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; Account A2{"A2"}; Account G1BS{"G1BS"}; Account G2SW{"G2SW"}; Account M1{"M1"}; env.fund(XRP(1000), G1BS, G2SW, A1, A2); env.fund(XRP(11000), M1); env.close(); env.trust(G1BS["HKD"](2000), A1); env.trust(G2SW["HKD"](2000), A2); env.trust(G1BS["HKD"](100000), M1); env.trust(G2SW["HKD"](100000), M1); env.close(); env(pay(G1BS, A1, G1BS["HKD"](1000))); env(pay(G2SW, A2, G2SW["HKD"](1000))); // SnapSwap wants to be able to set trust line quality settings so they // can charge a fee when transactions ripple across. Liquidity // provider, via trusting/holding both accounts env(pay(G1BS, M1, G1BS["HKD"](1200))); env(pay(G2SW, M1, G2SW["HKD"](5000))); env.close(); std::optional domainID; if (domainEnabled) domainID = setupDomain(env, {A1, A2, G1BS, G2SW, M1}); STPathSet st; STAmount sa, da; { auto const& send_amt = A2["HKD"](10); std::tie(st, sa, da) = find_paths(env, A1, A2, send_amt, std::nullopt, A2["HKD"].currency, domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same(st, stpath(G1BS, M1, G2SW))); } { auto const& send_amt = A1["HKD"](10); std::tie(st, sa, da) = find_paths(env, A2, A1, send_amt, std::nullopt, A1["HKD"].currency, domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A2["HKD"](10))); BEAST_EXPECT(same(st, stpath(G2SW, M1, G1BS))); } { auto const& send_amt = A2["HKD"](10); std::tie(st, sa, da) = find_paths(env, G1BS, A2, send_amt, std::nullopt, A1["HKD"].currency, domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, G1BS["HKD"](10))); BEAST_EXPECT(same(st, stpath(M1, G2SW))); } { auto const& send_amt = M1["HKD"](10); std::tie(st, sa, da) = find_paths(env, M1, G1BS, send_amt, std::nullopt, A1["HKD"].currency, domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, M1["HKD"](10))); BEAST_EXPECT(st.empty()); } { auto const& send_amt = A1["HKD"](10); std::tie(st, sa, da) = find_paths(env, G2SW, A1, send_amt, std::nullopt, A1["HKD"].currency, domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, G2SW["HKD"](10))); BEAST_EXPECT(same(st, stpath(M1, G1BS))); } } void path_find_05(bool const domainEnabled) { testcase( std::string("Path Find: non-XRP -> non-XRP, same currency") + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; Account A2{"A2"}; Account A3{"A3"}; Account A4{"A4"}; Account G1{"G1"}; Account G2{"G2"}; Account G3{"G3"}; Account G4{"G4"}; Account M1{"M1"}; Account M2{"M2"}; env.fund(XRP(1000), A1, A2, A3, G1, G2, G3, G4); env.fund(XRP(10000), A4); env.fund(XRP(11000), M1, M2); env.close(); env.trust(G1["HKD"](2000), A1); env.trust(G2["HKD"](2000), A2); env.trust(G1["HKD"](2000), A3); env.trust(G1["HKD"](100000), M1); env.trust(G2["HKD"](100000), M1); env.trust(G1["HKD"](100000), M2); env.trust(G2["HKD"](100000), M2); env.close(); env(pay(G1, A1, G1["HKD"](1000))); env(pay(G2, A2, G2["HKD"](1000))); env(pay(G1, A3, G1["HKD"](1000))); env(pay(G1, M1, G1["HKD"](1200))); env(pay(G2, M1, G2["HKD"](5000))); env(pay(G1, M2, G1["HKD"](1200))); env(pay(G2, M2, G2["HKD"](5000))); env.close(); std::optional domainID; if (domainEnabled) { domainID = setupDomain(env, {A1, A2, A3, A4, G1, G2, G3, G4, M1, M2}); env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), domain(*domainID)); env(offer(M2, XRP(10000), G2["HKD"](1000)), domain(*domainID)); env(offer(M2, G1["HKD"](1000), XRP(10000)), domain(*domainID)); } else { env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); env(offer(M2, XRP(10000), G2["HKD"](1000))); env(offer(M2, G1["HKD"](1000), XRP(10000))); } STPathSet st; STAmount sa, da; { // A) Borrow or repay -- // Source -> Destination (repay source issuer) auto const& send_amt = G1["HKD"](10); std::tie(st, sa, da) = find_paths(env, A1, G1, send_amt, std::nullopt, G1["HKD"].currency, domainID); BEAST_EXPECT(st.empty()); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); } { // A2) Borrow or repay -- // Source -> Destination (repay destination issuer) auto const& send_amt = A1["HKD"](10); std::tie(st, sa, da) = find_paths(env, A1, G1, send_amt, std::nullopt, G1["HKD"].currency, domainID); BEAST_EXPECT(st.empty()); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); } { // B) Common gateway -- // Source -> AC -> Destination auto const& send_amt = A3["HKD"](10); std::tie(st, sa, da) = find_paths(env, A1, A3, send_amt, std::nullopt, G1["HKD"].currency, domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same(st, stpath(G1))); } { // C) Gateway to gateway -- // Source -> OB -> Destination auto const& send_amt = G2["HKD"](10); std::tie(st, sa, da) = find_paths(env, G1, G2, send_amt, std::nullopt, G1["HKD"].currency, domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, G1["HKD"](10))); BEAST_EXPECT(same( st, stpath(IPE(G2["HKD"])), stpath(M1), stpath(M2), stpath(IPE(xrpIssue()), IPE(G2["HKD"])))); } { // D) User to unlinked gateway via order book -- // Source -> AC -> OB -> Destination auto const& send_amt = G2["HKD"](10); std::tie(st, sa, da) = find_paths(env, A1, G2, send_amt, std::nullopt, G1["HKD"].currency, domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same( st, stpath(G1, M1), stpath(G1, M2), stpath(G1, IPE(G2["HKD"])), stpath(G1, IPE(xrpIssue()), IPE(G2["HKD"])))); } { // I4) XRP bridge" -- // Source -> AC -> OB to XRP -> OB from XRP -> AC -> // Destination auto const& send_amt = A2["HKD"](10); std::tie(st, sa, da) = find_paths(env, A1, A2, send_amt, std::nullopt, G1["HKD"].currency, domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same( st, stpath(G1, M1, G2), stpath(G1, M2, G2), stpath(G1, IPE(G2["HKD"]), G2), stpath(G1, IPE(xrpIssue()), IPE(G2["HKD"]), G2))); } } void path_find_06(bool const domainEnabled) { testcase( std::string("Path Find: non-XRP -> non-XRP, same currency)") + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; Account A2{"A2"}; Account A3{"A3"}; Account G1{"G1"}; Account G2{"G2"}; Account M1{"M1"}; env.fund(XRP(11000), M1); env.fund(XRP(1000), A1, A2, A3, G1, G2); env.close(); env.trust(G1["HKD"](2000), A1); env.trust(G2["HKD"](2000), A2); env.trust(A2["HKD"](2000), A3); env.trust(G1["HKD"](100000), M1); env.trust(G2["HKD"](100000), M1); env.close(); env(pay(G1, A1, G1["HKD"](1000))); env(pay(G2, A2, G2["HKD"](1000))); env(pay(G1, M1, G1["HKD"](5000))); env(pay(G2, M1, G2["HKD"](5000))); env.close(); std::optional domainID; if (domainEnabled) { domainID = setupDomain(env, {A1, A2, A3, G1, G2, M1}); env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), domain(*domainID)); } else { env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); } // E) Gateway to user // Source -> OB -> AC -> Destination auto const& send_amt = A2["HKD"](10); STPathSet st; STAmount sa, da; std::tie(st, sa, da) = find_paths(env, G1, A2, send_amt, std::nullopt, G1["HKD"].currency, domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, G1["HKD"](10))); BEAST_EXPECT(same(st, stpath(M1, G2), stpath(IPE(G2["HKD"]), G2))); } void receive_max(bool const domainEnabled) { testcase(std::string("Receive max") + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const charlie = Account("charlie"); auto const gw = Account("gw"); auto const USD = gw["USD"]; { // XRP -> IOU receive max Env env = pathTestEnv(); env.fund(XRP(10000), alice, bob, charlie, gw); env.close(); env.trust(USD(100), alice, bob, charlie); env.close(); env(pay(gw, charlie, USD(10))); env.close(); std::optional domainID; if (domainEnabled) { domainID = setupDomain(env, {alice, bob, charlie, gw}); env(offer(charlie, XRP(10), USD(10)), domain(*domainID)); env.close(); } else { env(offer(charlie, XRP(10), USD(10))); env.close(); } auto [st, sa, da] = find_paths(env, alice, bob, USD(-1), XRP(100).value(), std::nullopt, domainID); BEAST_EXPECT(sa == XRP(10)); BEAST_EXPECT(equal(da, USD(10))); if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1)) { auto const& pathElem = st[0][0]; BEAST_EXPECT( pathElem.isOffer() && pathElem.getIssuerID() == gw.id() && pathElem.getCurrency() == USD.currency); } } { // IOU -> XRP receive max Env env = pathTestEnv(); env.fund(XRP(10000), alice, bob, charlie, gw); env.close(); env.trust(USD(100), alice, bob, charlie); env.close(); env(pay(gw, alice, USD(10))); env.close(); std::optional domainID; if (domainEnabled) { domainID = setupDomain(env, {alice, bob, charlie, gw}); env(offer(charlie, USD(10), XRP(10)), domain(*domainID)); env.close(); } else { env(offer(charlie, USD(10), XRP(10))); env.close(); } auto [st, sa, da] = find_paths(env, alice, bob, drops(-1), USD(100).value(), std::nullopt, domainID); BEAST_EXPECT(sa == USD(10)); BEAST_EXPECT(equal(da, XRP(10))); if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1)) { auto const& pathElem = st[0][0]; BEAST_EXPECT( pathElem.isOffer() && pathElem.getIssuerID() == xrpAccount() && pathElem.getCurrency() == xrpCurrency()); } } } void noripple_combinations() { using namespace jtx; // This test will create trust lines with various values of the noRipple // flag. alice <-> george <-> bob george will sort of act like a // gateway, but use a different name to avoid the usual assumptions // about gateways. auto const alice = Account("alice"); auto const bob = Account("bob"); auto const george = Account("george"); auto const USD = george["USD"]; auto test = [&](std::string casename, bool aliceRipple, bool bobRipple, bool expectPath) { testcase(casename); Env env = pathTestEnv(); env.fund(XRP(10000), noripple(alice, bob, george)); env.close(); // Set the same flags at both ends of the trustline, even though // only george's matter. env(trust(alice, USD(100), aliceRipple ? tfClearNoRipple : tfSetNoRipple)); env(trust(george, alice["USD"](100), aliceRipple ? tfClearNoRipple : tfSetNoRipple)); env(trust(bob, USD(100), bobRipple ? tfClearNoRipple : tfSetNoRipple)); env(trust(george, bob["USD"](100), bobRipple ? tfClearNoRipple : tfSetNoRipple)); env.close(); env(pay(george, alice, USD(70))); env.close(); auto [st, sa, da] = find_paths(env, "alice", "bob", Account("bob")["USD"](5)); BEAST_EXPECT(equal(da, bob["USD"](5))); if (expectPath) { BEAST_EXPECT(st.size() == 1); BEAST_EXPECT(same(st, stpath("george"))); BEAST_EXPECT(equal(sa, alice["USD"](5))); } else { BEAST_EXPECT(st.size() == 0); BEAST_EXPECT(equal(sa, XRP(0))); } }; test("ripple -> ripple", true, true, true); test("ripple -> no ripple", true, false, true); test("no ripple -> ripple", false, true, true); test("no ripple -> no ripple", false, false, false); } void hybrid_offer_path() { testcase("Hybrid offer path"); using namespace jtx; // test cases copied from path_find_05 and ensures path results for // different combinations of open/domain/hybrid offers. `func` is a // lambda param that creates different types of offers auto testPathfind = [&](auto func, bool const domainEnabled = false) { Env env = pathTestEnv(); Account A1{"A1"}; Account A2{"A2"}; Account A3{"A3"}; Account A4{"A4"}; Account G1{"G1"}; Account G2{"G2"}; Account G3{"G3"}; Account G4{"G4"}; Account M1{"M1"}; Account M2{"M2"}; env.fund(XRP(1000), A1, A2, A3, G1, G2, G3, G4); env.fund(XRP(10000), A4); env.fund(XRP(11000), M1, M2); env.close(); env.trust(G1["HKD"](2000), A1); env.trust(G2["HKD"](2000), A2); env.trust(G1["HKD"](2000), A3); env.trust(G1["HKD"](100000), M1); env.trust(G2["HKD"](100000), M1); env.trust(G1["HKD"](100000), M2); env.trust(G2["HKD"](100000), M2); env.close(); env(pay(G1, A1, G1["HKD"](1000))); env(pay(G2, A2, G2["HKD"](1000))); env(pay(G1, A3, G1["HKD"](1000))); env(pay(G1, M1, G1["HKD"](1200))); env(pay(G2, M1, G2["HKD"](5000))); env(pay(G1, M2, G1["HKD"](1200))); env(pay(G2, M2, G2["HKD"](5000))); env.close(); std::optional domainID = setupDomain(env, {A1, A2, A3, A4, G1, G2, G3, G4, M1, M2}); BEAST_EXPECT(domainID); func(env, M1, M2, G1, G2, *domainID); STPathSet st; STAmount sa, da; { // A) Borrow or repay -- // Source -> Destination (repay source issuer) auto const& send_amt = G1["HKD"](10); std::tie(st, sa, da) = find_paths( env, A1, G1, send_amt, std::nullopt, G1["HKD"].currency, domainEnabled ? domainID : std::nullopt); BEAST_EXPECT(st.empty()); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); } { // A2) Borrow or repay -- // Source -> Destination (repay destination issuer) auto const& send_amt = A1["HKD"](10); std::tie(st, sa, da) = find_paths( env, A1, G1, send_amt, std::nullopt, G1["HKD"].currency, domainEnabled ? domainID : std::nullopt); BEAST_EXPECT(st.empty()); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); } { // B) Common gateway -- // Source -> AC -> Destination auto const& send_amt = A3["HKD"](10); std::tie(st, sa, da) = find_paths( env, A1, A3, send_amt, std::nullopt, G1["HKD"].currency, domainEnabled ? domainID : std::nullopt); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same(st, stpath(G1))); } { // C) Gateway to gateway -- // Source -> OB -> Destination auto const& send_amt = G2["HKD"](10); std::tie(st, sa, da) = find_paths( env, G1, G2, send_amt, std::nullopt, G1["HKD"].currency, domainEnabled ? domainID : std::nullopt); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, G1["HKD"](10))); BEAST_EXPECT(same( st, stpath(IPE(G2["HKD"])), stpath(M1), stpath(M2), stpath(IPE(xrpIssue()), IPE(G2["HKD"])))); } { // D) User to unlinked gateway via order book -- // Source -> AC -> OB -> Destination auto const& send_amt = G2["HKD"](10); std::tie(st, sa, da) = find_paths( env, A1, G2, send_amt, std::nullopt, G1["HKD"].currency, domainEnabled ? domainID : std::nullopt); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same( st, stpath(G1, M1), stpath(G1, M2), stpath(G1, IPE(G2["HKD"])), stpath(G1, IPE(xrpIssue()), IPE(G2["HKD"])))); } { // I4) XRP bridge" -- // Source -> AC -> OB to XRP -> OB from XRP -> AC -> // Destination auto const& send_amt = A2["HKD"](10); std::tie(st, sa, da) = find_paths( env, A1, A2, send_amt, std::nullopt, G1["HKD"].currency, domainEnabled ? domainID : std::nullopt); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same( st, stpath(G1, M1, G2), stpath(G1, M2, G2), stpath(G1, IPE(G2["HKD"]), G2), stpath(G1, IPE(xrpIssue()), IPE(G2["HKD"]), G2))); } }; // the following tests exercise different combinations of open/hybrid // offers to make sure that hybrid offers work in pathfinding for open // order book { testPathfind( [](Env& env, Account M1, Account M2, Account G1, Account G2, uint256 domainID) { env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), domain(domainID), txflags(tfHybrid)); env(offer(M2, XRP(10000), G2["HKD"](1000))); env(offer(M2, G1["HKD"](1000), XRP(10000))); }); testPathfind( [](Env& env, Account M1, Account M2, Account G1, Account G2, uint256 domainID) { env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), domain(domainID), txflags(tfHybrid)); env(offer(M2, XRP(10000), G2["HKD"](1000)), domain(domainID), txflags(tfHybrid)); env(offer(M2, G1["HKD"](1000), XRP(10000))); }); testPathfind( [](Env& env, Account M1, Account M2, Account G1, Account G2, uint256 domainID) { env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), domain(domainID), txflags(tfHybrid)); env(offer(M2, XRP(10000), G2["HKD"](1000)), domain(domainID), txflags(tfHybrid)); env(offer(M2, G1["HKD"](1000), XRP(10000)), domain(domainID), txflags(tfHybrid)); }); testPathfind( [](Env& env, Account M1, Account M2, Account G1, Account G2, uint256 domainID) { env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); env(offer(M2, XRP(10000), G2["HKD"](1000))); env(offer(M2, G1["HKD"](1000), XRP(10000)), domain(domainID), txflags(tfHybrid)); }); testPathfind( [](Env& env, Account M1, Account M2, Account G1, Account G2, uint256 domainID) { env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); env(offer(M2, XRP(10000), G2["HKD"](1000)), domain(domainID), txflags(tfHybrid)); env(offer(M2, G1["HKD"](1000), XRP(10000)), domain(domainID), txflags(tfHybrid)); }); } // the following tests exercise different combinations of domain/hybrid // offers to make sure that hybrid offers work in pathfinding for domain // order book { testPathfind( [](Env& env, Account M1, Account M2, Account G1, Account G2, uint256 domainID) { env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), domain(domainID), txflags(tfHybrid)); env(offer(M2, XRP(10000), G2["HKD"](1000)), domain(domainID)); env(offer(M2, G1["HKD"](1000), XRP(10000)), domain(domainID)); }, true); testPathfind( [](Env& env, Account M1, Account M2, Account G1, Account G2, uint256 domainID) { env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), domain(domainID), txflags(tfHybrid)); env(offer(M2, XRP(10000), G2["HKD"](1000)), domain(domainID), txflags(tfHybrid)); env(offer(M2, G1["HKD"](1000), XRP(10000)), domain(domainID)); }, true); testPathfind( [](Env& env, Account M1, Account M2, Account G1, Account G2, uint256 domainID) { env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), domain(domainID)); env(offer(M2, XRP(10000), G2["HKD"](1000)), domain(domainID)); env(offer(M2, G1["HKD"](1000), XRP(10000)), domain(domainID), txflags(tfHybrid)); }, true); testPathfind( [](Env& env, Account M1, Account M2, Account G1, Account G2, uint256 domainID) { env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), domain(domainID)); env(offer(M2, XRP(10000), G2["HKD"](1000)), domain(domainID), txflags(tfHybrid)); env(offer(M2, G1["HKD"](1000), XRP(10000)), domain(domainID), txflags(tfHybrid)); }, true); } } void amm_domain_path() { testcase("AMM not used in domain path"); using namespace jtx; Env env = pathTestEnv(); PermissionedDEX permDex(env); auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = permDex; AMM amm(env, alice, XRP(10), USD(50)); STPathSet st; STAmount sa, da; auto const& send_amt = XRP(1); // doing pathfind with domain won't include amm std::tie(st, sa, da) = find_paths(env, bob, carol, send_amt, std::nullopt, USD.currency, domainID); BEAST_EXPECT(st.empty()); // a non-domain pathfind returns amm in the path std::tie(st, sa, da) = find_paths(env, bob, carol, send_amt, std::nullopt, USD.currency); BEAST_EXPECT(same(st, stpath(gw, IPE(xrpIssue())))); } void run() override { source_currencies_limit(); no_direct_path_no_intermediary_no_alternatives(); direct_path_no_intermediary(); payment_auto_path_find(); indirect_paths_path_find(); alternative_paths_consume_best_transfer_first(); issues_path_negative_ripple_client_issue_23_smaller(); issues_path_negative_ripple_client_issue_23_larger(); quality_paths_quality_set_and_test(); trust_auto_clear_trust_normal_clear(); trust_auto_clear_trust_auto_clear(); noripple_combinations(); for (bool const domainEnabled : {false, true}) { path_find(domainEnabled); path_find_consume_all(domainEnabled); alternative_path_consume_both(domainEnabled); alternative_paths_consume_best_transfer(domainEnabled); alternative_paths_limit_returned_paths_to_best_quality(domainEnabled); issues_path_negative_issue(domainEnabled); via_offers_via_gateway(domainEnabled); xrp_to_xrp(domainEnabled); receive_max(domainEnabled); // The following path_find_NN tests are data driven tests // that were originally implemented in js/coffee and migrated // here. The quantities and currencies used are taken directly from // those legacy tests, which in some cases probably represented // customer use cases. path_find_01(domainEnabled); path_find_02(domainEnabled); path_find_04(domainEnabled); path_find_05(domainEnabled); path_find_06(domainEnabled); } hybrid_offer_path(); amm_domain_path(); } }; BEAST_DEFINE_TESTSUITE(Path, app, xrpl); } // namespace test } // namespace xrpl