Skip to content

Commit

Permalink
Replace MPTID with PathFindMPT in AssetCache.
Browse files Browse the repository at this point in the history
Make line direction non-optional in getPathsOut().
Add Path unit-tests.
  • Loading branch information
gregtatcam committed Jan 10, 2025
1 parent 860a45c commit cd155e1
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 36 deletions.
134 changes: 128 additions & 6 deletions src/test/app/MPToken_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3336,8 +3336,9 @@ class MPToken_test : public beast::unit_test::suite
find_paths(env, carol, dan, MPT(-1));
BEAST_EXPECT(srcAmt == MPT1(100));
BEAST_EXPECT(dstAmt == MPT(100));
// This path is consistent with IOU1/gw / IOU/gw path -
// [gw1, IOU/gw], except for gw1. This is due to no MPT rippling
// Has this been IOU path, it would have started with gw, but not
// MPT. This is due to MPT not being bidirectional unlike from
// trustline.
BEAST_EXPECT(same(pathSet, stpath(IPE(mpt.issuanceID()))));
}

Expand Down Expand Up @@ -3419,13 +3420,134 @@ class MPToken_test : public beast::unit_test::suite
find_paths(env, carol, dan, MPT1(-1));
BEAST_EXPECT(srcAmt == MPT(100));
BEAST_EXPECT(dstAmt == MPT1(100));
// This path is consistent with IOU/gw / IOU/gw2 -
// IOU/gw2 / IOU1/gw1 path -
// [gw, IOU2/gw2, IOU1/gw1], except for gw.
// This is due to no MPT rippling
// Has this been IOU path, it would have started with gw, but not
// MPT. This is due to MPT not being bidirectional unlike from
// trustline.
BEAST_EXPECT(
same(pathSet, stpath(IPE(USD2), IPE(mpt1.issuanceID()))));
}

