1#include <xrpld/app/main/Application.h>
2#include <xrpld/rpc/detail/Pathfinder.h>
3#include <xrpld/rpc/detail/PathfinderUtils.h>
4#include <xrpld/rpc/detail/RippleLineCache.h>
6#include <xrpl/basics/join.h>
7#include <xrpl/core/JobQueue.h>
8#include <xrpl/json/to_string.h>
9#include <xrpl/ledger/OrderBookDB.h>
10#include <xrpl/ledger/PaymentSandbox.h>
11#include <xrpl/tx/paths/RippleCalc.h>
53constexpr std::size_t PATHFINDER_MAX_COMPLETE_PATHS = 1000;
55struct AccountCandidate
60 static int const highPriority = 10000;
64compareAccountCandidate(
66 AccountCandidate
const& first,
67 AccountCandidate
const& second)
69 if (first.priority < second.priority)
72 if (first.account > second.account)
75 return (first.priority ^ seq) < (second.priority ^ seq);
104 for (
auto const& node : type)
135smallestUsefulAmount(STAmount
const& amount,
int maxPaths)
137 return divide(amount, STAmount(maxPaths + 2),
amount.issue());
151 : mSrcAccount(uSrcAccount)
152 , mDstAccount(uDstAccount)
153 , mEffectiveDst(
isXRP(saDstAmount.getIssuer()) ? uDstAccount : saDstAmount.getIssuer())
154 , mDstAmount(saDstAmount)
155 , mSrcCurrency(uSrcCurrency)
156 , mSrcIssuer(uSrcIssuer)
157 , mSrcAmount(srcAmount.value_or(
STAmount(
160 uSrcIssuer.value_or(
isXRP(uSrcCurrency) ?
xrpAccount() : uSrcAccount)},
166 , mLedger(cache->getLedger())
169 , j_(app.getJournal(
"Pathfinder"))
173 "xrpl::Pathfinder::Pathfinder : valid inputs");
179 JLOG(
j_.
trace()) <<
"findPaths start";
183 JLOG(
j_.
debug()) <<
"Destination amount was zero.";
195 JLOG(
j_.
debug()) <<
"Tried to send to same issuer";
211 auto issuer = currencyIsXRP ?
AccountID() : account;
214 JLOG(
j_.
trace()) <<
"findPaths>"
217 <<
" mSrcCurrency=" <<
mSrcCurrency <<
" mSrcIssuer=" << issuerString;
221 JLOG(
j_.
debug()) <<
"findPaths< no ledger";
231 JLOG(
j_.
debug()) <<
"invalid source account";
237 JLOG(
j_.
debug()) <<
"Non-existent gateway";
247 JLOG(
j_.
debug()) <<
"New account not being funded in XRP ";
254 JLOG(
j_.
debug()) <<
"New account not getting enough funding: " <<
mDstAmount <<
" < "
263 if (bSrcXrp && bDstXrp)
266 JLOG(
j_.
debug()) <<
"XRP to XRP payment";
272 JLOG(
j_.
debug()) <<
"XRP to non-XRP payment";
278 JLOG(
j_.
debug()) <<
"non-XRP to XRP payment";
284 JLOG(
j_.
debug()) <<
"non-XRP to non-XRP - same currency";
290 JLOG(
j_.
debug()) <<
"non-XRP to non-XRP - cross currency";
295 for (
auto const& costedPath : mPathTable[paymentType])
297 if (continueCallback && !continueCallback())
300 if (costedPath.searchLevel <= searchLevel)
302 JLOG(
j_.
trace()) <<
"findPaths trying payment type " << paymentType;
323 uint64_t& qualityOut)
const
353 qualityOut =
getRate(rc.actualAmountOut, rc.actualAmountIn);
354 amountOut = rc.actualAmountOut;
363 mDstAmount - amountOut,
373 amountOut += rc.actualAmountOut;
380 JLOG(j_.
info()) <<
"checkpath: exception (" << e.
what() <<
") "
411 JLOG(
j_.
debug()) <<
"Default path contributes: " << rc.actualAmountIn;
421 JLOG(
j_.
debug()) <<
"Default path causes exception";
443 return path.
size() == 1;
453 for (
auto it = path.
begin() + 1; it != path.
end(); ++it)
468 JLOG(
j_.
trace()) <<
"rankPaths with " << paths.
size() <<
" candidates, and " << maxPaths
473 auto const saMinDstAmount = [&]() ->
STAmount {
477 return smallestUsefulAmount(
mDstAmount, maxPaths);
485 for (
int i = 0; i < paths.
size(); ++i)
487 if (continueCallback && !continueCallback())
489 auto const& currentPath = paths[i];
490 if (!currentPath.empty())
493 uint64_t uQuality = 0;
494 auto const resultCode =
503 JLOG(
j_.
debug()) <<
"findPaths: quality: " << uQuality <<
": "
506 rankedPaths.
push_back({uQuality, currentPath.size(), liquidity, i});
521 if (!convert_all_ && a.quality != b.quality)
522 return a.quality < b.quality;
525 if (a.liquidity != b.liquidity)
526 return a.liquidity > b.liquidity;
529 if (a.length != b.length)
530 return a.length < b.length;
533 return a.index > b.index;
540 STPath& fullLiquidityPath,
552 fullLiquidityPath.
empty(),
"xrpl::Pathfinder::getBestPaths : first empty path result");
556 rankPaths(maxPaths, extraPaths, extraPathRanks, continueCallback);
566 auto extraPathsIterator = extraPathRanks.
begin();
568 while (pathsIterator !=
mPathRanks.end() || extraPathsIterator != extraPathRanks.
end())
570 if (continueCallback && !continueCallback())
572 bool usePath =
false;
573 bool useExtraPath =
false;
579 else if (extraPathsIterator == extraPathRanks.
end())
583 else if (extraPathsIterator->quality < pathsIterator->quality)
587 else if (extraPathsIterator->quality > pathsIterator->quality)
591 else if (extraPathsIterator->liquidity > pathsIterator->liquidity)
595 else if (extraPathsIterator->liquidity < pathsIterator->liquidity)
606 auto& pathRank = usePath ? *pathsIterator : *extraPathsIterator;
608 auto const& path = usePath ?
mCompletePaths[pathRank.index] : extraPaths[pathRank.index];
611 ++extraPathsIterator;
616 auto iPathsLeft = maxPaths - bestPaths.
size();
617 if (iPathsLeft <= 0 && !fullLiquidityPath.
empty())
623 UNREACHABLE(
"xrpl::Pathfinder::getBestPaths : path not found");
628 bool startsWithIssuer =
false;
630 if (!issuerIsSender && usePath)
633 if (
isDefaultPath(path) || path.front().getAccountID() != srcIssuer)
638 startsWithIssuer =
true;
641 if (iPathsLeft > 1 || (iPathsLeft > 0 && pathRank.liquidity >= remaining))
645 remaining -= pathRank.liquidity;
648 else if (iPathsLeft == 0 && pathRank.liquidity >=
mDstAmount && fullLiquidityPath.
empty())
651 fullLiquidityPath = (startsWithIssuer ?
removeIssuer(path) : path);
652 JLOG(
j_.
debug()) <<
"Found extra full path: "
661 if (remaining > beast::zero)
664 fullLiquidityPath.
empty(),
"xrpl::Pathfinder::getBestPaths : second empty path result");
665 JLOG(
j_.
info()) <<
"Paths could not send " << remaining <<
" of " <<
mDstAmount;
681 return matchingCurrency && matchingAccount;
693 Issue const issue(currency, account);
706 int const aFlags = sleAccount->getFieldU32(sfFlags);
707 bool const bAuthRequired = (aFlags & lsfRequireAuth) != 0;
708 bool const bFrozen = ((aFlags & lsfGlobalFreeze) != 0);
716 if (
auto const lines =
mRLCache->getRippleLines(account, direction))
718 for (
auto const& rspEntry : *lines)
720 if (currency != rspEntry.getLimit().getCurrency())
724 rspEntry.getBalance() <= beast::zero &&
725 (!rspEntry.getLimitPeer() ||
726 -rspEntry.getBalance() >= rspEntry.getLimitPeer() ||
727 (bAuthRequired && !rspEntry.getAuth())))
730 else if (isDstCurrency && dstAccount == rspEntry.getAccountIDPeer())
734 else if (rspEntry.getNoRipplePeer())
738 else if (rspEntry.getFreezePeer())
760 JLOG(
j_.
debug()) <<
"addLink< on " << currentPaths.
size() <<
" source(s), flags=" << addFlags;
761 for (
auto const& path : currentPaths)
763 if (continueCallback && !continueCallback())
765 addLink(path, incompletePaths, addFlags, continueCallback);
776 auto it =
mPaths.find(pathType);
781 if (pathType.
empty())
783 if (continueCallback && !continueCallback())
794 JLOG(
j_.
debug()) <<
"getPaths< adding onto '" << pathTypeToString(parentPathType)
795 <<
"' to get '" << pathTypeToString(pathType) <<
"'";
800 auto nodeType = pathType.
back();
805 XRPL_ASSERT(pathsOut.
empty(),
"xrpl::Pathfinder::addPathsForType : empty paths");
838 JLOG(
j_.
debug()) <<
"getPaths> " << pathsOut.
size() <<
" partial paths found";
850 auto const flag((toAccount > fromAccount) ? lsfHighNoRipple : lsfLowNoRipple);
852 return sleRipple && ((sleRipple->getFieldU32(sfFlags) & flag) != 0u);
861 if (currentPath.
empty())
872 auto const& fromAccount =
883 for (
auto const& p : pathSet)
893 STPath const& currentPath,
899 auto const& uEndCurrency = pathEnd.getCurrency();
900 auto const& uEndIssuer = pathEnd.getIssuerID();
901 auto const& uEndAccount = pathEnd.getAccountID();
902 bool const bOnXRP = uEndCurrency.isZero();
909 JLOG(
j_.
trace()) <<
"addLink< flags=" << addFlags <<
" onXRP=" << bOnXRP
920 JLOG(
j_.
trace()) <<
"complete path found ax: "
932 bool const bRequireAuth((sleEnd->getFieldU32(sfFlags) & lsfRequireAuth) != 0u);
935 bool const bDestOnly((addFlags &
afAC_LAST) != 0u);
937 if (
auto const lines =
mRLCache->getRippleLines(
941 auto& rippleLines = *lines;
943 AccountCandidates candidates;
944 candidates.reserve(rippleLines.size());
946 for (
auto const& rs : rippleLines)
948 if (continueCallback && !continueCallback())
950 auto const& acct = rs.getAccountIDPeer();
953 if (hasEffectiveDestination && (acct ==
mDstAccount))
961 if (bDestOnly && !bToDestination)
966 if ((uEndCurrency == rs.getLimit().getCurrency()) &&
967 !currentPath.
hasSeen(acct, uEndCurrency, acct))
971 if (rs.getBalance() <= beast::zero &&
972 (!rs.getLimitPeer() || -rs.getBalance() >= rs.getLimitPeer() ||
973 (bRequireAuth && !rs.getAuth())))
977 else if (bIsNoRippleOut && rs.getNoRipple())
981 else if (bToDestination)
987 if (!currentPath.
empty())
989 JLOG(
j_.
trace()) <<
"complete path found ae: "
997 candidates.push_back({AccountCandidate::highPriority, acct});
1015 candidates.push_back({
out, acct});
1020 if (!candidates.empty())
1026 compareAccountCandidate,
1028 std::placeholders::_1,
1029 std::placeholders::_2));
1031 int count = candidates.size();
1037 else if (count > 50)
1042 auto it = candidates.begin();
1043 while (count-- != 0)
1045 if (continueCallback && !continueCallback())
1050 incompletePaths.
assembleAdd(currentPath, pathElement);
1058 JLOG(
j_.
warn()) <<
"Path ends on non-existent issuer";
1072 incompletePaths.
assembleAdd(currentPath, pathElement);
1077 bool const bDestOnly = (addFlags &
afOB_LAST) != 0;
1080 JLOG(
j_.
trace()) << books.size() <<
" books found from this currency/issuer";
1082 for (
auto const& book : books)
1084 if (continueCallback && !continueCallback())
1090 STPath newPath(currentPath);
1092 if (book.out.currency.isZero())
1103 JLOG(
j_.
trace()) <<
"complete path found bx: "
1112 else if (!currentPath.
hasSeen(
1113 book.out.account, book.out.currency, book.out.account))
1117 if ((newPath.
size() >= 2) && (newPath.
back().isAccount()) &&
1118 (newPath[newPath.
size() - 2].isOffer()))
1137 if (hasEffectiveDestination && book.out.account ==
mDstAccount &&
1147 JLOG(
j_.
trace()) <<
"complete path found ba: "
1172makePath(
char const*
string)
1216 auto& list = mPathTable[type];
1217 XRPL_ASSERT(list.empty(),
"xrpl::fillPaths : empty paths");
1218 for (
auto& cost : costs)
1219 list.push_back({cost.cost, makePath(cost.path)});
Stream trace() const
Severity stream access functions.
A currency issued by an account.
std::unique_ptr< LoadEvent > makeLoadEvent(JobType t, std::string const &name)
Return a scoped LoadEvent.
virtual std::vector< Book > getBooksByTakerPays(Issue const &issue, std::optional< Domain > const &domain=std::nullopt)=0
Get all order books that want a specific issue.
virtual bool isBookToXRP(Issue const &issue, std::optional< Domain > const &domain=std::nullopt)=0
Check if an order book to XRP exists for the given issue.
virtual int getBookSize(Issue const &issue, std::optional< Domain > const &domain=std::nullopt)=0
Get the count of order books that want a specific issue.
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
std::shared_ptr< RippleLineCache > mRLCache
static std::uint32_t const afADD_ACCOUNTS
std::unique_ptr< LoadEvent > m_loadEvent
Pathfinder(std::shared_ptr< RippleLineCache > const &cache, AccountID const &srcAccount, AccountID const &dstAccount, Currency const &uSrcCurrency, 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 issueMatchesOrigin(Issue const &)
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)
static std::uint32_t const afAC_LAST
std::optional< AccountID > mSrcIssuer
std::vector< NodeType > PathType
static std::uint32_t const afOB_XRP
std::map< PathType, STPathSet > mPaths
hash_map< Issue, int > mPathsOutCountMap
STPathSet & addPathsForType(PathType const &type, std::function< bool(void)> const &continueCallback)
std::optional< uint256 > mDomain
bool isNoRipple(AccountID const &fromAccount, AccountID const &toAccount, Currency const ¤cy)
bool findPaths(int searchLevel, std::function< bool(void)> const &continueCallback={})
int getPathsOut(Currency const ¤cy, AccountID const &account, LineDirection direction, bool isDestCurrency, AccountID const &dest, std::function< bool(void)> const &continueCallback)
static std::uint32_t const afOB_LAST
bool isNoRippleOut(STPath const ¤tPath)
static void initPathTable()
STAmount mRemainingAmount
The amount remaining from mSrcAccount after the default liquidity has been removed.
std::shared_ptr< ReadView const > mLedger
static std::uint32_t const afADD_BOOKS
std::vector< PathRank > mPathRanks
void computePathRanks(int maxPaths, std::function< bool(void)> const &continueCallback={})
Compute the rankings of the paths.
A wrapper which makes credits unavailable to balances.
std::string getFullText() const override
Currency const & getCurrency() const
bool native() const noexcept
AccountID const & getAccountID() const
Currency const & getCurrency() const
bool assembleAdd(STPath const &base, STPathElement const &tail)
void push_back(STPath const &e)
std::vector< STPath >::size_type size() const
Json::Value getJson(JsonOptions) const override
std::vector< STPathElement >::size_type size() const
Json::Value getJson(JsonOptions) const
void push_back(STPathElement const &e)
std::vector< STPathElement >::const_iterator end() const
bool hasSeen(AccountID const &account, Currency const ¤cy, AccountID const &issuer) const
void emplace_back(Args &&... args)
std::vector< STPathElement >::const_reference back() const
std::vector< STPathElement >::const_iterator begin() const
virtual JobQueue & getJobQueue()=0
virtual OrderBookDB & getOrderBookDB()=0
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 line(AccountID const &id0, AccountID const &id1, Currency const ¤cy) noexcept
The index of a trust line for a given currency.
Keylet account(AccountID const &id) noexcept
AccountID root.
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)
std::string to_string(base_uint< Bits, Tag > const &a)
STAmount largestAmount(STAmount const &amt)
base_uint< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
static STPath removeIssuer(STPath const &path)
std::string transToken(TER code)
Currency const & xrpCurrency()
XRP currency.
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.
std::uint64_t getRate(STAmount const &offerOut, STAmount const &offerIn)
bool convertAllCheck(STAmount const &a)
void addUniquePath(STPathSet &pathSet, STPath const &path)
bool isTesSuccess(TER x) noexcept
AccountID const & xrpAccount()
Compute AccountID from public key.