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

653 lines
22 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/mpt.h>
#include <test/jtx/offer.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/sendmax.h>
#include <test/jtx/ter.h>
#include <test/jtx/txflags.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STPathSet.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/tx/paths/RippleCalc.h>
#include <xrpl/tx/paths/detail/Steps.h>
#include <xrpl/tx/transactors/dex/AMMContext.h>
#include <optional>
#include <type_traits>
namespace xrpl::test {
struct PayStrandMPT_test : public beast::unit_test::suite
{
static jtx::DirectStepInfo
makeEndpointStep(jtx::Account const& src, jtx::Account const& dst, jtx::IOU const& iou)
{
return jtx::DirectStepInfo{.src = src, .dst = dst, .currency = iou.currency};
}
static jtx::MPTEndpointStepInfo
makeEndpointStep(jtx::Account const& src, jtx::Account const& dst, jtx::MPT const& mpt)
{
return jtx::MPTEndpointStepInfo{.src = src, .dst = dst, .mptid = mpt.mpt()};
}
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");
using M = MPTEndpointStepInfo;
using B = xrpl::Book;
using XRPS = XRPEndpointStepInfo;
AMMContext ammContext(alice, false);
auto test = [&, this](
jtx::Env& env,
Asset const& deliver,
std::optional<Asset> 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().getLogs().journal("Flow"));
BEAST_EXPECT(ter == expTer);
if (sizeof...(expSteps) != 0)
BEAST_EXPECT(jtx::equal(strand, std::forward<decltype(expSteps)>(expSteps)...));
};
{
auto testMultiToken = [&](auto&& issue1, auto&& issue2) {
Env env(*this, features);
env.fund(XRP(10'000), alice, bob, gw);
MPT const USD =
MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 1'000});
auto const bobUSD = issue1(
{.env = env,
.token = "USD",
.issuer = bob,
.holders = {alice},
.limit = 1'000});
MPT const EUR =
MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 1'000});
auto const bobEUR = issue2(
{.env = env,
.token = "EUR",
.issuer = bob,
.holders = {alice},
.limit = 1'000});
env(pay(gw, alice, EUR(100)));
{
// Original test is
// STPath({ipe(bob["USD"]), cpe(EUR.currency)});
// which ripples through same currency, different issuer.
// This results in 5 steps:
// 1 DirectStep alice -> gw EUR/gw
// 2 Book EUR/gw USD/bob
// 3 Book USD/bob EUR/bob
// 4 Book EUR/bob XRP
// 5 XRPEndpoint
// This is somewhat equivalent path with MPT
STPath const path = STPath({ipe(bobUSD), ipe(bobEUR), 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().getLogs().journal("Flow"));
(void)_;
BEAST_EXPECT(ter == tesSUCCESS);
}
{
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().getLogs().journal("Flow"));
(void)_;
BEAST_EXPECT(ter == tesSUCCESS);
}
};
testHelper2TokensMix(testMultiToken);
}
{
auto testMultiToken = [&](auto&& issue1, auto&& issue2) {
Env env(*this, features);
env.fund(XRP(10'000), alice, bob, carol, gw);
auto USD = issue1({.env = env, .token = "USD", .issuer = gw, .limit = 1'000});
using tUSD = std::decay_t<decltype(USD)>;
auto EUR = issue2({.env = env, .token = "EUR", .issuer = gw, .limit = 1'000});
using tEUR = std::decay_t<decltype(EUR)>;
auto const err = [&]() {
if constexpr (std::is_same_v<tUSD, MPT>)
{
return tecNO_AUTH;
}
else
{
return terNO_LINE;
}
}();
test(env, USD, std::nullopt, STPath(), err);
if constexpr (std::is_same_v<tUSD, MPT>)
{
MPTTester(env, gw, USD).authorizeHolders({alice, bob, carol});
}
else
{
env.trust(USD(1'000), 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,
makeEndpointStep(alice, gw, USD),
makeEndpointStep(gw, bob, USD));
if constexpr (std::is_same_v<tEUR, MPT>)
{
MPTTester(env, gw, EUR).authorizeHolders({alice, bob});
}
else
{
env.trust(EUR(1'000), alice, bob);
}
// Insert implied offer
test(
env,
EUR,
USD,
STPath(),
tesSUCCESS,
makeEndpointStep(alice, gw, USD),
B{USD, EUR, std::nullopt},
makeEndpointStep(gw, bob, EUR));
// Path with explicit offer
test(
env,
EUR,
USD,
STPath({ipe(EUR)}),
tesSUCCESS,
makeEndpointStep(alice, gw, USD),
B{USD, EUR, std::nullopt},
makeEndpointStep(gw, bob, EUR));
// Path with XRP src currency
test(
env,
USD,
xrpIssue(),
STPath({ipe(USD)}),
tesSUCCESS,
XRPS{alice},
B{XRP, USD, std::nullopt},
makeEndpointStep(gw, bob, USD));
// Path with XRP dst currency.
test(
env,
xrpIssue(),
USD,
STPath({STPathElement{
STPathElement::typeCurrency, xrpAccount(), xrpCurrency(), xrpAccount()}}),
tesSUCCESS,
makeEndpointStep(alice, gw, USD),
B{USD, XRP, std::nullopt},
XRPS{bob});
// Path with XRP cross currency bridged payment
test(
env,
EUR,
USD,
STPath({cpe(xrpCurrency())}),
tesSUCCESS,
makeEndpointStep(alice, gw, USD),
B{USD, XRP, std::nullopt},
B{XRP, EUR, std::nullopt},
makeEndpointStep(gw, bob, EUR));
// Create an offer with the same in/out issue
test(env, EUR, USD, STPath({ipe(USD), ipe(EUR)}), temBAD_PATH);
// 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);
};
testHelper2TokensMix(testMultiToken);
}
{
// cannot have more than one offer with the same output issue
using namespace jtx;
auto testMultiToken = [&](auto&& issue1, auto&& issue2) {
Env env(*this, features);
env.fund(XRP(10'000), alice, bob, carol, gw);
auto const USD = issue1(
{.env = env,
.token = "USD",
.issuer = gw,
.holders = {alice, bob, carol},
.limit = 10'000});
auto const EUR = issue2(
{.env = env,
.token = "EUR",
.issuer = gw,
.holders = {alice, bob, carol},
.limit = 10'000});
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));
};
testHelper2TokensMix(testMultiToken);
}
{
// check global freeze
Env env(*this, features);
env.fund(XRP(10000), alice, bob, gw);
auto USDM = MPTTester(
{.env = env,
.issuer = gw,
.holders = {alice, bob},
.flags = MPTDEXFlags | tfMPTCanLock,
.maxAmt = 1'000});
MPT const USD = USDM;
env(pay(gw, alice, USD(100)));
// Account can't issue payments
USDM.set({.holder = alice, .flags = tfMPTLock});
test(env, USD, std::nullopt, STPath(), terLOCKED);
USDM.set({.holder = alice, .flags = tfMPTUnlock});
test(env, USD, std::nullopt, STPath(), tesSUCCESS);
// Account can not issue funds
USDM.set({.flags = tfMPTLock});
test(env, USD, std::nullopt, STPath(), terLOCKED);
USDM.set({.flags = tfMPTUnlock});
test(env, USD, std::nullopt, STPath(), tesSUCCESS);
// Account can not receive funds
USDM.set({.holder = bob, .flags = tfMPTLock});
test(env, USD, std::nullopt, STPath(), terLOCKED);
USDM.set({.holder = bob, .flags = tfMPTUnlock});
test(env, USD, std::nullopt, STPath(), tesSUCCESS);
}
{
// check no auth
// An account may require authorization to receive MPTs from an
// issuer
Env env(*this, features);
env.fund(XRP(10'000), alice, bob, gw);
auto USDM = MPTTester(
{.env = env,
.issuer = gw,
.flags = MPTDEXFlags | tfMPTRequireAuth,
.maxAmt = 1'000});
MPT const USD = USDM;
// Authorize alice but not bob
USDM.authorize({.account = alice});
USDM.authorize({.holder = alice});
env(pay(gw, alice, USD(100)));
env.require(balance(alice, USD(100)));
test(env, USD, std::nullopt, STPath(), tecNO_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().getLogs().journal("Flow"));
BEAST_EXPECT(ter == tesSUCCESS);
BEAST_EXPECT(equal(strand, M{alice, gw, USD}));
}
{
// last step xrp from offer
Env env(*this, features);
env.fund(XRP(10'000), alice, bob, gw);
MPT const USD =
MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 1'000});
env(pay(gw, alice, USD(100)));
// alice -> USD/XRP -> bob
STPath path;
path.emplace_back(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().getLogs().journal("Flow"));
BEAST_EXPECT(ter == tesSUCCESS);
BEAST_EXPECT(
equal(strand, M{alice, gw, USD}, 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");
{
Env env(*this, features);
env.fund(XRP(10000), alice, bob, carol, gw);
MPT const USD = MPTTester(
{.env = env, .issuer = gw, .holders = {alice, bob, carol}, .maxAmt = 10'000});
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);
MPT const USD = MPTTester(
{.env = env, .issuer = gw, .holders = {alice, bob, carol}, .maxAmt = 10'000});
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 EUR = gw["EUR"];
auto const CNY = gw["CNY"];
{
Env env(*this, features);
env.fund(XRP(10'000), alice, bob, carol, gw);
MPT const USD = MPTTester(
{.env = env, .issuer = gw, .holders = {alice, bob, carol}, .maxAmt = 10'000});
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));
}
{
auto testMultiToken = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
Env env(*this, features);
env.fund(XRP(10'000), alice, bob, carol, gw);
auto const USD = issue1(
{.env = env,
.token = "USD",
.issuer = gw,
.holders = {alice, bob, carol},
.limit = 10'000});
auto const EUR = issue2(
{.env = env,
.token = "EUR",
.issuer = gw,
.holders = {alice, bob, carol},
.limit = 10'000});
auto const CNY = issue3(
{.env = env,
.token = "CNY",
.issuer = gw,
.holders = {alice, bob, carol},
.limit = 10'000});
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));
};
testHelper3TokensMix(testMultiToken);
}
}
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");
Env env(*this, features);
env.fund(XRP(10'000), alice, bob, gw);
MPT const USD = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
STAmount const sendMax{USD, 100, 1};
STAmount const noAccountAmount{MPTIssue{0, 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 = testable_amendments();
testToStrand(sa);
testRIPD1373(sa);
testLoop(sa);
testNoAccount(sa);
}
};
BEAST_DEFINE_TESTSUITE(PayStrandMPT, app, xrpl);
} // namespace xrpl::test