feat: Add MPT support to DEX (#5285)

This commit is contained in:
Gregory Tsipenyuk
2026-04-08 12:17:37 -04:00
committed by GitHub
parent 6d1a5be8d2
commit dfcad69155
223 changed files with 34897 additions and 4228 deletions

View File

@@ -1,9 +1,16 @@
#include <test/jtx/TestHelpers.h>
#include <test/jtx/mpt.h>
#include <test/jtx/offer.h>
#include <test/jtx/owners.h>
#include <test/jtx/rate.h>
#include <test/jtx/trust.h>
#include <xrpld/rpc/RPCHandler.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/resource/Fees.h>
namespace xrpl {
namespace test {
@@ -55,7 +62,7 @@ stpath_append_one(STPath& st, STPathElement const& pe)
bool
equal(STAmount const& sa1, STAmount const& sa2)
{
return sa1 == sa2 && sa1.issue().account == sa2.issue().account;
return sa1 == sa2 && sa1.getIssuer() == sa2.getIssuer();
}
// Issue path element
@@ -65,9 +72,211 @@ IPE(Issue const& iss)
return STPathElement(
STPathElement::typeCurrency | STPathElement::typeIssuer,
xrpAccount(),
iss.currency,
PathAsset{iss.currency},
iss.account);
}
STPathElement
IPE(MPTIssue const& iss)
{
return STPathElement(
STPathElement::typeMPT | STPathElement::typeIssuer,
xrpAccount(),
PathAsset{iss.getMptID()},
iss.getIssuer());
}
static void
addSourceAsset(
Json::Value& jv,
PathAsset const& srcAsset,
std::optional<AccountID> const& srcIssuer)
{
std::visit(
[&]<typename TAsset>(TAsset const& asset) {
if constexpr (std::is_same_v<TAsset, Currency>)
{
jv[jss::currency] = to_string(asset);
if (srcIssuer)
jv[jss::issuer] = to_string(*srcIssuer);
}
else
{
if (srcIssuer)
Throw<std::runtime_error>("MPT source_currencies can't have issuer");
jv[jss::mpt_issuance_id] = to_string(asset);
}
},
srcAsset.value());
}
Json::Value
rpf(jtx::Account const& src,
jtx::Account const& dst,
STAmount const& dstAmount,
std::optional<STAmount> const& sendMax,
std::optional<PathAsset> const& srcAsset,
std::optional<AccountID> const& srcIssuer)
{
Json::Value jv = Json::objectValue;
jv[jss::command] = "ripple_path_find";
jv[jss::source_account] = toBase58(src);
jv[jss::destination_account] = toBase58(dst);
jv[jss::destination_amount] = dstAmount.getJson(JsonOptions::none);
if (sendMax)
jv[jss::send_max] = sendMax->getJson(JsonOptions::none);
if (srcAsset)
{
auto& sc = jv[jss::source_currencies] = Json::arrayValue;
Json::Value j = Json::objectValue;
addSourceAsset(j, *srcAsset, srcIssuer);
sc.append(j);
}
return jv;
}
jtx::Env
pathTestEnv(beast::unit_test::suite& suite)
{
// 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(suite, envconfig([](std::unique_ptr<Config> cfg) {
cfg->PATH_SEARCH_OLD = 7;
cfg->PATH_SEARCH = 7;
cfg->PATH_SEARCH_MAX = 10;
return cfg;
}));
}
Json::Value
find_paths_request(
jtx::Env& env,
jtx::Account const& src,
jtx::Account const& dst,
STAmount const& saDstAmount,
std::optional<STAmount> const& saSendMax,
std::optional<PathAsset> const& srcAsset,
std::optional<AccountID> const& srcIssuer,
std::optional<uint256> const& domain)
{
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 (srcAsset)
{
auto& sc = params[jss::source_currencies] = Json::arrayValue;
Json::Value j = Json::objectValue;
addSourceAsset(j, *srcAsset, srcIssuer);
sc.append(j);
}
if (domain)
params[jss::domain] = to_string(*domain);
Json::Value result;
gate g;
app.getJobQueue().postCoro(jtCLIENT, "RPC-Client", [&](auto const& coro) {
context.params = std::move(params);
context.coro = coro;
RPC::doCommand(context, result);
g.signal();
});
using namespace std::chrono_literals;
using namespace beast::unit_test;
g.wait_for(5s);
return result;
}
std::tuple<STPathSet, STAmount, STAmount>
find_paths(
jtx::Env& env,
jtx::Account const& src,
jtx::Account const& dst,
STAmount const& saDstAmount,
std::optional<STAmount> const& saSendMax,
std::optional<PathAsset> const& srcAsset,
std::optional<AccountID> const& srcIssuer,
std::optional<uint256> const& domain)
{
Json::Value result =
find_paths_request(env, src, dst, saDstAmount, saSendMax, srcAsset, srcIssuer, domain);
if (result.isMember(jss::error))
return std::make_tuple(STPathSet{}, STAmount{}, STAmount{});
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));
}
std::tuple<STPathSet, STAmount, STAmount>
find_paths_by_element(
jtx::Env& env,
jtx::Account const& src,
jtx::Account const& dst,
STAmount const& saDstAmount,
std::optional<STAmount> const& saSendMax,
std::optional<STPathElement> const& srcElement,
std::optional<AccountID> const& srcIssuer,
std::optional<uint256> const& domain)
{
return find_paths(
env, src, dst, saDstAmount, saSendMax, srcElement->getPathAsset(), srcIssuer, domain);
}
/******************************************************************************/
@@ -87,9 +296,9 @@ xrpMinusFee(Env const& env, std::int64_t xrpAmount)
[[nodiscard]] bool
expectHolding(Env& env, AccountID const& account, STAmount const& value, bool defaultLimits)
{
if (auto const sle = env.le(keylet::line(account, value.issue())))
if (auto const sle = env.le(keylet::line(account, value.get<Issue>())))
{
Issue const issue = value.issue();
Issue const issue = value.get<Issue>();
bool const accountLow = account < issue.account;
bool expectDefaultTrustLine = true;
@@ -98,15 +307,15 @@ expectHolding(Env& env, AccountID const& account, STAmount const& value, bool de
STAmount low{issue};
STAmount high{issue};
low.setIssuer(accountLow ? account : issue.account);
high.setIssuer(accountLow ? issue.account : account);
low.get<Issue>().account = accountLow ? account : issue.account;
high.get<Issue>().account = accountLow ? issue.account : account;
expectDefaultTrustLine =
sle->getFieldAmount(sfLowLimit) == low && sle->getFieldAmount(sfHighLimit) == high;
}
auto amount = sle->getFieldAmount(sfBalance);
amount.setIssuer(value.issue().account);
amount.get<Issue>().account = value.getIssuer();
if (!accountLow)
amount.negate();
return amount == value && expectDefaultTrustLine;
@@ -134,6 +343,14 @@ expectHolding(Env& env, AccountID const& account, None const& value)
value.asset.value());
}
[[nodiscard]] bool
expectMPT(Env& env, AccountID const& account, STAmount const& value)
{
auto const mptIssuanceID = keylet::mptIssuance(value.asset().get<MPTIssue>());
auto const mptToken = env.le(keylet::mptoken(mptIssuanceID.key, account));
return mptToken && (*mptToken)[sfMPTAmount] == value.mpt().value();
}
[[nodiscard]] bool
expectOffers(
Env& env,
@@ -157,7 +374,7 @@ expectOffers(
}
return true;
});
return size == cnt && matched == toMatch.size();
return size == cnt && ((toMatch.empty() && size != 0) || (matched == toMatch.size()));
}
Json::Value
@@ -185,6 +402,34 @@ ledgerEntryState(
return env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
}
Json::Value
ledgerEntryOffer(jtx::Env& env, jtx::Account const& acct, std::uint32_t offer_seq)
{
Json::Value jvParams;
jvParams[jss::offer][jss::account] = acct.human();
jvParams[jss::offer][jss::seq] = offer_seq;
return env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
}
Json::Value
ledgerEntryMPT(jtx::Env& env, jtx::Account const& acct, MPTID const& mptID)
{
Json::Value jvParams;
jvParams[jss::mptoken][jss::account] = acct.human();
jvParams[jss::mptoken][jss::mpt_issuance_id] = to_string(mptID);
return env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
}
Json::Value
getBookOffers(jtx::Env& env, Asset const& taker_pays, Asset const& taker_gets)
{
Json::Value jvbp;
jvbp[jss::ledger_index] = "current";
taker_pays.setJson(jvbp[jss::taker_pays]);
taker_gets.setJson(jvbp[jss::taker_gets]);
return env.rpc("json", "book_offers", to_string(jvbp))[jss::result];
}
Json::Value
accountBalance(Env& env, Account const& acct)
{
@@ -312,22 +557,133 @@ n_offers(Env& env, std::size_t n, Account const& account, STAmount const& in, ST
// Currency path element
STPathElement
cpe(Currency const& c)
cpe(PathAsset const& pa)
{
return STPathElement(STPathElement::typeCurrency, xrpAccount(), c, xrpAccount());
return pa.visit(
[](Currency const& currency) {
return STPathElement(STPathElement::typeCurrency, xrpAccount(), currency, xrpAccount());
},
[](MPTID const& mpt) {
return STPathElement(STPathElement::typeMPT, xrpAccount(), mpt, xrpAccount());
});
};
// All path element
STPathElement
allPathElements(AccountID const& a, Issue const& iss)
allPathElements(AccountID const& a, Asset const& asset)
{
return STPathElement(
STPathElement::typeAccount | STPathElement::typeCurrency | STPathElement::typeIssuer,
a,
iss.currency,
iss.account);
return STPathElement(a, asset, asset.getIssuer());
};
STPathElement
ipe(Asset const& asset)
{
return asset.visit(
[](Issue const& issue) {
return STPathElement(
STPathElement::typeCurrency | STPathElement::typeIssuer,
xrpAccount(),
issue.currency,
issue.account);
},
[](MPTIssue const& issue) {
return STPathElement(
STPathElement::typeMPT | STPathElement::typeIssuer,
xrpAccount(),
issue.getMptID(),
issue.getIssuer());
});
};
// Issuer path element
STPathElement
iape(AccountID const& account)
{
return STPathElement(STPathElement::typeIssuer, xrpAccount(), xrpCurrency(), account);
};
// Account path element
STPathElement
ape(AccountID const& a)
{
return STPathElement(STPathElement::typeAccount, a, xrpCurrency(), xrpAccount());
};
bool
equal(std::unique_ptr<xrpl::Step> const& s1, DirectStepInfo const& dsi)
{
if (!s1)
return false;
return test::directStepEqual(*s1, dsi.src, dsi.dst, dsi.currency);
}
bool
equal(std::unique_ptr<xrpl::Step> const& s1, MPTEndpointStepInfo const& dsi)
{
if (!s1)
return false;
return test::mptEndpointStepEqual(*s1, dsi.src, dsi.dst, dsi.mptid);
}
bool
equal(std::unique_ptr<xrpl::Step> const& s1, XRPEndpointStepInfo const& xrpStepInfo)
{
if (!s1)
return false;
return test::xrpEndpointStepEqual(*s1, xrpStepInfo.acc);
}
bool
equal(std::unique_ptr<xrpl::Step> const& s1, xrpl::Book const& bsi)
{
if (!s1)
return false;
return bookStepEqual(*s1, bsi);
}
namespace detail {
IOU
issueHelperIOU(IssuerArgs const& args)
{
auto const iou = args.issuer[args.token];
if (args.transferFee != 0)
{
auto const tfee = 1. + (static_cast<double>(args.transferFee) / 100'000);
args.env(rate(args.issuer, tfee));
}
for (auto const& account : args.holders)
{
args.env(trust(account, iou(args.limit.value_or(1'000))));
}
return iou;
}
MPT
issueHelperMPT(IssuerArgs const& args)
{
using namespace jtx;
if (args.limit)
{
MPT const mpt = MPTTester(
{.env = args.env,
.issuer = args.issuer,
.holders = args.holders,
.transferFee = args.transferFee,
.maxAmt = args.limit});
return mpt;
}
MPT const mpt = MPTTester(
{.env = args.env,
.issuer = args.issuer,
.holders = args.holders,
.transferFee = args.transferFee});
return mpt;
}
} // namespace detail
/* LoanBroker */
/******************************************************************************/