mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-23 07:10:53 +00:00
feat: Add MPT support to DEX (#5285)
This commit is contained in:
committed by
GitHub
parent
6d1a5be8d2
commit
dfcad69155
@@ -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 */
|
||||
/******************************************************************************/
|
||||
|
||||
|
||||
Reference in New Issue
Block a user