// Cross-asset payment via offers (two steps)
// Start/End with mpt/mp2 and book steps in the middle
// offers are MPT/MPT
{
Env env = pathTestEnv(*this);
Account const gw2{"gw2"};
env.fund(XRP(1'000), gw, gw1, gw2, alice, bob, carol, dan);

MPTTester mpt(env, gw, {.holders = {alice, carol}, .fund = false});
mpt.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.authorize({.account = carol});
mpt.pay(gw, carol, 200);

MPTTester mpt1(env, gw1, {.holders = {bob, alice}, .fund = false});
mpt1.create(
{.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT1 = mpt1["MPT1"];
mpt1.authorize({.account = alice});
mpt1.pay(gw1, alice, 200);
mpt1.authorize({.account = bob});

MPTTester mpt2(env, gw2, {.holders = {bob, dan}, .fund = false});
mpt2.create(
{.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT2 = mpt2["MPT2"];
mpt2.authorize({.account = bob});
mpt2.pay(gw2, bob, 200);
mpt2.authorize({.account = dan});

env(offer(alice, MPT(100), MPT1(100)));
env(offer(bob, MPT1(100), MPT2(100)));
env.close();

auto const [pathSet, srcAmt, dstAmt] =
find_paths(env, carol, dan, MPT2(-1));
BEAST_EXPECT(srcAmt == MPT(100));
BEAST_EXPECT(dstAmt == MPT2(100));
// Has this been IOU path, it would have started with gw, but not
// MPT. This is due to MPT not being bidirectional unlike from
// trustline
BEAST_EXPECT(same(
pathSet,
stpath(IPE(mpt1.issuanceID()), IPE(mpt2.issuanceID()))));
}

// Cross-asset payment via offers (three steps)
// Start/End with mpt/mp3 and book steps in the middle
// offers are MPT/MPT
{
Env env = pathTestEnv(*this);
Account const gw2{"gw2"};
Account const gw3{"gw3"};
Account const greg{"greg"};
env.fund(
XRP(1'000), gw, gw1, gw2, gw3, alice, bob, carol, greg, dan);

MPTTester mpt(env, gw, {.holders = {alice, carol}, .fund = false});
mpt.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.authorize({.account = carol});
mpt.pay(gw, carol, 200);

MPTTester mpt1(env, gw1, {.holders = {bob, alice}, .fund = false});
mpt1.create(
{.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT1 = mpt1["MPT1"];
mpt1.authorize({.account = alice});
mpt1.pay(gw1, alice, 200);
mpt1.authorize({.account = bob});

MPTTester mpt2(env, gw2, {.holders = {bob, greg}, .fund = false});
mpt2.create(
{.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT2 = mpt2["MPT2"];
mpt2.authorize({.account = bob});
mpt2.pay(gw2, bob, 200);
mpt2.authorize({.account = greg});

MPTTester mpt3(env, gw3, {.holders = {greg, dan}, .fund = false});
mpt3.create(
{.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT3 = mpt3["MPT3"];
mpt3.authorize({.account = greg});
mpt3.pay(gw3, greg, 200);
mpt3.authorize({.account = dan});

env(offer(alice, MPT(100), MPT1(100)));
env(offer(bob, MPT1(100), MPT2(100)));
env(offer(greg, MPT2(100), MPT3(100)));
env.close();

auto const [pathSet, srcAmt, dstAmt] =
find_paths(env, carol, dan, MPT3(-1));
BEAST_EXPECT(srcAmt == MPT(100));
BEAST_EXPECT(dstAmt == MPT3(100));
// Has this been IOU path, it would have started with gw, but not
// MPT. This is due to MPT not being bidirectional unlike from
// trustline
BEAST_EXPECT(same(
pathSet,
stpath(
IPE(mpt1.issuanceID()),
IPE(mpt2.issuanceID()),
IPE(mpt3.issuanceID()))));

// TODO, path finding doesn't work, but the payment does
// Note that it doesn't work for all IOU either
env(pay(carol, dan, MPT3(100)),
path(~MPT1, ~MPT2, ~MPT3),
sendmax(MPT(100)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
}

void
Expand Down
2 changes: 1 addition & 1 deletion src/test/jtx/TestHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ rpf(jtx::Account const& src,
jtx::Account const& dst,
STAmount const& dstAmount,
std::optional<STAmount> const& sendMax = std::nullopt,
std::optional<Currency> const& srcCurrency = std::nullopt);
std::optional<PathAsset> const& srcAsset = std::nullopt);

jtx::Env
pathTestEnv(beast::unit_test::suite& suite);
Expand Down
9 changes: 6 additions & 3 deletions src/test/jtx/impl/TestHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ rpf(jtx::Account const& src,
jtx::Account const& dst,
STAmount const& dstAmount,
std::optional<STAmount> const& sendMax,
std::optional<Currency> const& srcCurrency)
std::optional<PathAsset> const& srcAsset)
{
Json::Value jv = Json::objectValue;
jv[jss::command] = "ripple_path_find";
Expand All @@ -113,11 +113,14 @@ rpf(jtx::Account const& src,
jv[jss::destination_amount] = dstAmount.getJson(JsonOptions::none);
if (sendMax)
jv[jss::send_max] = sendMax->getJson(JsonOptions::none);
if (srcCurrency)
if (srcAsset)
{
auto& sc = jv[jss::source_currencies] = Json::arrayValue;
Json::Value j = Json::objectValue;
j[jss::currency] = to_string(srcCurrency.value());
if (srcAsset->holds<Currency>())
j[jss::currency] = to_string(srcAsset->get<Currency>());
else
j[jss::mpt_issuance_id] = to_string(srcAsset->get<MPTID>());
sc.append(j);
}

Expand Down
33 changes: 28 additions & 5 deletions src/xrpld/app/paths/AssetCache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,21 +129,43 @@ AssetCache::getRippleLines(AccountID const& accountID, LineDirection direction)
return it->second;
}

std::shared_ptr<std::vector<MPTID>> const&
std::shared_ptr<std::vector<PathFindMPT>> const&
AssetCache::getMPTs(const ripple::AccountID& account)
{
std::lock_guard sl(mLock);

if (auto it = mpts_.find(account); it != mpts_.end())
return it->second;

std::vector<MPTID> mpts;
std::vector<PathFindMPT> mpts;
// Get issued/authorized tokens
forEachItem(*ledger_, account, [&](std::shared_ptr<SLE const> const& sle) {
if (sle->getType() == ltMPTOKEN_ISSUANCE)
mpts.push_back(makeMptID(sle->getFieldU32(sfSequence), account));
{
auto const mptID = makeMptID(sle->getFieldU32(sfSequence), account);
auto const maxAmount =
(*sle)[~sfMaximumAmount].value_or(maxMPTokenAmount);
bool const maxedOut = sle->at(sfOutstandingAmount) == maxAmount;
mpts.emplace_back(mptID, false, maxedOut);
}
else if (sle->getType() == ltMPTOKEN)
mpts.push_back(sle->getFieldH192(sfMPTokenIssuanceID));
{
auto const mptID = sle->getFieldH192(sfMPTokenIssuanceID);
bool const zeroBalance = sle->at(sfMPTAmount) == 0;
bool const maxedOut = [&] {
if (auto const sleIssuance =
ledger_->read(keylet::mptIssuance(mptID)))
{
auto const maxAmount =
(*sleIssuance)[~sfMaximumAmount].value_or(
maxMPTokenAmount);
return sleIssuance->at(sfOutstandingAmount) == maxAmount;
}
return true;
}();

mpts.emplace_back(mptID, zeroBalance, maxedOut);
}
});

totalMPTCount_ += mpts.size();
Expand All @@ -152,7 +174,8 @@ AssetCache::getMPTs(const ripple::AccountID& account)
mpts_.emplace(account, nullptr);
else
mpts_.emplace(
account, std::make_shared<std::vector<MPTID>>(std::move(mpts)));
account,
std::make_shared<std::vector<PathFindMPT>>(std::move(mpts)));

return mpts_[account];
}
Expand Down
5 changes: 3 additions & 2 deletions src/xrpld/app/paths/AssetCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#define RIPPLE_APP_PATHS_RIPPLELINECACHE_H_INCLUDED

#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/paths/MPT.h>
#include <xrpld/app/paths/TrustLine.h>
#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/hardened_hash.h>
Expand Down Expand Up @@ -62,7 +63,7 @@ class AssetCache final : public CountedObject<AssetCache>
std::shared_ptr<std::vector<PathFindTrustLine>>
getRippleLines(AccountID const& accountID, LineDirection direction);

std::shared_ptr<std::vector<MPTID>> const&
std::shared_ptr<std::vector<PathFindMPT>> const&
getMPTs(AccountID const& account);

private:
Expand Down Expand Up @@ -128,7 +129,7 @@ class AssetCache final : public CountedObject<AssetCache>
AccountKey::Hash>
lines_;
std::size_t totalLineCount_ = 0;
hash_map<AccountID, std::shared_ptr<std::vector<MPTID>>> mpts_;
hash_map<AccountID, std::shared_ptr<std::vector<PathFindMPT>>> mpts_;
std::size_t totalMPTCount_ = 0;
};

Expand Down
68 changes: 68 additions & 0 deletions src/xrpld/app/paths/MPT.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================

#ifndef RIPPLE_APP_PATHS_MPT_H_INCLUDED
#define RIPPLE_APP_PATHS_MPT_H_INCLUDED

#include <xrpl/protocol/MPTIssue.h>

namespace ripple {

class PathFindMPT final
{
private:
MPTID const mptID_;
// If true then holder's balance is 0, always false for issuer
bool const zeroBalance_;
// OutstandingAmount is equal to MaximumAmount
bool const maxedOut_;

public:
PathFindMPT(MPTID const& mptID)
: mptID_(mptID), zeroBalance_(false), maxedOut_(false)
{
}
PathFindMPT(MPTID const& mptID, bool zeroBalance, bool maxedOut)
: mptID_(mptID), zeroBalance_(zeroBalance), maxedOut_(maxedOut)
{
}
operator MPTID const&() const
{
return mptID_;
}
MPTID const&
getMptID() const
{
return mptID_;
}
bool
isZeroBalance() const
{
return zeroBalance_;
}
bool
isMaxedOut() const
{
return maxedOut_;
}
};

} // namespace ripple

#endif // RIPPLE_APP_PATHS_MPT_H_INCLUDED
6 changes: 3 additions & 3 deletions src/xrpld/app/paths/PathRequest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ PathRequest::isValid(std::shared_ptr<AssetCache> const& crCache)
if (auto mpts = crCache->getMPTs(*raDstAccount))
{
for (auto const& mpt : *mpts)
jvDestCur.append(to_string(mpt));
jvDestCur.append(to_string(mpt.getMptID()));
}

jvStatus[jss::destination_tag] =
Expand Down Expand Up @@ -564,7 +564,7 @@ PathRequest::findPaths(
if (sourceAssets.size() >= RPC::Tuning::max_auto_src_cur)
return false;
for (auto const& mpt : *mpts)
sourceAssets.insert(mpt);
sourceAssets.insert(MPTIssue{mpt});
}
}

Expand Down Expand Up @@ -737,7 +737,7 @@ PathRequest::doUpdate(
if (auto mpts = cache->getMPTs(*raDstAccount))
{
for (auto const& mpt : *mpts)
destAssets.append(to_string(mpt));
destAssets.append(to_string(mpt.getMptID()));
}
}

Expand Down
Loading

0 comments on commit cd155e1

Please sign in to comment.