1#include <xrpld/rpc/detail/Pathfinder.h>
3#include <xrpld/app/main/Application.h>
4#include <xrpld/rpc/detail/AssetCache.h>
5#include <xrpld/rpc/detail/PathfinderUtils.h>
6#include <xrpld/rpc/detail/TrustLine.h>
8#include <xrpl/basics/Log.h>
9#include <xrpl/basics/base_uint.h>
10#include <xrpl/basics/join.h>
11#include <xrpl/beast/utility/Zero.h>
12#include <xrpl/beast/utility/instrumentation.h>
13#include <xrpl/core/Job.h>
14#include <xrpl/core/JobQueue.h>
15#include <xrpl/json/to_string.h>
16#include <xrpl/ledger/ApplyView.h>
17#include <xrpl/ledger/OrderBookDB.h>
18#include <xrpl/ledger/PaymentSandbox.h>
19#include <xrpl/ledger/helpers/MPTokenHelpers.h>
20#include <xrpl/protocol/AccountID.h>
21#include <xrpl/protocol/Asset.h>
22#include <xrpl/protocol/Indexes.h>
23#include <xrpl/protocol/LedgerFormats.h>
24#include <xrpl/protocol/MPTIssue.h>
25#include <xrpl/protocol/PathAsset.h>
26#include <xrpl/protocol/SField.h>
27#include <xrpl/protocol/STAmount.h>
28#include <xrpl/protocol/STPathSet.h>
29#include <xrpl/protocol/TER.h>
30#include <xrpl/protocol/UintTypes.h>
31#include <xrpl/tx/paths/RippleCalc.h>
49 return os << static_cast<int>(t);
54 return os << static_cast<int>(t);
96constexpr std::size_t kPathfinderMaxCompletePaths = 1000;
98struct AccountCandidate
103 static int const kHighPriority = 10000;
107compareAccountCandidate(
109 AccountCandidate
const& first,
110 AccountCandidate
const& second)
113 if (first.priority != second.priority)
114 return first.priority > second.priority;
117 if (first.account != second.account)
118 return first.account > second.account;
122 return (first.priority ^ seq) < (second.priority ^ seq);
125using AccountCandidates = std::vector<AccountCandidate>;
133using CostedPathList = std::vector<CostedPath>;
135using PathTable = std::map<Pathfinder::PaymentType, CostedPathList>;
142using PathCostList = std::vector<PathCost>;
151 for (
auto const& node : type)
182smallestUsefulAmount(
STAmount const& amount,
int maxPaths)
190 std::optional<AccountID>
const& srcIssuer,
193 return pathAsset.visit(
198 [](
MPTID const& mpt) {
return STAmount(mpt, 1u, 0,
true); });
204 return pathAsset.
visit(
223 ,
effectiveDst_(
isXRP(saDstAmount.getIssuer()) ? uDstAccount : saDstAmount.getIssuer())
227 ,
srcAmount_(srcAmount.value_or(amountFromPathAsset(uSrcPathAsset, uSrcIssuer, uSrcAccount)))
233 ,
j_(app.getJournal(
"Pathfinder"))
237 "xrpl::Pathfinder::Pathfinder : valid inputs");
243 JLOG(
j_.trace()) <<
"findPaths start";
247 JLOG(
j_.debug()) <<
"Destination amount was zero.";
259 JLOG(
j_.debug()) <<
"Tried to send to same issuer";
275 auto issuer = currencyIsXRP ?
AccountID() : account;
278 JLOG(
j_.trace()) <<
"findPaths>"
281 <<
" srcPathAsset_=" <<
srcPathAsset_ <<
" srcIssuer_=" << issuerString;
285 JLOG(
j_.debug()) <<
"findPaths< no ledger";
295 JLOG(
j_.debug()) <<
"invalid source account";
301 JLOG(
j_.debug()) <<
"Non-existent gateway";
311 JLOG(
j_.debug()) <<
"New account not being funded in XRP ";
318 JLOG(
j_.debug()) <<
"New account not getting enough funding: " <<
dstAmount_ <<
" < "
327 if (bSrcXrp && bDstXrp)
330 JLOG(
j_.debug()) <<
"XRP to XRP payment";
336 JLOG(
j_.debug()) <<
"XRP to non-XRP payment";
342 JLOG(
j_.debug()) <<
"non-XRP to XRP payment";
348 JLOG(
j_.debug()) <<
"non-XRP to non-XRP - same currency";
354 JLOG(
j_.debug()) <<
"non-XRP to non-XRP - cross currency";
359 for (
auto const& costedPath : gPathTable[paymentType])
361 if (continueCallback && !continueCallback())
364 if (costedPath.searchLevel <= searchLevel)
366 JLOG(
j_.trace()) <<
"findPaths trying payment type " << paymentType;
387 uint64_t& qualityOut)
const
417 qualityOut =
getRate(rc.actualAmountOut, rc.actualAmountIn);
418 amountOut = rc.actualAmountOut;
437 amountOut += rc.actualAmountOut;
444 JLOG(
j_.info()) <<
"checkpath: exception (" << e.
what() <<
") "
475 JLOG(
j_.debug()) <<
"Default path contributes: " << rc.actualAmountIn;
480 JLOG(
j_.debug()) <<
"Default path fails: " <<
transToken(rc.result());
485 JLOG(
j_.debug()) <<
"Default path causes exception";
507 return path.size() == 1;
517 for (
auto it =
path.begin() + 1; it !=
path.end(); ++it)
532 JLOG(
j_.trace()) <<
"rankPaths with " << paths.
size() <<
" candidates, and " << maxPaths
537 auto const saMinDstAmount = [&]() ->
STAmount {
541 return smallestUsefulAmount(
dstAmount_, maxPaths);
549 for (
int i = 0; i < paths.
size(); ++i)
551 if (continueCallback && !continueCallback())
553 auto const& currentPath = paths[i];
554 if (!currentPath.empty())
557 uint64_t uQuality = 0;
558 auto const resultCode =
562 JLOG(
j_.debug()) <<
"findPaths: dropping : " <<
transToken(resultCode) <<
": "
567 JLOG(
j_.debug()) <<
"findPaths: quality: " << uQuality <<
": "
571 {.quality = uQuality,
572 .length = currentPath.size(),
573 .liquidity = liquidity,
606 STPath& fullLiquidityPath,
611 JLOG(
j_.debug()) <<
"findPaths: " <<
completePaths_.size() <<
" paths and " << extraPaths.
size()
618 fullLiquidityPath.
empty(),
"xrpl::Pathfinder::getBestPaths : first empty path result");
622 rankPaths(maxPaths, extraPaths, extraPathRanks, continueCallback);
632 auto extraPathsIterator = extraPathRanks.
begin();
634 while (pathsIterator !=
pathRanks_.end() || extraPathsIterator != extraPathRanks.
end())
636 if (continueCallback && !continueCallback())
638 bool usePath =
false;
639 bool useExtraPath =
false;
645 else if (extraPathsIterator == extraPathRanks.
end())
649 else if (extraPathsIterator->quality != pathsIterator->quality)
652 useExtraPath = extraPathsIterator->quality < pathsIterator->quality;
653 usePath = !useExtraPath;
655 else if (extraPathsIterator->liquidity != pathsIterator->liquidity)
658 useExtraPath = extraPathsIterator->liquidity > pathsIterator->liquidity;
659 usePath = !useExtraPath;
668 auto& pathRank = usePath ? *pathsIterator : *extraPathsIterator;
670 auto const&
path = usePath ?
completePaths_[pathRank.index] : extraPaths[pathRank.index];
673 ++extraPathsIterator;
678 auto iPathsLeft = maxPaths - bestPaths.
size();
679 if (iPathsLeft <= 0 && !fullLiquidityPath.
empty())
685 UNREACHABLE(
"xrpl::Pathfinder::getBestPaths : path not found");
690 bool startsWithIssuer =
false;
692 if (!issuerIsSender && usePath)
700 startsWithIssuer =
true;
703 if (iPathsLeft > 1 || (iPathsLeft > 0 && pathRank.liquidity >= remaining))
707 remaining -= pathRank.liquidity;
710 else if (iPathsLeft == 0 && pathRank.liquidity >=
dstAmount_ && fullLiquidityPath.
empty())
714 JLOG(
j_.debug()) <<
"Found extra full path: "
719 JLOG(
j_.debug()) <<
"Skipping a non-filling path: "
727 fullLiquidityPath.
empty(),
"xrpl::Pathfinder::getBestPaths : second empty path result");
728 JLOG(
j_.info()) <<
"Paths could not send " << remaining <<
" of " <<
dstAmount_;
744 return matchingAsset && matchingAccount;
756 Asset const asset = assetFromPathAsset(pathAsset, account);
769 auto const aFlags = sleAccount->getFieldU32(sfFlags);
770 bool const bAuthRequired = [&]() {
772 return (aFlags & lsfRequireAuth) != 0;
775 bool const bFrozen = [&]() {
777 return (aFlags & lsfGlobalFreeze) != 0;
785 count =
app_.getOrderBookDB().getBookSize(asset,
domain_);
789 if (
auto const lines =
rLCache_->getRippleLines(account, direction))
791 for (
auto const& rspEntry : *lines)
796 (!rspEntry.getLimitPeer() ||
797 -rspEntry.getBalance() >= rspEntry.getLimitPeer() ||
798 (bAuthRequired && !rspEntry.getAuth())))
800 if (isDstAsset && dstAccount == rspEntry.getAccountIDPeer())
805 if (rspEntry.getNoRipplePeer())
807 if (rspEntry.getFreezePeer())
814 if (
auto const mpts =
rLCache_->getMPTs(account))
816 for (
auto const& mpt : *mpts)
818 if (pathAsset.
get<
MPTID>() != mpt.getMptID() || !mpt.canSend(account) ||
844 JLOG(
j_.debug()) <<
"addLink< on " << currentPaths.
size() <<
" source(s), flags=" << addFlags;
845 for (
auto const&
path : currentPaths)
847 if (continueCallback && !continueCallback())
849 addLink(
path, incompletePaths, addFlags, continueCallback);
860 auto it =
paths_.find(pathType);
865 if (pathType.
empty() || (continueCallback && !continueCallback()))
867 static auto const kEmptyPath =
PathType{};
879 JLOG(
j_.debug()) <<
"getPaths< adding onto '" << pathTypeToString(parentPathType)
880 <<
"' to get '" << pathTypeToString(pathType) <<
"'";
885 auto nodeType = pathType.
back();
890 XRPL_ASSERT(pathsOut.
empty(),
"xrpl::Pathfinder::addPathsForType : empty paths");
920 JLOG(
j_.debug()) << (
completePaths_.size() - initialSize) <<
" complete paths added";
923 JLOG(
j_.debug()) <<
"getPaths> " << pathsOut.
size() <<
" partial paths found";
935 auto const flag((toAccount > fromAccount) ? lsfHighNoRipple : lsfLowNoRipple);
937 return sleRipple && sleRipple->isFlag(flag);
946 if (currentPath.
empty())
957 auto const& fromAccount =
965 STPath const& currentPath,
971 auto const& uEndPathAsset = pathEnd.getPathAsset();
972 auto const& uEndIssuer = pathEnd.getIssuerID();
973 auto const& uEndAccount = pathEnd.getAccountID();
974 bool const bOnXRP =
isXRP(uEndPathAsset);
981 JLOG(
j_.trace()) <<
"addLink< flags=" << addFlags <<
" onXRP=" << bOnXRP
992 JLOG(
j_.trace()) <<
"complete path found ax: "
1004 bool const bRequireAuth(sleEnd->isFlag(lsfRequireAuth));
1005 bool const bIsEndAsset(uEndPathAsset ==
dstAmount_.asset());
1007 bool const bDestOnly((addFlags &
kAfAcLast) != 0u);
1009 AccountCandidates candidates;
1012 candidates.reserve(assets.size());
1014 static constexpr bool kIsLine =
1016 static constexpr bool kIsMpt =
1019 for (
auto const& asset : assets)
1021 if (continueCallback && !continueCallback())
1023 auto const& acct = [&]()
constexpr {
1024 if constexpr (kIsLine)
1025 return asset.getAccountIDPeer();
1027 if constexpr (kIsMpt)
1031 if constexpr (kIsLine)
1032 return asset.getDirectionPeer();
1038 if (hasEffectiveDestination && (acct ==
dstAccount_))
1046 if (bDestOnly && !bToDestination)
1051 auto const correctAsset = [&]() {
1052 if constexpr (kIsLine)
1054 return uEndPathAsset.get<
Currency>() ==
1055 asset.getLimit().template
get<Issue>().currency;
1057 if constexpr (kIsMpt)
1059 return uEndPathAsset.get<
MPTID>() == asset.getMptID();
1062 auto checkAsset = [&]() {
1063 if constexpr (kIsLine)
1067 (!asset.getLimitPeer() ||
1068 -asset.getBalance() >= asset.getLimitPeer() ||
1069 (bRequireAuth && !asset.getAuth()))) ||
1070 (bIsNoRippleOut && asset.getNoRipple()));
1072 if constexpr (kIsMpt)
1077 return !asset.canSend(uEndAccount) ||
1082 if (correctAsset && !currentPath.
hasSeen(acct, uEndPathAsset, acct))
1097 if (!currentPath.
empty())
1100 <<
"complete path found ae: "
1105 else if (!bDestOnly)
1108 candidates.push_back({AccountCandidate::kHighPriority, acct});
1126 candidates.push_back({out, acct});
1132 uEndPathAsset.visit(
1134 if (
auto const lines =
rLCache_->getRippleLines(
1142 if (
auto const mpts =
rLCache_->getMPTs(uEndAccount))
1148 if (!candidates.empty())
1153 AccountCandidate
const& first, AccountCandidate
const& second) {
1154 return compareAccountCandidate(seq, first, second);
1157 int count = candidates.size();
1163 else if (count > 50)
1168 auto it = candidates.begin();
1169 while (count-- != 0)
1171 if (continueCallback && !continueCallback())
1176 incompletePaths.
assembleAdd(currentPath, pathElement);
1183 JLOG(
j_.warn()) <<
"Path ends on non-existent issuer";
1194 app_.getOrderBookDB().isBookToXRP(
1195 assetFromPathAsset(uEndPathAsset, uEndIssuer),
domain_))
1199 incompletePaths.
assembleAdd(currentPath, pathElement);
1204 bool const bDestOnly = (addFlags &
kAfObLast) != 0;
1205 auto books =
app_.getOrderBookDB().getBooksByTakerPays(
1206 assetFromPathAsset(uEndPathAsset, uEndIssuer),
domain_);
1207 JLOG(
j_.trace()) << books.size() <<
" books found from this currency/issuer";
1209 for (
auto const& book : books)
1211 if (continueCallback && !continueCallback())
1217 STPath newPath(currentPath);
1219 if (
isXRP(book.out))
1230 JLOG(
j_.trace()) <<
"complete path found bx: "
1236 [[maybe_unused]]
auto result = incompletePaths.
pushBack(newPath);
1237 XRPL_ASSERT(result,
"xrpl::Pathfinder::addLink : unique path");
1240 else if (!currentPath.
hasSeen(
1241 book.out.getIssuer(), book.out, book.out.getIssuer()))
1247 if ((newPath.
size() >= 2) && (newPath.
back().isAccount()) &&
1248 (newPath[newPath.
size() - 2].isOffer()))
1255 book.out.getIssuer());
1264 book.out.getIssuer());
1267 if (hasEffectiveDestination && book.out.getIssuer() ==
dstAccount_ &&
1277 JLOG(
j_.trace()) <<
"complete path found ba: "
1288 book.out.getIssuer(),
1290 book.out.getIssuer()));
1302makePath(
char const*
string)
1346 auto& list = gPathTable[type];
1347 XRPL_ASSERT(list.empty(),
"xrpl::fillPaths : empty paths");
1348 for (
auto& cost : costs)
1349 list.push_back({.searchLevel = cost.cost, .type = makePath(cost.path)});
1372 {{.cost = 1, .path =
"sfd"},
1373 {.cost = 3, .path =
"sfad"},
1374 {.cost = 5, .path =
"sfaad"},
1375 {.cost = 6, .path =
"sbfd"},
1376 {.cost = 8, .path =
"sbafd"},
1377 {.cost = 9, .path =
"sbfad"},
1378 {.cost = 10, .path =
"sbafad"}});
1382 {{.cost = 1, .path =
"sxd"},
1383 {.cost = 2, .path =
"saxd"},
1384 {.cost = 6, .path =
"saaxd"},
1385 {.cost = 7, .path =
"sbxd"},
1386 {.cost = 8, .path =
"sabxd"},
1387 {.cost = 9, .path =
"sabaxd"}});
1393 {.cost = 1, .path =
"sad"},
1394 {.cost = 1, .path =
"sfd"},
1395 {.cost = 4, .path =
"safd"},
1396 {.cost = 4, .path =
"sfad"},
1397 {.cost = 5, .path =
"saad"},
1398 {.cost = 5, .path =
"sbfd"},
1399 {.cost = 6, .path =
"sxfad"},
1400 {.cost = 6, .path =
"safad"},
1401 {.cost = 6, .path =
"saxfd"},
1403 {.cost = 6, .path =
"saxfad"},
1404 {.cost = 6, .path =
"sabfd"},
1405 {.cost = 7, .path =
"saaad"},
1412 {.cost = 1, .path =
"sfad"},
1413 {.cost = 1, .path =
"safd"},
1414 {.cost = 3, .path =
"safad"},
1415 {.cost = 4, .path =
"sxfd"},
1416 {.cost = 5, .path =
"saxfd"},
1417 {.cost = 5, .path =
"sxfad"},
1418 {.cost = 5, .path =
"sbfd"},
1419 {.cost = 6, .path =
"saxfad"},
1420 {.cost = 6, .path =
"sabfd"},
1421 {.cost = 7, .path =
"saafd"},
1422 {.cost = 8, .path =
"saafad"},
1423 {.cost = 9, .path =
"safaad"},
constexpr auto visit(Visitors &&... visitors) const -> decltype(auto)
constexpr TIss const & get() const
AccountID const & getIssuer() const
A currency issued by an account.
constexpr bool isXRP() const
constexpr bool holds() const
static std::uint32_t const kAfObLast
void addLinks(STPathSet const ¤tPaths, STPathSet &incompletePaths, int addFlags, std::function< bool(void)> const &continueCallback)
void rankPaths(int maxPaths, STPathSet const &paths, std::vector< PathRank > &rankedPaths, std::function< bool(void)> const &continueCallback)
TER getPathLiquidity(STPath const &path, STAmount const &minDstAmount, STAmount &amountOut, uint64_t &qualityOut) const
bool issueMatchesOrigin(Asset const &)
static std::uint32_t const kAfAddAccounts
STPathSet getBestPaths(int maxPaths, STPath &fullLiquidityPath, STPathSet const &extraPaths, AccountID const &srcIssuer, std::function< bool(void)> const &continueCallback={})
void addLink(STPath const ¤tPath, STPathSet &incompletePaths, int addFlags, std::function< bool(void)> const &continueCallback)
std::vector< NodeType > PathType
std::unique_ptr< LoadEvent > loadEvent_
HashMap< Asset, int > pathsOutCountMap_
static std::uint32_t const kAfObXrp
std::optional< AccountID > srcIssuer_
std::map< PathType, STPathSet > paths_
static std::uint32_t const kAfAcLast
std::shared_ptr< AssetCache > rLCache_
STPathSet & addPathsForType(PathType const &type, std::function< bool(void)> const &continueCallback)
Pathfinder(std::shared_ptr< AssetCache > const &cache, AccountID const &srcAccount, AccountID const &dstAccount, PathAsset const &uSrcPathAsset, std::optional< AccountID > const &uSrcIssuer, STAmount const &dstAmount, std::optional< STAmount > const &srcAmount, std::optional< UInt256 > const &domain, Application &app)
Construct a pathfinder without an issuer.
bool isNoRipple(AccountID const &fromAccount, AccountID const &toAccount, Currency const ¤cy)
bool findPaths(int searchLevel, std::function< bool(void)> const &continueCallback={})
bool isNoRippleOut(STPath const ¤tPath)
std::optional< UInt256 > domain_
static void initPathTable()
STAmount remainingAmount_
The amount remaining from srcAccount_ after the default liquidity has been removed.
static std::uint32_t const kAfAddBooks
int getPathsOut(PathAsset const &pathAsset, AccountID const &account, LineDirection direction, bool isDestPathAsset, AccountID const &dest, std::function< bool(void)> const &continueCallback)
std::vector< PathRank > pathRanks_
std::shared_ptr< ReadView const > ledger_
void computePathRanks(int maxPaths, std::function< bool(void)> const &continueCallback={})
Compute the rankings of the paths.
A wrapper which makes credits unavailable to balances.
Asset const & asset() const
std::uint32_t getNodeType() const
AccountID const & getAccountID() const
Currency const & getCurrency() const
bool assembleAdd(STPath const &base, STPathElement const &tail)
assembleAdd adds a path to the set by combining a base path and a tail element.
std::vector< STPath >::size_type size() const
json::Value getJson(JsonOptions) const override
bool pushBack(STPath const &e)
pushBack adds a path to the set.
std::vector< STPathElement >::size_type size() const
bool hasSeen(AccountID const &account, PathAsset const &asset, AccountID const &issuer) const
void pushBack(STPathElement const &e)
std::vector< STPathElement >::const_iterator end() const
void emplaceBack(Args &&... args)
std::vector< STPathElement >::const_reference back() const
json::Value getJson(JsonOptions) const
static Output rippleCalculate(PaymentSandbox &view, STAmount const &saMaxAmountReq, STAmount const &saDstAmountReq, AccountID const &uDstAccountID, AccountID const &uSrcAccountID, STPathSet const &spsPaths, std::optional< UInt256 > const &domainID, ServiceRegistry ®istry, Input const *const pInputs=nullptr)
Keylet account(AccountID const &id) noexcept
AccountID root.
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const ¤cy) noexcept
The index of a trust line for a given currency.
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
STAmount divide(STAmount const &amount, Rate const &rate)
STAmount convertAmount(STAmount const &amt, bool all)
bool isXRP(AccountID const &c)
bool isIndividualFrozen(ReadView const &view, AccountID const &account, MPTIssue const &mptIssue)
Returns true if account's MPToken for mptIssue carries the individual-lock flag (lsfMPTLocked).
T get(Section const §ion, std::string const &name, T const &defaultValue=T{})
Retrieve a key/value pair from a section.
STAmount largestAmount(STAmount const &amt)
BaseUInt< 160, detail::CurrencyTag > Currency
Currency is a hash representing a specific currency.
std::ostream & operator<<(std::ostream &out, BaseUInt< Bits, Tag > const &u)
static STPath removeIssuer(STPath const &path)
AccountID getMPTIssuer(MPTID const &mptid)
std::string transToken(TER code)
Currency const & xrpCurrency()
XRP currency.
std::string to_string(BaseUInt< Bits, Tag > const &a)
bool isGlobalFrozen(ReadView const &view, AccountID const &issuer)
Check if the issuer has the global freeze flag set.
static bool isDefaultPath(STPath const &path)
LineDirection
Describes how an account was found in a path, and how to find the next set of paths.
BaseUInt< 192 > MPTID
MPTID is a 192-bit value representing MPT Issuance ID, which is a concatenation of a 32-bit sequence ...
std::uint64_t getRate(STAmount const &offerOut, STAmount const &offerIn)
bool convertAllCheck(STAmount const &a)
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
bool isTesSuccess(TER x) noexcept
TERSubset< CanCvtToTER > TER
TER requireAuth(ReadView const &view, MPTIssue const &mptIssue, AccountID const &account, AuthType authType=AuthType::Legacy, std::uint8_t depth=0)
Check if the account lacks required authorization for MPT.
AccountID const & xrpAccount()
Compute AccountID from public key.
constexpr bool equalTokens(Asset const &lhs, Asset const &rhs)