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