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