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