rippled
Loading...
Searching...
No Matches
Pathfinder.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2012, 2013 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#include <xrpld/app/ledger/OrderBookDB.h>
21#include <xrpld/app/main/Application.h>
22#include <xrpld/app/paths/Pathfinder.h>
23#include <xrpld/app/paths/RippleCalc.h>
24#include <xrpld/app/paths/RippleLineCache.h>
25#include <xrpld/app/paths/detail/PathfinderUtils.h>
26#include <xrpld/core/JobQueue.h>
27#include <xrpld/ledger/PaymentSandbox.h>
28#include <xrpl/basics/Log.h>
29#include <xrpl/basics/join.h>
30#include <xrpl/json/to_string.h>
31
32#include <tuple>
33
34/*
35
36Core Pathfinding Engine
37
38The pathfinding request is identified by category, XRP to XRP, XRP to
39non-XRP, non-XRP to XRP, same currency non-XRP to non-XRP, cross-currency
40non-XRP to non-XRP. For each category, there is a table of paths that the
41pathfinder searches for. Complete paths are collected.
42
43Each complete path is then rated and sorted. Paths with no or trivial
44liquidity are dropped. Otherwise, paths are sorted based on quality,
45liquidity, and path length.
46
47Path slots are filled in quality (ratio of out to in) order, with the
48exception that the last path must have enough liquidity to complete the
49payment (assuming no liquidity overlap). In addition, if no selected path
50is capable of providing enough liquidity to complete the payment by itself,
51an extra "covering" path is returned.
52
53The selected paths are then tested to determine if they can complete the
54payment and, if so, at what cost. If they fail and a covering path was
55found, the test is repeated with the covering path. If this succeeds, the
56final paths and the estimated cost are returned.
57
58The engine permits the search depth to be selected and the paths table
59includes the depth at which each path type is found. A search depth of zero
60causes no searching to be done. Extra paths can also be injected, and this
61should be used to preserve previously-found paths across invokations for the
62same path request (particularly if the search depth may change).
63
64*/
65
66namespace ripple {
67
68namespace {
69
70// This is an arbitrary cutoff, and it might cause us to miss other
71// good paths with this arbitrary cut off.
72constexpr std::size_t PATHFINDER_MAX_COMPLETE_PATHS = 1000;
73
74struct AccountCandidate
75{
76 int priority;
77 AccountID account;
78
79 static const int highPriority = 10000;
80};
81
82bool
83compareAccountCandidate(
84 std::uint32_t seq,
85 AccountCandidate const& first,
86 AccountCandidate const& second)
87{
88 if (first.priority < second.priority)
89 return false;
90
91 if (first.account > second.account)
92 return true;
93
94 return (first.priority ^ seq) < (second.priority ^ seq);
95}
96
97using AccountCandidates = std::vector<AccountCandidate>;
98
99struct CostedPath
100{
101 int searchLevel;
103};
104
105using CostedPathList = std::vector<CostedPath>;
106
108
109struct PathCost
110{
111 int cost;
112 char const* path;
113};
114using PathCostList = std::vector<PathCost>;
115
116static PathTable mPathTable;
117
119pathTypeToString(Pathfinder::PathType const& type)
120{
121 std::string ret;
122
123 for (auto const& node : type)
124 {
125 switch (node)
126 {
128 ret.append("s");
129 break;
131 ret.append("a");
132 break;
134 ret.append("b");
135 break;
137 ret.append("x");
138 break;
140 ret.append("f");
141 break;
143 ret.append("d");
144 break;
145 }
146 }
147
148 return ret;
149}
150
151// Return the smallest amount of useful liquidity for a given amount, and the
152// total number of paths we have to evaluate.
153STAmount
154smallestUsefulAmount(STAmount const& amount, int maxPaths)
155{
156 return divide(amount, STAmount(maxPaths + 2), amount.issue());
157}
158} // namespace
159
162 AccountID const& uSrcAccount,
163 AccountID const& uDstAccount,
164 Currency const& uSrcCurrency,
165 std::optional<AccountID> const& uSrcIssuer,
166 STAmount const& saDstAmount,
167 std::optional<STAmount> const& srcAmount,
168 Application& app)
169 : mSrcAccount(uSrcAccount)
170 , mDstAccount(uDstAccount)
171 , mEffectiveDst(
172 isXRP(saDstAmount.getIssuer()) ? uDstAccount
173 : saDstAmount.getIssuer())
174 , mDstAmount(saDstAmount)
175 , mSrcCurrency(uSrcCurrency)
176 , mSrcIssuer(uSrcIssuer)
177 , mSrcAmount(srcAmount.value_or(STAmount(
178 Issue{
179 uSrcCurrency,
180 uSrcIssuer.value_or(
181 isXRP(uSrcCurrency) ? xrpAccount() : uSrcAccount)},
182 1u,
183 0,
184 true)))
185 , convert_all_(convertAllCheck(mDstAmount))
186 , mLedger(cache->getLedger())
187 , mRLCache(cache)
188 , app_(app)
189 , j_(app.journal("Pathfinder"))
190{
191 XRPL_ASSERT(
192 !uSrcIssuer || isXRP(uSrcCurrency) == isXRP(uSrcIssuer.value()),
193 "ripple::Pathfinder::Pathfinder : valid inputs");
194}
195
196bool
198 int searchLevel,
199 std::function<bool(void)> const& continueCallback)
200{
201 JLOG(j_.trace()) << "findPaths start";
202 if (mDstAmount == beast::zero)
203 {
204 // No need to send zero money.
205 JLOG(j_.debug()) << "Destination amount was zero.";
206 mLedger.reset();
207 return false;
208
209 // TODO(tom): why do we reset the ledger just in this case and the one
210 // below - why don't we do it each time we return false?
211 }
212
215 {
216 // No need to send to same account with same currency.
217 JLOG(j_.debug()) << "Tried to send to same issuer";
218 mLedger.reset();
219 return false;
220 }
221
222 if (mSrcAccount == mEffectiveDst &&
224 {
225 // Default path might work, but any path would loop
226 return true;
227 }
228
230 auto currencyIsXRP = isXRP(mSrcCurrency);
231
232 bool useIssuerAccount = mSrcIssuer && !currencyIsXRP && !isXRP(*mSrcIssuer);
233 auto& account = useIssuerAccount ? *mSrcIssuer : mSrcAccount;
234 auto issuer = currencyIsXRP ? AccountID() : account;
235 mSource = STPathElement(account, mSrcCurrency, issuer);
236 auto issuerString =
238 JLOG(j_.trace()) << "findPaths>" << " mSrcAccount=" << mSrcAccount
239 << " mDstAccount=" << mDstAccount
240 << " mDstAmount=" << mDstAmount.getFullText()
241 << " mSrcCurrency=" << mSrcCurrency
242 << " mSrcIssuer=" << issuerString;
243
244 if (!mLedger)
245 {
246 JLOG(j_.debug()) << "findPaths< no ledger";
247 return false;
248 }
249
250 bool bSrcXrp = isXRP(mSrcCurrency);
251 bool bDstXrp = isXRP(mDstAmount.getCurrency());
252
253 if (!mLedger->exists(keylet::account(mSrcAccount)))
254 {
255 // We can't even start without a source account.
256 JLOG(j_.debug()) << "invalid source account";
257 return false;
258 }
259
260 if ((mEffectiveDst != mDstAccount) &&
262 {
263 JLOG(j_.debug()) << "Non-existent gateway";
264 return false;
265 }
266
267 if (!mLedger->exists(keylet::account(mDstAccount)))
268 {
269 // Can't find the destination account - we must be funding a new
270 // account.
271 if (!bDstXrp)
272 {
273 JLOG(j_.debug()) << "New account not being funded in XRP ";
274 return false;
275 }
276
277 auto const reserve = STAmount(mLedger->fees().accountReserve(0));
278 if (mDstAmount < reserve)
279 {
280 JLOG(j_.debug())
281 << "New account not getting enough funding: " << mDstAmount
282 << " < " << reserve;
283 return false;
284 }
285 }
286
287 // Now compute the payment type from the types of the source and destination
288 // currencies.
289 PaymentType paymentType;
290 if (bSrcXrp && bDstXrp)
291 {
292 // XRP -> XRP
293 JLOG(j_.debug()) << "XRP to XRP payment";
294 paymentType = pt_XRP_to_XRP;
295 }
296 else if (bSrcXrp)
297 {
298 // XRP -> non-XRP
299 JLOG(j_.debug()) << "XRP to non-XRP payment";
300 paymentType = pt_XRP_to_nonXRP;
301 }
302 else if (bDstXrp)
303 {
304 // non-XRP -> XRP
305 JLOG(j_.debug()) << "non-XRP to XRP payment";
306 paymentType = pt_nonXRP_to_XRP;
307 }
308 else if (mSrcCurrency == mDstAmount.getCurrency())
309 {
310 // non-XRP -> non-XRP - Same currency
311 JLOG(j_.debug()) << "non-XRP to non-XRP - same currency";
312 paymentType = pt_nonXRP_to_same;
313 }
314 else
315 {
316 // non-XRP to non-XRP - Different currency
317 JLOG(j_.debug()) << "non-XRP to non-XRP - cross currency";
318 paymentType = pt_nonXRP_to_nonXRP;
319 }
320
321 // Now iterate over all paths for that paymentType.
322 for (auto const& costedPath : mPathTable[paymentType])
323 {
324 if (continueCallback && !continueCallback())
325 return false;
326 // Only use paths with at most the current search level.
327 if (costedPath.searchLevel <= searchLevel)
328 {
329 JLOG(j_.trace()) << "findPaths trying payment type " << paymentType;
330 addPathsForType(costedPath.type, continueCallback);
331
332 if (mCompletePaths.size() > PATHFINDER_MAX_COMPLETE_PATHS)
333 break;
334 }
335 }
336
337 JLOG(j_.debug()) << mCompletePaths.size() << " complete paths found";
338
339 // Even if we find no paths, default paths may work, and we don't check them
340 // currently.
341 return true;
342}
343
344TER
346 STPath const& path, // IN: The path to check.
347 STAmount const& minDstAmount, // IN: The minimum output this path must
348 // deliver to be worth keeping.
349 STAmount& amountOut, // OUT: The actual liquidity along the path.
350 uint64_t& qualityOut) const // OUT: The returned initial quality
351{
352 STPathSet pathSet;
353 pathSet.push_back(path);
354
356 rcInput.defaultPathsAllowed = false;
357
358 PaymentSandbox sandbox(&*mLedger, tapNONE);
359
360 try
361 {
362 // Compute a path that provides at least the minimum liquidity.
363 if (convert_all_)
364 rcInput.partialPaymentAllowed = true;
365
367 sandbox,
368 mSrcAmount,
369 minDstAmount,
370 mDstAccount,
371 mSrcAccount,
372 pathSet,
373 app_.logs(),
374 &rcInput);
375 // If we can't get even the minimum liquidity requested, we're done.
376 if (rc.result() != tesSUCCESS)
377 return rc.result();
378
379 qualityOut = getRate(rc.actualAmountOut, rc.actualAmountIn);
380 amountOut = rc.actualAmountOut;
381
382 if (!convert_all_)
383 {
384 // Now try to compute the remaining liquidity.
385 rcInput.partialPaymentAllowed = true;
387 sandbox,
388 mSrcAmount,
389 mDstAmount - amountOut,
390 mDstAccount,
391 mSrcAccount,
392 pathSet,
393 app_.logs(),
394 &rcInput);
395
396 // If we found further liquidity, add it into the result.
397 if (rc.result() == tesSUCCESS)
398 amountOut += rc.actualAmountOut;
399 }
400
401 return tesSUCCESS;
402 }
403 catch (std::exception const& e)
404 {
405 JLOG(j_.info()) << "checkpath: exception (" << e.what() << ") "
406 << path.getJson(JsonOptions::none);
407 return tefEXCEPTION;
408 }
409}
410
411void
413 int maxPaths,
414 std::function<bool(void)> const& continueCallback)
415{
417
418 // Must subtract liquidity in default path from remaining amount.
419 try
420 {
421 PaymentSandbox sandbox(&*mLedger, tapNONE);
422
424 rcInput.partialPaymentAllowed = true;
426 sandbox,
431 STPathSet(),
432 app_.logs(),
433 &rcInput);
434
435 if (rc.result() == tesSUCCESS)
436 {
437 JLOG(j_.debug())
438 << "Default path contributes: " << rc.actualAmountIn;
439 mRemainingAmount -= rc.actualAmountOut;
440 }
441 else
442 {
443 JLOG(j_.debug())
444 << "Default path fails: " << transToken(rc.result());
445 }
446 }
447 catch (std::exception const&)
448 {
449 JLOG(j_.debug()) << "Default path causes exception";
450 }
451
452 rankPaths(maxPaths, mCompletePaths, mPathRanks, continueCallback);
453}
454
455static bool
457{
458 // FIXME: default paths can consist of more than just an account:
459 //
460 // JoelKatz writes:
461 // So the test for whether a path is a default path is incorrect. I'm not
462 // sure it's worth the complexity of fixing though. If we are going to fix
463 // it, I'd suggest doing it this way:
464 //
465 // 1) Compute the default path, probably by using 'expandPath' to expand an
466 // empty path. 2) Chop off the source and destination nodes.
467 //
468 // 3) In the pathfinding loop, if the source issuer is not the sender,
469 // reject all paths that don't begin with the issuer's account node or match
470 // the path we built at step 2.
471 return path.size() == 1;
472}
473
474static STPath
476{
477 // This path starts with the issuer, which is already implied
478 // so remove the head node
479 STPath ret;
480
481 for (auto it = path.begin() + 1; it != path.end(); ++it)
482 ret.push_back(*it);
483
484 return ret;
485}
486
487// For each useful path in the input path set,
488// create a ranking entry in the output vector of path ranks
489void
491 int maxPaths,
492 STPathSet const& paths,
493 std::vector<PathRank>& rankedPaths,
494 std::function<bool(void)> const& continueCallback)
495{
496 JLOG(j_.trace()) << "rankPaths with " << paths.size() << " candidates, and "
497 << maxPaths << " maximum";
498 rankedPaths.clear();
499 rankedPaths.reserve(paths.size());
500
501 auto const saMinDstAmount = [&]() -> STAmount {
502 if (!convert_all_)
503 {
504 // Ignore paths that move only very small amounts.
505 return smallestUsefulAmount(mDstAmount, maxPaths);
506 }
507
508 // On convert_all_ partialPaymentAllowed will be set to true
509 // and requiring a huge amount will find the highest liquidity.
511 }();
512
513 for (int i = 0; i < paths.size(); ++i)
514 {
515 if (continueCallback && !continueCallback())
516 return;
517 auto const& currentPath = paths[i];
518 if (!currentPath.empty())
519 {
520 STAmount liquidity;
521 uint64_t uQuality;
522 auto const resultCode = getPathLiquidity(
523 currentPath, saMinDstAmount, liquidity, uQuality);
524 if (resultCode != tesSUCCESS)
525 {
526 JLOG(j_.debug())
527 << "findPaths: dropping : " << transToken(resultCode)
528 << ": " << currentPath.getJson(JsonOptions::none);
529 }
530 else
531 {
532 JLOG(j_.debug()) << "findPaths: quality: " << uQuality << ": "
533 << currentPath.getJson(JsonOptions::none);
534
535 rankedPaths.push_back(
536 {uQuality, currentPath.size(), liquidity, i});
537 }
538 }
539 }
540
541 // Sort paths by:
542 // cost of path (when considering quality)
543 // width of path
544 // length of path
545 // A better PathRank is lower, best are sorted to the beginning.
546 std::sort(
547 rankedPaths.begin(),
548 rankedPaths.end(),
549 [&](Pathfinder::PathRank const& a, Pathfinder::PathRank const& b) {
550 // 1) Higher quality (lower cost) is better
551 if (!convert_all_ && a.quality != b.quality)
552 return a.quality < b.quality;
553
554 // 2) More liquidity (higher volume) is better
555 if (a.liquidity != b.liquidity)
556 return a.liquidity > b.liquidity;
557
558 // 3) Shorter paths are better
559 if (a.length != b.length)
560 return a.length < b.length;
561
562 // 4) Tie breaker
563 return a.index > b.index;
564 });
565}
566
569 int maxPaths,
570 STPath& fullLiquidityPath,
571 STPathSet const& extraPaths,
572 AccountID const& srcIssuer,
573 std::function<bool(void)> const& continueCallback)
574{
575 JLOG(j_.debug()) << "findPaths: " << mCompletePaths.size() << " paths and "
576 << extraPaths.size() << " extras";
577
578 if (mCompletePaths.empty() && extraPaths.empty())
579 return mCompletePaths;
580
581 XRPL_ASSERT(
582 fullLiquidityPath.empty(),
583 "ripple::Pathfinder::getBestPaths : first empty path result");
584 const bool issuerIsSender =
585 isXRP(mSrcCurrency) || (srcIssuer == mSrcAccount);
586
587 std::vector<PathRank> extraPathRanks;
588 rankPaths(maxPaths, extraPaths, extraPathRanks, continueCallback);
589
590 STPathSet bestPaths;
591
592 // The best PathRanks are now at the start. Pull off enough of them to
593 // fill bestPaths, then look through the rest for the best individual
594 // path that can satisfy the entire liquidity - if one exists.
595 STAmount remaining = mRemainingAmount;
596
597 auto pathsIterator = mPathRanks.begin();
598 auto extraPathsIterator = extraPathRanks.begin();
599
600 while (pathsIterator != mPathRanks.end() ||
601 extraPathsIterator != extraPathRanks.end())
602 {
603 if (continueCallback && !continueCallback())
604 break;
605 bool usePath = false;
606 bool useExtraPath = false;
607
608 if (pathsIterator == mPathRanks.end())
609 useExtraPath = true;
610 else if (extraPathsIterator == extraPathRanks.end())
611 usePath = true;
612 else if (extraPathsIterator->quality < pathsIterator->quality)
613 useExtraPath = true;
614 else if (extraPathsIterator->quality > pathsIterator->quality)
615 usePath = true;
616 else if (extraPathsIterator->liquidity > pathsIterator->liquidity)
617 useExtraPath = true;
618 else if (extraPathsIterator->liquidity < pathsIterator->liquidity)
619 usePath = true;
620 else
621 {
622 // Risk is high they have identical liquidity
623 useExtraPath = true;
624 usePath = true;
625 }
626
627 auto& pathRank = usePath ? *pathsIterator : *extraPathsIterator;
628
629 auto const& path = usePath ? mCompletePaths[pathRank.index]
630 : extraPaths[pathRank.index];
631
632 if (useExtraPath)
633 ++extraPathsIterator;
634
635 if (usePath)
636 ++pathsIterator;
637
638 auto iPathsLeft = maxPaths - bestPaths.size();
639 if (!(iPathsLeft > 0 || fullLiquidityPath.empty()))
640 break;
641
642 if (path.empty())
643 {
644 UNREACHABLE("ripple::Pathfinder::getBestPaths : path not found");
645 continue;
646 }
647
648 bool startsWithIssuer = false;
649
650 if (!issuerIsSender && usePath)
651 {
652 // Need to make sure path matches issuer constraints
653 if (isDefaultPath(path) || path.front().getAccountID() != srcIssuer)
654 {
655 continue;
656 }
657
658 startsWithIssuer = true;
659 }
660
661 if (iPathsLeft > 1 ||
662 (iPathsLeft > 0 && pathRank.liquidity >= remaining))
663 // last path must fill
664 {
665 --iPathsLeft;
666 remaining -= pathRank.liquidity;
667 bestPaths.push_back(startsWithIssuer ? removeIssuer(path) : path);
668 }
669 else if (
670 iPathsLeft == 0 && pathRank.liquidity >= mDstAmount &&
671 fullLiquidityPath.empty())
672 {
673 // We found an extra path that can move the whole amount.
674 fullLiquidityPath = (startsWithIssuer ? removeIssuer(path) : path);
675 JLOG(j_.debug()) << "Found extra full path: "
676 << fullLiquidityPath.getJson(JsonOptions::none);
677 }
678 else
679 {
680 JLOG(j_.debug()) << "Skipping a non-filling path: "
681 << path.getJson(JsonOptions::none);
682 }
683 }
684
685 if (remaining > beast::zero)
686 {
687 XRPL_ASSERT(
688 fullLiquidityPath.empty(),
689 "ripple::Pathfinder::getBestPaths : second empty path result");
690 JLOG(j_.info()) << "Paths could not send " << remaining << " of "
691 << mDstAmount;
692 }
693 else
694 {
695 JLOG(j_.debug()) << "findPaths: RESULTS: "
696 << bestPaths.getJson(JsonOptions::none);
697 }
698 return bestPaths;
699}
700
701bool
703{
704 bool matchingCurrency = (issue.currency == mSrcCurrency);
705 bool matchingAccount = isXRP(issue.currency) ||
706 (mSrcIssuer && issue.account == mSrcIssuer) ||
707 issue.account == mSrcAccount;
708
709 return matchingCurrency && matchingAccount;
710}
711
712int
714 Currency const& currency,
715 AccountID const& account,
716 LineDirection direction,
717 bool isDstCurrency,
718 AccountID const& dstAccount,
719 std::function<bool(void)> const& continueCallback)
720{
721 Issue const issue(currency, account);
722
723 auto [it, inserted] = mPathsOutCountMap.emplace(issue, 0);
724
725 // If it was already present, return the stored number of paths
726 if (!inserted)
727 return it->second;
728
729 auto sleAccount = mLedger->read(keylet::account(account));
730
731 if (!sleAccount)
732 return 0;
733
734 int aFlags = sleAccount->getFieldU32(sfFlags);
735 bool const bAuthRequired = (aFlags & lsfRequireAuth) != 0;
736 bool const bFrozen = ((aFlags & lsfGlobalFreeze) != 0);
737
738 int count = 0;
739
740 if (!bFrozen)
741 {
742 count = app_.getOrderBookDB().getBookSize(issue);
743
744 if (auto const lines = mRLCache->getRippleLines(account, direction))
745 {
746 for (auto const& rspEntry : *lines)
747 {
748 if (currency != rspEntry.getLimit().getCurrency())
749 {
750 }
751 else if (
752 rspEntry.getBalance() <= beast::zero &&
753 (!rspEntry.getLimitPeer() ||
754 -rspEntry.getBalance() >= rspEntry.getLimitPeer() ||
755 (bAuthRequired && !rspEntry.getAuth())))
756 {
757 }
758 else if (
759 isDstCurrency && dstAccount == rspEntry.getAccountIDPeer())
760 {
761 count += 10000; // count a path to the destination extra
762 }
763 else if (rspEntry.getNoRipplePeer())
764 {
765 // This probably isn't a useful path out
766 }
767 else if (rspEntry.getFreezePeer())
768 {
769 // Not a useful path out
770 }
771 else
772 {
773 ++count;
774 }
775 }
776 }
777 }
778 it->second = count;
779 return count;
780}
781
782void
784 STPathSet const& currentPaths, // The paths to build from
785 STPathSet& incompletePaths, // The set of partial paths we add to
786 int addFlags,
787 std::function<bool(void)> const& continueCallback)
788{
789 JLOG(j_.debug()) << "addLink< on " << currentPaths.size()
790 << " source(s), flags=" << addFlags;
791 for (auto const& path : currentPaths)
792 {
793 if (continueCallback && !continueCallback())
794 return;
795 addLink(path, incompletePaths, addFlags, continueCallback);
796 }
797}
798
801 PathType const& pathType,
802 std::function<bool(void)> const& continueCallback)
803{
804 JLOG(j_.debug()) << "addPathsForType "
805 << CollectionAndDelimiter(pathType, ", ");
806 // See if the set of paths for this type already exists.
807 auto it = mPaths.find(pathType);
808 if (it != mPaths.end())
809 return it->second;
810
811 // Otherwise, if the type has no nodes, return the empty path.
812 if (pathType.empty())
813 return mPaths[pathType];
814 if (continueCallback && !continueCallback())
815 return mPaths[{}];
816
817 // Otherwise, get the paths for the parent PathType by calling
818 // addPathsForType recursively.
819 PathType parentPathType = pathType;
820 parentPathType.pop_back();
821
822 STPathSet const& parentPaths =
823 addPathsForType(parentPathType, continueCallback);
824 STPathSet& pathsOut = mPaths[pathType];
825
826 JLOG(j_.debug()) << "getPaths< adding onto '"
827 << pathTypeToString(parentPathType) << "' to get '"
828 << pathTypeToString(pathType) << "'";
829
830 int initialSize = mCompletePaths.size();
831
832 // Add the last NodeType to the lists.
833 auto nodeType = pathType.back();
834 switch (nodeType)
835 {
836 case nt_SOURCE:
837 // Source must always be at the start, so pathsOut has to be empty.
838 XRPL_ASSERT(
839 pathsOut.empty(),
840 "ripple::Pathfinder::addPathsForType : empty paths");
841 pathsOut.push_back(STPath());
842 break;
843
844 case nt_ACCOUNTS:
845 addLinks(parentPaths, pathsOut, afADD_ACCOUNTS, continueCallback);
846 break;
847
848 case nt_BOOKS:
849 addLinks(parentPaths, pathsOut, afADD_BOOKS, continueCallback);
850 break;
851
852 case nt_XRP_BOOK:
853 addLinks(
854 parentPaths,
855 pathsOut,
857 continueCallback);
858 break;
859
860 case nt_DEST_BOOK:
861 addLinks(
862 parentPaths,
863 pathsOut,
865 continueCallback);
866 break;
867
868 case nt_DESTINATION:
869 // FIXME: What if a different issuer was specified on the
870 // destination amount?
871 // TODO(tom): what does this even mean? Should it be a JIRA?
872 addLinks(
873 parentPaths,
874 pathsOut,
876 continueCallback);
877 break;
878 }
879
880 if (mCompletePaths.size() != initialSize)
881 {
882 JLOG(j_.debug()) << (mCompletePaths.size() - initialSize)
883 << " complete paths added";
884 }
885
886 JLOG(j_.debug()) << "getPaths> " << pathsOut.size()
887 << " partial paths found";
888 return pathsOut;
889}
890
891bool
893 AccountID const& fromAccount,
894 AccountID const& toAccount,
895 Currency const& currency)
896{
897 auto sleRipple =
898 mLedger->read(keylet::line(toAccount, fromAccount, currency));
899
900 auto const flag(
901 (toAccount > fromAccount) ? lsfHighNoRipple : lsfLowNoRipple);
902
903 return sleRipple && (sleRipple->getFieldU32(sfFlags) & flag);
904}
905
906// Does this path end on an account-to-account link whose last account has
907// set "no ripple" on the link?
908bool
910{
911 // Must have at least one link.
912 if (currentPath.empty())
913 return false;
914
915 // Last link must be an account.
916 STPathElement const& endElement = currentPath.back();
917 if (!(endElement.getNodeType() & STPathElement::typeAccount))
918 return false;
919
920 // If there's only one item in the path, return true if that item specifies
921 // no ripple on the output. A path with no ripple on its output can't be
922 // followed by a link with no ripple on its input.
923 auto const& fromAccount = (currentPath.size() == 1)
925 : (currentPath.end() - 2)->getAccountID();
926 auto const& toAccount = endElement.getAccountID();
927 return isNoRipple(fromAccount, toAccount, endElement.getCurrency());
928}
929
930void
931addUniquePath(STPathSet& pathSet, STPath const& path)
932{
933 // TODO(tom): building an STPathSet this way is quadratic in the size
934 // of the STPathSet!
935 for (auto const& p : pathSet)
936 {
937 if (p == path)
938 return;
939 }
940 pathSet.push_back(path);
941}
942
943void
945 const STPath& currentPath, // The path to build from
946 STPathSet& incompletePaths, // The set of partial paths we add to
947 int addFlags,
948 std::function<bool(void)> const& continueCallback)
949{
950 auto const& pathEnd = currentPath.empty() ? mSource : currentPath.back();
951 auto const& uEndCurrency = pathEnd.getCurrency();
952 auto const& uEndIssuer = pathEnd.getIssuerID();
953 auto const& uEndAccount = pathEnd.getAccountID();
954 bool const bOnXRP = uEndCurrency.isZero();
955
956 // Does pathfinding really need to get this to
957 // a gateway (the issuer of the destination amount)
958 // rather than the ultimate destination?
959 bool const hasEffectiveDestination = mEffectiveDst != mDstAccount;
960
961 JLOG(j_.trace()) << "addLink< flags=" << addFlags << " onXRP=" << bOnXRP
962 << " completePaths size=" << mCompletePaths.size();
963 JLOG(j_.trace()) << currentPath.getJson(JsonOptions::none);
964
965 if (addFlags & afADD_ACCOUNTS)
966 {
967 // add accounts
968 if (bOnXRP)
969 {
970 if (mDstAmount.native() && !currentPath.empty())
971 { // non-default path to XRP destination
972 JLOG(j_.trace()) << "complete path found ax: "
973 << currentPath.getJson(JsonOptions::none);
974 addUniquePath(mCompletePaths, currentPath);
975 }
976 }
977 else
978 {
979 // search for accounts to add
980 auto const sleEnd = mLedger->read(keylet::account(uEndAccount));
981
982 if (sleEnd)
983 {
984 bool const bRequireAuth(
985 sleEnd->getFieldU32(sfFlags) & lsfRequireAuth);
986 bool const bIsEndCurrency(
987 uEndCurrency == mDstAmount.getCurrency());
988 bool const bIsNoRippleOut(isNoRippleOut(currentPath));
989 bool const bDestOnly(addFlags & afAC_LAST);
990
991 if (auto const lines = mRLCache->getRippleLines(
992 uEndAccount,
993 bIsNoRippleOut ? LineDirection::incoming
995 {
996 auto& rippleLines = *lines;
997
998 AccountCandidates candidates;
999 candidates.reserve(rippleLines.size());
1000
1001 for (auto const& rs : rippleLines)
1002 {
1003 if (continueCallback && !continueCallback())
1004 return;
1005 auto const& acct = rs.getAccountIDPeer();
1006 LineDirection const direction = rs.getDirectionPeer();
1007
1008 if (hasEffectiveDestination && (acct == mDstAccount))
1009 {
1010 // We skipped the gateway
1011 continue;
1012 }
1013
1014 bool bToDestination = acct == mEffectiveDst;
1015
1016 if (bDestOnly && !bToDestination)
1017 {
1018 continue;
1019 }
1020
1021 if ((uEndCurrency == rs.getLimit().getCurrency()) &&
1022 !currentPath.hasSeen(acct, uEndCurrency, acct))
1023 {
1024 // path is for correct currency and has not been
1025 // seen
1026 if (rs.getBalance() <= beast::zero &&
1027 (!rs.getLimitPeer() ||
1028 -rs.getBalance() >= rs.getLimitPeer() ||
1029 (bRequireAuth && !rs.getAuth())))
1030 {
1031 // path has no credit
1032 }
1033 else if (bIsNoRippleOut && rs.getNoRipple())
1034 {
1035 // Can't leave on this path
1036 }
1037 else if (bToDestination)
1038 {
1039 // destination is always worth trying
1040 if (uEndCurrency == mDstAmount.getCurrency())
1041 {
1042 // this is a complete path
1043 if (!currentPath.empty())
1044 {
1045 JLOG(j_.trace())
1046 << "complete path found ae: "
1047 << currentPath.getJson(
1050 mCompletePaths, currentPath);
1051 }
1052 }
1053 else if (!bDestOnly)
1054 {
1055 // this is a high-priority candidate
1056 candidates.push_back(
1057 {AccountCandidate::highPriority, acct});
1058 }
1059 }
1060 else if (acct == mSrcAccount)
1061 {
1062 // going back to the source is bad
1063 }
1064 else
1065 {
1066 // save this candidate
1067 int out = getPathsOut(
1068 uEndCurrency,
1069 acct,
1070 direction,
1071 bIsEndCurrency,
1073 continueCallback);
1074 if (out)
1075 candidates.push_back({out, acct});
1076 }
1077 }
1078 }
1079
1080 if (!candidates.empty())
1081 {
1082 std::sort(
1083 candidates.begin(),
1084 candidates.end(),
1085 std::bind(
1086 compareAccountCandidate,
1087 mLedger->seq(),
1088 std::placeholders::_1,
1089 std::placeholders::_2));
1090
1091 int count = candidates.size();
1092 // allow more paths from source
1093 if ((count > 10) && (uEndAccount != mSrcAccount))
1094 count = 10;
1095 else if (count > 50)
1096 count = 50;
1097
1098 auto it = candidates.begin();
1099 while (count-- != 0)
1100 {
1101 if (continueCallback && !continueCallback())
1102 return;
1103 // Add accounts to incompletePaths
1104 STPathElement pathElement(
1106 it->account,
1107 uEndCurrency,
1108 it->account);
1109 incompletePaths.assembleAdd(
1110 currentPath, pathElement);
1111 ++it;
1112 }
1113 }
1114 }
1115 }
1116 else
1117 {
1118 JLOG(j_.warn()) << "Path ends on non-existent issuer";
1119 }
1120 }
1121 }
1122 if (addFlags & afADD_BOOKS)
1123 {
1124 // add order books
1125 if (addFlags & afOB_XRP)
1126 {
1127 // to XRP only
1128 if (!bOnXRP &&
1129 app_.getOrderBookDB().isBookToXRP({uEndCurrency, uEndIssuer}))
1130 {
1131 STPathElement pathElement(
1133 xrpAccount(),
1134 xrpCurrency(),
1135 xrpAccount());
1136 incompletePaths.assembleAdd(currentPath, pathElement);
1137 }
1138 }
1139 else
1140 {
1141 bool bDestOnly = (addFlags & afOB_LAST) != 0;
1143 {uEndCurrency, uEndIssuer});
1144 JLOG(j_.trace())
1145 << books.size() << " books found from this currency/issuer";
1146
1147 for (auto const& book : books)
1148 {
1149 if (continueCallback && !continueCallback())
1150 return;
1151 if (!currentPath.hasSeen(
1152 xrpAccount(), book.out.currency, book.out.account) &&
1153 !issueMatchesOrigin(book.out) &&
1154 (!bDestOnly ||
1155 (book.out.currency == mDstAmount.getCurrency())))
1156 {
1157 STPath newPath(currentPath);
1158
1159 if (book.out.currency.isZero())
1160 { // to XRP
1161
1162 // add the order book itself
1163 newPath.emplace_back(
1165 xrpAccount(),
1166 xrpCurrency(),
1167 xrpAccount());
1168
1170 {
1171 // destination is XRP, add account and path is
1172 // complete
1173 JLOG(j_.trace())
1174 << "complete path found bx: "
1175 << currentPath.getJson(JsonOptions::none);
1176 addUniquePath(mCompletePaths, newPath);
1177 }
1178 else
1179 incompletePaths.push_back(newPath);
1180 }
1181 else if (!currentPath.hasSeen(
1182 book.out.account,
1183 book.out.currency,
1184 book.out.account))
1185 {
1186 // Don't want the book if we've already seen the issuer
1187 // book -> account -> book
1188 if ((newPath.size() >= 2) &&
1189 (newPath.back().isAccount()) &&
1190 (newPath[newPath.size() - 2].isOffer()))
1191 {
1192 // replace the redundant account with the order book
1193 newPath[newPath.size() - 1] = STPathElement(
1196 xrpAccount(),
1197 book.out.currency,
1198 book.out.account);
1199 }
1200 else
1201 {
1202 // add the order book
1203 newPath.emplace_back(
1206 xrpAccount(),
1207 book.out.currency,
1208 book.out.account);
1209 }
1210
1211 if (hasEffectiveDestination &&
1212 book.out.account == mDstAccount &&
1213 book.out.currency == mDstAmount.getCurrency())
1214 {
1215 // We skipped a required issuer
1216 }
1217 else if (
1218 book.out.account == mEffectiveDst &&
1219 book.out.currency == mDstAmount.getCurrency())
1220 { // with the destination account, this path is
1221 // complete
1222 JLOG(j_.trace())
1223 << "complete path found ba: "
1224 << currentPath.getJson(JsonOptions::none);
1225 addUniquePath(mCompletePaths, newPath);
1226 }
1227 else
1228 {
1229 // add issuer's account, path still incomplete
1230 incompletePaths.assembleAdd(
1231 newPath,
1234 book.out.account,
1235 book.out.currency,
1236 book.out.account));
1237 }
1238 }
1239 }
1240 }
1241 }
1242 }
1243}
1244
1245namespace {
1246
1248makePath(char const* string)
1249{
1251
1252 while (true)
1253 {
1254 switch (*string++)
1255 {
1256 case 's': // source
1258 break;
1259
1260 case 'a': // accounts
1262 break;
1263
1264 case 'b': // books
1266 break;
1267
1268 case 'x': // xrp book
1270 break;
1271
1272 case 'f': // book to final currency
1274 break;
1275
1276 case 'd':
1277 // Destination (with account, if required and not already
1278 // present).
1280 break;
1281
1282 case 0:
1283 return ret;
1284 }
1285 }
1286}
1287
1288void
1289fillPaths(Pathfinder::PaymentType type, PathCostList const& costs)
1290{
1291 auto& list = mPathTable[type];
1292 XRPL_ASSERT(list.empty(), "ripple::fillPaths : empty paths");
1293 for (auto& cost : costs)
1294 list.push_back({cost.cost, makePath(cost.path)});
1295}
1296
1297} // namespace
1298
1299// Costs:
1300// 0 = minimum to make some payments possible
1301// 1 = include trivial paths to make common cases work
1302// 4 = normal fast search level
1303// 7 = normal slow search level
1304// 10 = most agressive
1305
1306void
1308{
1309 // CAUTION: Do not include rules that build default paths
1310
1311 mPathTable.clear();
1312 fillPaths(pt_XRP_to_XRP, {});
1313
1314 fillPaths(
1316 {{1, "sfd"}, // source -> book -> gateway
1317 {3, "sfad"}, // source -> book -> account -> destination
1318 {5, "sfaad"}, // source -> book -> account -> account -> destination
1319 {6, "sbfd"}, // source -> book -> book -> destination
1320 {8, "sbafd"}, // source -> book -> account -> book -> destination
1321 {9, "sbfad"}, // source -> book -> book -> account -> destination
1322 {10, "sbafad"}});
1323
1324 fillPaths(
1326 {{1, "sxd"}, // gateway buys XRP
1327 {2, "saxd"}, // source -> gateway -> book(XRP) -> dest
1328 {6, "saaxd"},
1329 {7, "sbxd"},
1330 {8, "sabxd"},
1331 {9, "sabaxd"}});
1332
1333 // non-XRP to non-XRP (same currency)
1334 fillPaths(
1336 {
1337 {1, "sad"}, // source -> gateway -> destination
1338 {1, "sfd"}, // source -> book -> destination
1339 {4, "safd"}, // source -> gateway -> book -> destination
1340 {4, "sfad"},
1341 {5, "saad"},
1342 {5, "sbfd"},
1343 {6, "sxfad"},
1344 {6, "safad"},
1345 {6, "saxfd"}, // source -> gateway -> book to XRP -> book ->
1346 // destination
1347 {6, "saxfad"},
1348 {6, "sabfd"}, // source -> gateway -> book -> book -> destination
1349 {7, "saaad"},
1350 });
1351
1352 // non-XRP to non-XRP (different currency)
1353 fillPaths(
1355 {
1356 {1, "sfad"},
1357 {1, "safd"},
1358 {3, "safad"},
1359 {4, "sxfd"},
1360 {5, "saxfd"},
1361 {5, "sxfad"},
1362 {5, "sbfd"},
1363 {6, "saxfad"},
1364 {6, "sabfd"},
1365 {7, "saafd"},
1366 {8, "saafad"},
1367 {9, "safaad"},
1368 });
1369}
1370
1371} // namespace ripple
T append(T... args)
T back(T... args)
T begin(T... args)
T bind(T... args)
Stream debug() const
Definition: Journal.h:328
Stream info() const
Definition: Journal.h:334
Stream trace() const
Severity stream access functions.
Definition: Journal.h:322
Stream warn() const
Definition: Journal.h:340
virtual OrderBookDB & getOrderBookDB()=0
virtual JobQueue & getJobQueue()=0
virtual Logs & logs()=0
A currency issued by an account.
Definition: Issue.h:36
AccountID account
Definition: Issue.h:39
Currency currency
Definition: Issue.h:38
std::unique_ptr< LoadEvent > makeLoadEvent(JobType t, std::string const &name)
Return a scoped LoadEvent.
Definition: JobQueue.cpp:178
bool isBookToXRP(Issue const &)
std::vector< Book > getBooksByTakerPays(Issue const &)
int getBookSize(Issue const &)
bool issueMatchesOrigin(Issue const &)
Definition: Pathfinder.cpp:702
void rankPaths(int maxPaths, STPathSet const &paths, std::vector< PathRank > &rankedPaths, std::function< bool(void)> const &continueCallback)
Definition: Pathfinder.cpp:490
bool findPaths(int searchLevel, std::function< bool(void)> const &continueCallback={})
Definition: Pathfinder.cpp:197
std::optional< AccountID > mSrcIssuer
Definition: Pathfinder.h:201
STAmount mDstAmount
Definition: Pathfinder.h:199
STPathSet mCompletePaths
Definition: Pathfinder.h:213
AccountID mSrcAccount
Definition: Pathfinder.h:196
std::unique_ptr< LoadEvent > m_loadEvent
Definition: Pathfinder.h:209
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, Application &app)
Construct a pathfinder without an issuer.
Definition: Pathfinder.cpp:160
std::shared_ptr< RippleLineCache > mRLCache
Definition: Pathfinder.h:210
TER getPathLiquidity(STPath const &path, STAmount const &minDstAmount, STAmount &amountOut, uint64_t &qualityOut) const
Definition: Pathfinder.cpp:345
AccountID mEffectiveDst
Definition: Pathfinder.h:198
std::map< PathType, STPathSet > mPaths
Definition: Pathfinder.h:215
Currency mSrcCurrency
Definition: Pathfinder.h:200
static std::uint32_t const afADD_ACCOUNTS
Definition: Pathfinder.h:223
Application & app_
Definition: Pathfinder.h:219
void computePathRanks(int maxPaths, std::function< bool(void)> const &continueCallback={})
Compute the rankings of the paths.
Definition: Pathfinder.cpp:412
STAmount mRemainingAmount
The amount remaining from mSrcAccount after the default liquidity has been removed.
Definition: Pathfinder.h:205
bool isNoRippleOut(STPath const &currentPath)
Definition: Pathfinder.cpp:909
void addLinks(STPathSet const &currentPaths, STPathSet &incompletePaths, int addFlags, std::function< bool(void)> const &continueCallback)
Definition: Pathfinder.cpp:783
static std::uint32_t const afADD_BOOKS
Definition: Pathfinder.h:226
int getPathsOut(Currency const &currency, AccountID const &account, LineDirection direction, bool isDestCurrency, AccountID const &dest, std::function< bool(void)> const &continueCallback)
Definition: Pathfinder.cpp:713
beast::Journal const j_
Definition: Pathfinder.h:220
bool isNoRipple(AccountID const &fromAccount, AccountID const &toAccount, Currency const &currency)
Definition: Pathfinder.cpp:892
STPathElement mSource
Definition: Pathfinder.h:212
STPathSet & addPathsForType(PathType const &type, std::function< bool(void)> const &continueCallback)
Definition: Pathfinder.cpp:800
static std::uint32_t const afOB_XRP
Definition: Pathfinder.h:229
static void initPathTable()
hash_map< Issue, int > mPathsOutCountMap
Definition: Pathfinder.h:217
std::vector< NodeType > PathType
Definition: Pathfinder.h:93
std::vector< PathRank > mPathRanks
Definition: Pathfinder.h:214
AccountID mDstAccount
Definition: Pathfinder.h:197
void addLink(STPath const &currentPath, STPathSet &incompletePaths, int addFlags, std::function< bool(void)> const &continueCallback)
Definition: Pathfinder.cpp:944
STPathSet getBestPaths(int maxPaths, STPath &fullLiquidityPath, STPathSet const &extraPaths, AccountID const &srcIssuer, std::function< bool(void)> const &continueCallback={})
Definition: Pathfinder.cpp:568
static std::uint32_t const afAC_LAST
Definition: Pathfinder.h:235
std::shared_ptr< ReadView const > mLedger
Definition: Pathfinder.h:208
STAmount mSrcAmount
Definition: Pathfinder.h:202
static std::uint32_t const afOB_LAST
Definition: Pathfinder.h:232
A wrapper which makes credits unavailable to balances.
Currency const & getCurrency() const
Definition: STAmount.h:493
std::string getFullText() const override
Definition: STAmount.cpp:537
bool native() const noexcept
Definition: STAmount.h:449
Currency const & getCurrency() const
Definition: STPathSet.h:366
AccountID const & getAccountID() const
Definition: STPathSet.h:360
auto getNodeType() const
Definition: STPathSet.h:322
bool empty() const
Definition: STPathSet.h:508
void push_back(STPath const &e)
Definition: STPathSet.h:514
bool assembleAdd(STPath const &base, STPathElement const &tail)
Definition: STPathSet.cpp:127
Json::Value getJson(JsonOptions) const override
Definition: STPathSet.cpp:204
std::vector< STPath >::size_type size() const
Definition: STPathSet.h:502
bool empty() const
Definition: STPathSet.h:404
std::vector< STPathElement >::const_iterator end() const
Definition: STPathSet.h:429
Json::Value getJson(JsonOptions) const
Definition: STPathSet.cpp:177
void push_back(STPathElement const &e)
Definition: STPathSet.h:410
bool hasSeen(AccountID const &account, Currency const &currency, AccountID const &issuer) const
Definition: STPathSet.cpp:161
std::vector< STPathElement >::size_type size() const
Definition: STPathSet.h:398
std::vector< STPathElement >::const_reference back() const
Definition: STPathSet.h:441
void emplace_back(Args &&... args)
Definition: STPathSet.h:417
bool isZero() const
Definition: base_uint.h:540
static Output rippleCalculate(PaymentSandbox &view, STAmount const &saMaxAmountReq, STAmount const &saDstAmountReq, AccountID const &uDstAccountID, AccountID const &uSrcAccountID, STPathSet const &spsPaths, Logs &l, Input const *const pInputs=nullptr)
Definition: RippleCalc.cpp:31
T clear(T... args)
T empty(T... args)
T end(T... args)
Keylet line(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition: Indexes.cpp:235
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition: Indexes.cpp:175
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: algorithm.h:26
base_uint< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition: AccountID.h:49
STAmount divide(STAmount const &amount, Rate const &rate)
Definition: Rate2.cpp:93
STAmount convertAmount(STAmount const &amt, bool all)
bool isXRP(AccountID const &c)
Definition: AccountID.h:91
AccountID const & xrpAccount()
Compute AccountID from public key.
Definition: AccountID.cpp:178
bool convertAllCheck(STAmount const &a)
@ lsfHighNoRipple
@ lsfRequireAuth
@ lsfLowNoRipple
@ lsfGlobalFreeze
static bool isDefaultPath(STPath const &path)
Definition: Pathfinder.cpp:456
std::uint64_t getRate(STAmount const &offerOut, STAmount const &offerIn)
Definition: STAmount.cpp:483
@ tefEXCEPTION
Definition: TER.h:172
static STPath removeIssuer(STPath const &path)
Definition: Pathfinder.cpp:475
std::string transToken(TER code)
Definition: TER.cpp:257
Currency const & xrpCurrency()
XRP currency.
Definition: UintTypes.cpp:119
@ tesSUCCESS
Definition: TER.h:242
std::string to_string(base_uint< Bits, Tag > const &a)
Definition: base_uint.h:630
STAmount largestAmount(STAmount const &amt)
@ tapNONE
Definition: ApplyView.h:31
void addUniquePath(STPathSet &pathSet, STPath const &path)
Definition: Pathfinder.cpp:931
@ jtPATH_FIND
Definition: Job.h:83
LineDirection
Describes how an account was found in a path, and how to find the next set of paths.
Definition: TrustLine.h:41
T pop_back(T... args)
T push_back(T... args)
T reserve(T... args)
T sort(T... args)
T value(T... args)
T what(T... args)