Fix pathfinding with multiple issuers for one currency (RIPD-618).

* Allow pathfinding requests where the starting currency may have
  multiple issuers.

* Cache paths over all issuers to avoid repeating work.

* Clear the ledger checkpoint in one retry case.

* Add an additional node at the front of paths when the starting issuer
  is not the source account.
This commit is contained in:
Tom Ritchford
2014-10-27 14:45:58 -04:00
parent 6904e66384
commit bb44bdd047
7 changed files with 356 additions and 105 deletions

View File

@@ -22,6 +22,89 @@
namespace ripple {
class FindPaths::Impl {
public:
Impl (
RippleLineCache::ref cache,
Account const& srcAccount,
Account const& dstAccount,
STAmount const& dstAmount,
int searchLevel,
unsigned int maxPaths)
: cache_ (cache),
srcAccount_ (srcAccount),
dstAccount_ (dstAccount),
dstAmount_ (dstAmount),
searchLevel_ (searchLevel),
maxPaths_ (maxPaths)
{
}
bool findPathsForIssue (
Issue const& issue,
STPathSet& pathsOut,
STPath& fullLiquidityPath)
{
if (auto& pathfinder = getPathFinder (issue.currency))
{
pathsOut = pathfinder->getBestPaths (
maxPaths_, fullLiquidityPath, issue.account);
return true;
}
return false;
}
private:
hash_map<Currency, std::unique_ptr<Pathfinder>> currencyMap_;
RippleLineCache::ref cache_;
Account const srcAccount_;
Account const dstAccount_;
STAmount const dstAmount_;
int const searchLevel_;
unsigned int const maxPaths_;
std::unique_ptr<Pathfinder> const& getPathFinder (Currency const& currency)
{
auto i = currencyMap_.find (currency);
if (i != currencyMap_.end ())
return i->second;
auto pathfinder = std::make_unique<Pathfinder> (
cache_, srcAccount_, dstAccount_, currency, dstAmount_);
if (pathfinder->findPaths (searchLevel_))
pathfinder->computePathRanks (maxPaths_);
else
pathfinder.reset (); // It's a bad request - clear it.
return currencyMap_[currency] = std::move (pathfinder);
// TODO(tom): why doesn't this faster way compile?
// return currencyMap_.insert (i, std::move (pathfinder)).second;
}
};
FindPaths::FindPaths (
RippleLineCache::ref cache,
Account const& srcAccount,
Account const& dstAccount,
STAmount const& dstAmount,
int level,
unsigned int maxPaths)
: impl_ (std::make_unique<Impl> (
cache, srcAccount, dstAccount, dstAmount, level, maxPaths))
{
}
FindPaths::~FindPaths() = default;
bool FindPaths::findPathsForIssue (
Issue const& issue,
STPathSet& pathsOut,
STPath& fullLiquidityPath)
{
return impl_->findPathsForIssue (issue, pathsOut, fullLiquidityPath);
}
bool findPathsForOneIssuer (
RippleLineCache::ref cache,
Account const& srcAccount,
@@ -44,9 +127,9 @@ bool findPathsForOneIssuer (
if (!pf.findPaths (searchLevel))
return false;
// Yes, ensurePathsAreComplete is called BEFORE we compute the paths...
pf.ensurePathsAreComplete (pathsOut);
pathsOut = pf.getBestPaths(maxPaths, fullLiquidityPath);
pf.addPathsFromPreviousPathfinding (pathsOut);
pf.computePathRanks (maxPaths);
pathsOut = pf.getBestPaths(maxPaths, fullLiquidityPath, srcIssue.account);
return true;
}

View File

@@ -22,6 +22,43 @@
namespace ripple {
class FindPaths
{
public:
FindPaths (
RippleLineCache::ref cache,
Account const& srcAccount,
Account const& dstAccount,
STAmount const& dstAmount,
/** searchLevel is the maximum search level allowed in an output path.
*/
int searchLevel,
/** maxPaths is the maximum number of paths that can be returned in
pathsOut. */
unsigned int const maxPaths);
~FindPaths();
bool findPathsForIssue (
Issue const& issue,
/** On input, pathsOut contains any paths you want to ensure are
included if still good.
On output, pathsOut will have any additional paths found. Only
non-default paths without source or destination will be added. */
STPathSet& pathsOut,
/** On input, fullLiquidityPath must be an empty STPath.
On output, if fullLiquidityPath is non-empty, it contains one extra
path that can move the entire liquidity requested. */
STPath& fullLiquidityPath);
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
bool findPathsForOneIssuer (
RippleLineCache::ref cache,
Account const& srcAccount,

View File

@@ -331,6 +331,11 @@ int PathRequest::parseJson (Json::Value const& jvParams, bool complete)
return PFR_PJ_INVALID;
}
if (uCur.isNonZero() && uIss.isZero())
{
uIss = raSrcAccount.getAccountID();
}
sciSourceCurrencies.insert ({uCur, uIss});
}
}
@@ -435,6 +440,13 @@ Json::Value PathRequest::doUpdate (RippleLineCache::ref cache, bool fast)
bool found = false;
FindPaths fp (
cache,
raSrcAccount.getAccountID (),
raDstAccount.getAccountID (),
saDstAmount,
iLevel,
4); // iMaxPaths
for (auto const& currIssuer: sourceCurrencies)
{
{
@@ -448,14 +460,8 @@ Json::Value PathRequest::doUpdate (RippleLineCache::ref cache, bool fast)
}
STPathSet& spsPaths = mContext[currIssuer];
STPath fullLiquidityPath;
auto valid = findPathsForOneIssuer (
cache,
raSrcAccount.getAccountID(),
raDstAccount.getAccountID(),
auto valid = fp.findPathsForIssue (
currIssuer,
saDstAmount,
iLevel,
4, // iMaxPaths
spsPaths,
fullLiquidityPath);
CondLog (!valid, lsDEBUG, PathRequest)
@@ -464,12 +470,12 @@ Json::Value PathRequest::doUpdate (RippleLineCache::ref cache, bool fast)
if (valid)
{
LedgerEntrySet lesSandbox (cache->getLedger (), tapNONE);
auto& account = !isXRP (currIssuer.account)
auto& sourceAccount = !isXRP (currIssuer.account)
? currIssuer.account
: isXRP (currIssuer.currency)
? xrpAccount()
: raSrcAccount.getAccountID ();
STAmount saMaxAmount ({currIssuer.currency, account}, 1);
STAmount saMaxAmount ({currIssuer.currency, sourceAccount}, 1);
saMaxAmount.negate ();
m_journal.debug << iIdentifier
@@ -488,7 +494,9 @@ Json::Value PathRequest::doUpdate (RippleLineCache::ref cache, bool fast)
m_journal.debug
<< iIdentifier << " Trying with an extra path element";
spsPaths.push_back (fullLiquidityPath);
rc = path::RippleCalc::rippleCalculate (lesSandbox,
lesSandbox.clear();
rc = path::RippleCalc::rippleCalculate (
lesSandbox,
saMaxAmount,
saDstAmount,
raDstAccount.getAccountID (),
@@ -507,6 +515,8 @@ Json::Value PathRequest::doUpdate (RippleLineCache::ref cache, bool fast)
if (rc.result () == tesSUCCESS)
{
Json::Value jvEntry (Json::objectValue);
rc.actualAmountIn.setIssuer (sourceAccount);
jvEntry["source_amount"] = rc.actualAmountIn.getJson (0);
jvEntry["paths_computed"] = spsPaths.getJson (0);
found = true;

View File

@@ -64,17 +64,10 @@ namespace {
// width of path
// correct currency at the end.
struct PathRank
{
std::uint64_t quality;
std::uint64_t length;
STAmount liquidity;
int index;
};
// Compare two PathRanks. A better PathRank is lower, so the best are sorted to
// the beginning.
bool comparePathRank (PathRank const& a, PathRank const& b)
bool comparePathRank (
Pathfinder::PathRank const& a, Pathfinder::PathRank const& b)
{
// 1) Higher quality (lower cost) is better
if (a.quality != b.quality)
@@ -179,12 +172,32 @@ Pathfinder::Pathfinder (
mDstAmount (saDstAmount),
mSrcCurrency (uSrcCurrency),
mSrcIssuer (uSrcIssuer),
mSrcAmount ({mSrcCurrency, mSrcIssuer}, 1u, 0, true),
mSrcAmount ({uSrcCurrency, uSrcIssuer}, 1u, 0, true),
mLedger (cache->getLedger ()),
mRLCache (cache)
{
}
Pathfinder::Pathfinder (
RippleLineCache::ref cache,
Account const& uSrcAccount,
Account const& uDstAccount,
Currency const& uSrcCurrency,
STAmount const& saDstAmount)
: mSrcAccount (uSrcAccount),
mDstAccount (uDstAccount),
mDstAmount (saDstAmount),
mSrcCurrency (uSrcCurrency),
mSrcAmount ({uSrcCurrency, uSrcAccount}, 1u, 0, true),
mLedger (cache->getLedger ()),
mRLCache (cache)
{
}
Pathfinder::~Pathfinder()
{
}
bool Pathfinder::findPaths (int searchLevel)
{
if (mDstAmount == zero)
@@ -210,19 +223,21 @@ bool Pathfinder::findPaths (int searchLevel)
m_loadEvent = getApp ().getJobQueue ().getLoadEvent (
jtPATH_FIND, "FindPath");
auto currencyIsXRP = isXRP (mSrcCurrency);
auto issuerIsXRP = isXRP (mSrcIssuer);
bool useIssuerAccount = !currencyIsXRP && !issuerIsXRP;
auto& account = useIssuerAccount ? mSrcIssuer : mSrcAccount;
bool useIssuerAccount
= mSrcIssuer && !currencyIsXRP && !isXRP (*mSrcIssuer);
auto& account = useIssuerAccount ? *mSrcIssuer : mSrcAccount;
auto issuer = currencyIsXRP ? Account() : account;
mSource = STPathElement (account, mSrcCurrency, issuer);
auto issuerString = mSrcIssuer
? to_string (*mSrcIssuer) : std::string ("none");
WriteLog (lsTRACE, Pathfinder)
<< "findPaths>"
<< " mSrcAccount=" << mSrcAccount
<< " mDstAccount=" << mDstAccount
<< " mDstAmount=" << mDstAmount.getFullText ()
<< " mSrcCurrency=" << mSrcCurrency
<< " mSrcIssuer=" << mSrcIssuer;
<< " mSrcIssuer=" << issuerString;
if (!mLedger)
{
@@ -318,11 +333,10 @@ bool Pathfinder::findPaths (int searchLevel)
return true;
}
void Pathfinder::ensurePathsAreComplete (STPathSet& pathsOut)
void Pathfinder::addPathsFromPreviousPathfinding (STPathSet& pathsOut)
{
// Add any result paths that aren't in mCompletePaths.
// TODO(tom): this is also quadratic in the size of the paths.
// TODO(tom): how could a path possibly not be in mCompletePaths?
for (auto const& path : pathsOut)
{
// make sure no paths were lost
@@ -338,9 +352,7 @@ void Pathfinder::ensurePathsAreComplete (STPathSet& pathsOut)
}
}
// TODO(tom): this asssert never triggers. We should probably
// remove this whole loop, which might be expensive.
assert (found);
// TODO(tom): write a test that exercises this code path.
if (!found)
mCompletePaths.push_back (path);
}
@@ -410,14 +422,23 @@ TER Pathfinder::getPathLiquidity (
}
}
STPathSet Pathfinder::getBestPaths (int maxPaths, STPath& fullLiquidityPath)
namespace {
// Return the smallest amount of useful liquidity for a given amount, and the
// total number of paths we have to evaluate.
STAmount smallestUsefulAmount (STAmount const& amount, int maxPaths)
{
assert (fullLiquidityPath.empty ());
return divide (amount, STAmount (maxPaths + 2), amount);
}
} // namespace
void Pathfinder::computePathRanks (int maxPaths)
{
if (mCompletePaths.size () <= maxPaths)
return mCompletePaths;
return;
STAmount remaining = mDstAmount;
mRemainingAmount = mDstAmount;
// Must subtract liquidity in default path from remaining amount.
try
@@ -439,7 +460,7 @@ STPathSet Pathfinder::getBestPaths (int maxPaths, STPath& fullLiquidityPath)
{
WriteLog (lsDEBUG, Pathfinder)
<< "Default path contributes: " << rc.actualAmountIn;
remaining -= rc.actualAmountOut;
mRemainingAmount -= rc.actualAmountOut;
}
else
{
@@ -453,12 +474,9 @@ STPathSet Pathfinder::getBestPaths (int maxPaths, STPath& fullLiquidityPath)
}
// Ignore paths that move only very small amounts.
// TODO(tom): the logic of "very small" is pretty arbitrary here.
auto saMinDstAmount = divide (
mDstAmount, STAmount (maxPaths + 2), mDstAmount);
auto saMinDstAmount = smallestUsefulAmount (mDstAmount, maxPaths);
// Get the PathRank for each path.
std::vector<PathRank> pathRanks;
for (int i = 0; i < mCompletePaths.size (); ++i)
{
auto const& currentPath = mCompletePaths[i];
@@ -479,76 +497,130 @@ STPathSet Pathfinder::getBestPaths (int maxPaths, STPath& fullLiquidityPath)
"findPaths: quality: " << uQuality <<
": " << currentPath.getJson (0);
pathRanks.push_back ({uQuality, currentPath.size (), liquidity, i});
mPathRanks.push_back ({uQuality, currentPath.size (), liquidity, i});
}
}
std::sort (mPathRanks.begin (), mPathRanks.end (), comparePathRank);
}
static bool isDefaultPath (STPath const& path)
{
// TODO(tom): default paths can consist of more than just an account:
// https://forum.ripple.com/viewtopic.php?f=2&t=8206&start=10#p57713
//
// JoelKatz writes:
// So the test for whether a path is a default path is incorrect. I'm not
// sure it's worth the complexity of fixing though. If we are going to fix
// it, I'd suggest doing it this way:
//
// 1) Compute the default path, probably by using 'expandPath' to expand an
// empty path. 2) Chop off the source and destination nodes.
//
// 3) In the pathfinding loop, if the source issuer is not the sender,
// reject all paths that don't begin with the issuer's account node or match
// the path we built at step 2.
return path.size() == 1;
}
static STPath removeIssuer (STPath const& path)
{
// This path starts with the issuer, which is already implied
// so remove the head node
STPath ret;
for (auto it = path.begin() + 1; it != path.end(); ++it)
ret.push_back (*it);
return ret;
}
STPathSet Pathfinder::getBestPaths (
int maxPaths,
STPath& fullLiquidityPath,
Account const& srcIssuer)
{
assert (fullLiquidityPath.empty ());
const bool issuerIsSender = isXRP (mSrcCurrency) || (srcIssuer == mSrcAccount);
if (issuerIsSender && (mCompletePaths.size () <= maxPaths))
return mCompletePaths;
STPathSet bestPaths;
if (pathRanks.size ())
// The best PathRanks are now at the start. Pull off enough of them to
// fill bestPaths, then look through the rest for the best individual
// path that can satisfy the entire liquidity - if one exists.
STAmount remaining = mRemainingAmount;
for (auto& pathRank: mPathRanks)
{
std::sort (pathRanks.begin (), pathRanks.end (), comparePathRank);
auto iPathsLeft = maxPaths - bestPaths.size ();
if (!(iPathsLeft > 0 || fullLiquidityPath.empty ()))
break;
// The best PathRanks are now at the start. Pull off enough of them to
// fill bestPaths, then look through the rest for the best individual
// path that can satisfy the entire liquidity - if one exists.
for (auto& pathRank: pathRanks)
auto& path = mCompletePaths[pathRank.index];
assert (!path.empty ());
if (path.empty ())
continue;
bool startsWithIssuer = false;
if (! issuerIsSender)
{
auto iPathsLeft = maxPaths - bestPaths.size ();
if (!(iPathsLeft > 0 || fullLiquidityPath.empty ()))
break;
if (iPathsLeft > 1 ||
(iPathsLeft > 0 && pathRank.liquidity >= remaining))
if (path.front ().getAccountID() != srcIssuer)
continue;
if (isDefaultPath (path))
{
// last path must fill
--iPathsLeft;
remaining -= pathRank.liquidity;
bestPaths.push_back (mCompletePaths[pathRank.index]);
}
else if (iPathsLeft == 0 &&
pathRank.liquidity >= mDstAmount &&
fullLiquidityPath.empty ())
{
// We found an extra path that can move the whole amount.
fullLiquidityPath = mCompletePaths[pathRank.index];
WriteLog (lsDEBUG, Pathfinder) <<
"Found extra full path: " << fullLiquidityPath.getJson (0);
}
else
{
WriteLog (lsDEBUG, Pathfinder) <<
"Skipping a non-filling path: " <<
mCompletePaths[pathRank.index].getJson (0);
continue;
}
startsWithIssuer = true;
}
if (remaining > zero)
if (iPathsLeft > 1 ||
(iPathsLeft > 0 && pathRank.liquidity >= remaining))
// last path must fill
{
assert (fullLiquidityPath.empty ());
WriteLog (lsINFO, Pathfinder) <<
"Paths could not send " << remaining << " of " << mDstAmount;
--iPathsLeft;
remaining -= pathRank.liquidity;
bestPaths.push_back (startsWithIssuer ? removeIssuer (path) : path);
}
else if (iPathsLeft == 0 &&
pathRank.liquidity >= mDstAmount &&
fullLiquidityPath.empty ())
{
// We found an extra path that can move the whole amount.
fullLiquidityPath = (startsWithIssuer ? removeIssuer (path) : path);
WriteLog (lsDEBUG, Pathfinder) <<
"Found extra full path: " << fullLiquidityPath.getJson (0);
}
else
{
WriteLog (lsDEBUG, Pathfinder) <<
"findPaths: RESULTS: " << bestPaths.getJson (0);
"Skipping a non-filling path: " << path.getJson (0);
}
}
if (remaining > zero)
{
assert (fullLiquidityPath.empty ());
WriteLog (lsINFO, Pathfinder) <<
"Paths could not send " << remaining << " of " << mDstAmount;
}
else
{
WriteLog (lsDEBUG, Pathfinder) <<
"findPaths: RESULTS: non-defaults filtered away";
"findPaths: RESULTS: " << bestPaths.getJson (0);
}
return bestPaths;
}
bool Pathfinder::issueMatchesOrigin (Issue const& issue)
{
return issue.currency == mSrcCurrency &&
(isXRP (issue.currency) ||
issue.account == mSrcIssuer ||
issue.account == mSrcAccount);
bool matchingCurrency = (issue.currency == mSrcCurrency);
bool matchingAccount =
isXRP (issue.currency) ||
(mSrcIssuer && issue.account == mSrcIssuer) ||
issue.account == mSrcAccount;
return matchingCurrency && matchingAccount;
}
int Pathfinder::getPathsOut (
@@ -624,7 +696,7 @@ void Pathfinder::addLinks (
int addFlags)
{
WriteLog (lsDEBUG, Pathfinder)
<< "addLink< on " << currentPaths.size()
<< "addLink< on " << currentPaths.size ()
<< " source(s), flags=" << addFlags;
for (auto const& path: currentPaths)
addLink (path, incompletePaths, addFlags);
@@ -725,11 +797,9 @@ bool Pathfinder::isNoRippleOut (STPath const& currentPath)
if (!(endElement.getNodeType () & STPathElement::typeAccount))
return false;
// What account are we leaving?
// TODO(tom): clarify what's going on here when we only have one item in the
// path.
// TODO(tom): why aren't we checking that the previous node is also an
// account?
// If there's only one item in the path, return true if that item specifies
// no ripple on the output. A path with no ripple on its output can't be
// followed by a link with no ripple on its input.
auto const& fromAccount = (currentPath.size () == 1)
? mSrcAccount
: (currentPath.end () - 2)->getAccountID ();
@@ -929,7 +999,7 @@ void Pathfinder::addLink (
auto books = getApp ().getOrderBookDB ().getBooksByTakerPays(
{uEndCurrency, uEndIssuer});
WriteLog (lsTRACE, Pathfinder)
<< books.size() << " books found from this currency/issuer";
<< books.size () << " books found from this currency/issuer";
for (auto const& book : books)
{

View File

@@ -31,6 +31,7 @@ namespace ripple {
class Pathfinder
{
public:
/** Construct a pathfinder with an issuer.*/
Pathfinder (
RippleLineCache::ref cache,
Account const& srcAccount,
@@ -39,18 +40,35 @@ public:
Account const& uSrcIssuer,
STAmount const& dstAmount);
/** Construct a pathfinder without an issuer.*/
Pathfinder (
RippleLineCache::ref cache,
Account const& srcAccount,
Account const& dstAccount,
Currency const& uSrcCurrency,
STAmount const& dstAmount);
~Pathfinder();
static void initPathTable ();
bool findPaths (int searchLevel);
void ensurePathsAreComplete (STPathSet&);
/** Make sure that all the input paths are included in mCompletePaths. */
void addPathsFromPreviousPathfinding (STPathSet&);
/** Compute the rankings of the paths. */
void computePathRanks (int maxPaths);
/* Get the best paths, up to maxPaths in number, from mCompletePaths.
On return, if fullLiquidityPath is not empty, then it contains the best
additional single path which can consume all the liquidity.
*/
STPathSet getBestPaths (int maxPaths, STPath& fullLiquidityPath);
STPathSet getBestPaths (
int maxPaths,
STPath& fullLiquidityPath,
Account const& srcIssuer);
enum NodeType
{
@@ -76,9 +94,17 @@ public:
pt_nonXRP_to_nonXRP // Destination currency is NOT the same as source.
};
struct PathRank
{
std::uint64_t quality;
std::uint64_t length;
STAmount liquidity;
int index;
};
private:
/*
Call graph of methoids
Call graph of Pathfinder methods.
findPaths:
addPathsForType:
@@ -89,12 +115,14 @@ private:
isNoRippleOut:
isNoRipple
ensurePathsAreComplete
addPathsFromPreviousPathfinding
getBestPaths:
computePathRanks:
rippleCalculate
getPathLiquidity:
rippleCalculate
getBestPaths
*/
@@ -143,8 +171,11 @@ private:
Account mDstAccount;
STAmount mDstAmount;
Currency mSrcCurrency;
Account mSrcIssuer;
boost::optional<Account> mSrcIssuer;
STAmount mSrcAmount;
/** The amount remaining from mSrcAccount after the default liquidity has
been removed. */
STAmount mRemainingAmount;
Ledger::pointer mLedger;
LoadEvent::pointer m_loadEvent;
@@ -152,6 +183,7 @@ private:
STPathElement mSource;
STPathSet mCompletePaths;
std::vector<PathRank> mPathRanks;
std::map<PathType, STPathSet> mPaths;
hash_map<Issue, int> mPathsOutCountMap;

View File

@@ -135,6 +135,8 @@ Json::Value doRipplePathFind (RPC::Context& context)
// Fill in currencies destination will accept
Json::Value jvDestCur (Json::arrayValue);
// TODO(tom): this could be optimized the same way that
// PathRequest::doUpdate() is - if we don't obsolete this code first.
auto usDestCurrID = accountDestCurrencies (raDst, cache, true);
for (auto const& uCurrency: usDestCurrID)
jvDestCur.append (to_string (uCurrency));
@@ -144,6 +146,29 @@ Json::Value doRipplePathFind (RPC::Context& context)
Json::Value jvArray (Json::arrayValue);
int level = getConfig().PATH_SEARCH_OLD;
if ((getConfig().PATH_SEARCH_MAX > level)
&& !getApp().getFeeTrack().isLoadedLocal())
{
++level;
}
if (context.params_.isMember("depth")
&& context.params_["depth"].isIntegral())
{
int rLev = context.params_["search_depth"].asInt ();
if ((rLev < level) || (context.role_ == Config::ADMIN))
level = rLev;
}
FindPaths fp (
cache,
raSrc.getAccountID(),
raDst.getAccountID(),
saDstAmount,
level,
4); // max paths
for (unsigned int i = 0; i != jvSrcCurrencies.size (); ++i)
{
Json::Value jvSource = jvSrcCurrencies[i];
@@ -203,14 +228,8 @@ Json::Value doRipplePathFind (RPC::Context& context)
}
STPath fullLiquidityPath;
auto valid = findPathsForOneIssuer(
cache,
raSrc.getAccountID(),
raDst.getAccountID(),
auto valid = fp.findPathsForIssue (
{uSrcCurrencyID, uSrcIssuerID},
saDstAmount,
level,
4, // iMaxPaths
spsComputed,
fullLiquidityPath);
if (!valid)

View File

@@ -126,7 +126,6 @@ static Json::Value signPayment(
&& params.isMember ("build_path"))
{
// Need a ripple path.
STPathSet spsPaths;
Currency uSrcCurrencyID;
Account uSrcIssuerID;
@@ -154,6 +153,7 @@ static Json::Value signPayment(
return rpcError (rpcTOO_BUSY);
auto cache = std::make_shared<RippleLineCache> (lSnapshot);
STPathSet spsPaths;
STPath fullLiquidityPath;
auto valid = findPathsForOneIssuer (
cache,