Files
rippled/src/test/app/PathMPT_test.cpp
2026-04-21 15:32:51 +00:00

464 lines
16 KiB
C++

#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h>
#include <test/jtx/domain.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/mpt.h>
#include <test/jtx/offer.h>
#include <test/jtx/pay.h>
#include <test/jtx/permissioned_dex.h>
#include <xrpld/core/Config.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/RPCHandler.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/JobQueue.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STPathSet.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/resource/Charge.h>
#include <xrpl/resource/Consumer.h>
#include <xrpl/resource/Fees.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <tuple>
#include <vector>
namespace xrpl::test {
namespace detail {
static Json::Value
rpf(jtx::Account const& src,
jtx::Account const& dst,
xrpl::test::jtx::MPT const& USD,
std::vector<MPTID> const& num_src)
{
Json::Value jv = Json::objectValue;
jv[jss::command] = "ripple_path_find";
jv[jss::source_account] = toBase58(src);
if (!num_src.empty())
{
auto& sc = (jv[jss::source_currencies] = Json::arrayValue);
Json::Value j = Json::objectValue;
for (auto const& id : num_src)
{
j[jss::mpt_issuance_id] = to_string(id);
sc.append(j);
}
}
auto const d = toBase58(dst);
jv[jss::destination_account] = d;
Json::Value& j = (jv[jss::destination_amount] = Json::objectValue);
j[jss::mpt_issuance_id] = to_string(USD.mpt());
j[jss::value] = "1";
return jv;
}
} // namespace detail
//------------------------------------------------------------------------------
class PathMPT_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<Config> cfg) {
cfg->PATH_SEARCH_OLD = 7;
cfg->PATH_SEARCH = 7;
cfg->PATH_SEARCH_MAX = 10;
return cfg;
}));
}
public:
void
source_currencies_limit()
{
testcase("source currency limits");
using namespace std::chrono_literals;
using namespace jtx;
Env env = pathTestEnv();
auto const gw = Account("gateway");
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10'000), "alice", "bob", gw);
MPT const USD =
MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 100});
auto& app = env.app();
Resource::Charge loadType = Resource::feeReferenceRPC;
Resource::Consumer c;
RPC::JsonContext context{
{.j = env.journal,
.app = app,
.loadType = loadType,
.netOps = app.getOPs(),
.ledgerMaster = app.getLedgerMaster(),
.consumer = c,
.role = Role::USER,
.coro = {},
.infoSub = {},
.apiVersion = RPC::apiVersionIfUnspecified},
{},
{}};
Json::Value result;
gate g;
// Test RPC::Tuning::max_src_cur source currencies.
std::vector<MPTID> num_src;
num_src.reserve(RPC::Tuning::max_src_cur);
for (std::uint8_t i = 0; i < RPC::Tuning::max_src_cur; ++i)
num_src.push_back(makeMptID(i, bob));
app.getJobQueue().postCoro(jtCLIENT, "RPC-Client", [&](auto const& coro) {
context.params = xrpl::test::detail::rpf(alice, bob, USD, num_src);
context.coro = coro;
RPC::doCommand(context, result);
g.signal();
});
BEAST_EXPECT(g.wait_for(5s));
BEAST_EXPECT(!result.isMember(jss::error));
// Test more than RPC::Tuning::max_src_cur source currencies.
num_src.push_back(makeMptID(RPC::Tuning::max_src_cur, bob));
app.getJobQueue().postCoro(jtCLIENT, "RPC-Client", [&](auto const& coro) {
context.params = xrpl::test::detail::rpf(alice, bob, USD, num_src);
context.coro = coro;
RPC::doCommand(context, result);
g.signal();
});
BEAST_EXPECT(g.wait_for(5s));
BEAST_EXPECT(result.isMember(jss::error));
// Test RPC::Tuning::max_auto_src_cur source currencies.
num_src.clear();
for (auto i = 0; i < (RPC::Tuning::max_auto_src_cur - 1); ++i)
{
auto CURM = MPTTester({.env = env, .issuer = alice, .holders = {bob}});
num_src.push_back(CURM.issuanceID());
}
app.getJobQueue().postCoro(jtCLIENT, "RPC-Client", [&](auto const& coro) {
context.params = xrpl::test::detail::rpf(alice, bob, USD, {});
context.coro = coro;
RPC::doCommand(context, result);
g.signal();
});
BEAST_EXPECT(g.wait_for(5s));
BEAST_EXPECT(!result.isMember(jss::error));
// Test more than RPC::Tuning::max_auto_src_cur source currencies.
auto CURM = MPTTester({.env = env, .issuer = alice, .holders = {bob}});
app.getJobQueue().postCoro(jtCLIENT, "RPC-Client", [&](auto const& coro) {
context.params = xrpl::test::detail::rpf(alice, bob, USD, {});
context.coro = coro;
RPC::doCommand(context, result);
g.signal();
});
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(10'000), "alice", "bob");
auto USDM = MPTTester({.env = env, .issuer = "bob"});
auto const result = find_paths(env, "alice", "bob", USDM(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(10'000), "alice", "bob");
MPT const USD = MPTTester({.env = env, .issuer = "alice", .holders = {"bob"}});
STPathSet st;
STAmount sa;
std::tie(st, sa, std::ignore) = find_paths(env, "alice", "bob", USD(5));
BEAST_EXPECT(st.empty());
BEAST_EXPECT(equal(sa, USD(5)));
}
void
payment_auto_path_find()
{
testcase("payment auto path find");
using namespace jtx;
Env env = pathTestEnv();
auto const gw = Account("gateway");
env.fund(XRP(10'000), "alice", "bob", gw);
MPT const USD = MPTTester({.env = env, .issuer = gw, .holders = {"alice", "bob"}});
env(pay(gw, "alice", USD(70)));
env(pay("alice", "bob", USD(24)));
env.require(balance("alice", USD(46)));
env.require(balance("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");
env.fund(XRP(10'000), "alice", "bob", gw);
MPT const USD = MPTTester({.env = env, .issuer = gw, .holders = {"alice", "bob"}});
env(pay(gw, "alice", USD(70)));
env(pay(gw, "bob", USD(50)));
std::optional<uint256> domainID;
if (domainEnabled)
domainID = setupDomain(env, {"alice", "bob", gw});
STPathSet st;
STAmount sa;
STAmount da;
std::tie(st, sa, da) = find_paths(
env, "alice", "bob", USD(5), std::nullopt, std::nullopt, std::nullopt, domainID);
// Note, a direct IOU payment will have "gateway" as alternative path
// since IOU supports rippling
BEAST_EXPECT(st.empty());
BEAST_EXPECT(equal(sa, USD(5)));
BEAST_EXPECT(equal(da, USD(5)));
}
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();
auto const gw = Account("gateway");
env.fund(XRP(10'000), "alice", "bob", "carol", gw);
MPT const USD = MPTTester({.env = env, .issuer = gw, .holders = {"bob", "carol"}});
MPT const AUD(makeMptID(0, gw));
env(pay(gw, "carol", USD(100)));
std::optional<uint256> 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",
AUD(-1),
std::optional<STAmount>(XRP(100'000'000)),
std::nullopt,
std::nullopt,
domainID);
BEAST_EXPECT(st.empty());
std::tie(st, sa, da) = find_paths(
env,
"alice",
"bob",
USD(-1),
std::optional<STAmount>(XRP(100'000'000)),
std::nullopt,
std::nullopt,
domainID);
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.getMPTID() == USD.issuanceID);
}
BEAST_EXPECT(sa == XRP(100));
BEAST_EXPECT(equal(da, 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<STAmount>(XRP(1000000)),
std::nullopt,
std::nullopt); // not specifying a domain
BEAST_EXPECT(st.empty());
}
}
}
void
alternative_paths_consume_best_transfer(bool const domainEnabled)
{
testcase(
std::string("alternative path consume best transfer") +
(domainEnabled ? " w/ " : " w/o ") + "domain");
using namespace jtx;
Env env = pathTestEnv();
auto const gw = Account("gateway");
auto const gw2 = Account("gateway2");
env.fund(XRP(10'000), "alice", "bob", gw, gw2);
MPT const USD = MPTTester({.env = env, .issuer = gw, .holders = {"alice", "bob"}});
MPT const gw2_USD = MPTTester(
{.env = env, .issuer = gw2, .holders = {"alice", "bob"}, .transferFee = 1'000});
std::optional<uint256> 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)));
}
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");
{
// XRP -> MPT receive max
Env env = pathTestEnv();
env.fund(XRP(10'000), alice, bob, charlie, gw);
env.close();
MPT const USD = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, charlie}});
env(pay(gw, charlie, USD(10)));
env.close();
std::optional<uint256> domainID;
if (domainEnabled)
{
domainID = setupDomain(env, {alice, bob, charlie, gw});
env(offer(charlie, XRP(10), USD(10)), domain(*domainID));
}
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, 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.getMPTID() == USD.mpt());
}
}
{
// MPT -> XRP receive max
Env env = pathTestEnv();
env.fund(XRP(10'000), alice, bob, charlie, gw);
env.close();
MPT const USD = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, charlie}});
env(pay(gw, alice, USD(10)));
env.close();
std::optional<uint256> domainID;
if (domainEnabled)
{
domainID = setupDomain(env, {alice, bob, charlie, gw});
env(offer(charlie, USD(10), XRP(10)), domain(*domainID));
}
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, 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
run() override
{
source_currencies_limit();
no_direct_path_no_intermediary_no_alternatives();
direct_path_no_intermediary();
payment_auto_path_find();
for (auto const domainEnabled : {false, true})
{
path_find(domainEnabled);
path_find_consume_all(domainEnabled);
alternative_paths_consume_best_transfer(domainEnabled);
receive_max(domainEnabled);
}
}
};
BEAST_DEFINE_TESTSUITE(PathMPT, app, xrpl);
} // namespace xrpl::test