mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-20 11:05:54 +00:00
846 lines
35 KiB
C++
846 lines
35 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
Copyright (c) 2011-2013, OpenCoin, Inc.
|
|
*/
|
|
//==============================================================================
|
|
|
|
SETUP_LOG (Pathfinder)
|
|
|
|
/*
|
|
we just need to find a succession of the highest quality paths there until we find enough width
|
|
|
|
Don't do branching within each path
|
|
|
|
We have a list of paths we are working on but how do we compare the ones that are terminating in a different currency?
|
|
|
|
Loops
|
|
|
|
TODO: what is a good way to come up with multiple paths?
|
|
Maybe just change the sort criteria?
|
|
first a low cost one and then a fat short one?
|
|
|
|
|
|
OrderDB:
|
|
getXRPOffers();
|
|
|
|
// return list of all orderbooks that want XRP
|
|
// return list of all orderbooks that want IssuerID
|
|
// return list of all orderbooks that want this issuerID and currencyID
|
|
*/
|
|
|
|
/*
|
|
Test sending to XRP
|
|
Test XRP to XRP
|
|
Test offer in middle
|
|
Test XRP to USD
|
|
Test USD to EUR
|
|
*/
|
|
|
|
// we sort the options by:
|
|
// cost of path
|
|
// length of path
|
|
// width of path
|
|
// correct currency at the end
|
|
|
|
// quality, length, liquidity, index
|
|
typedef boost::tuple<uint64, int, STAmount, unsigned int> path_LQ_t;
|
|
|
|
// Lower numbers have better quality. Sort higher quality first.
|
|
static bool bQualityCmp (const path_LQ_t& a, const path_LQ_t& b)
|
|
{
|
|
// 1) Higher quality (lower cost) is better
|
|
if (a.get<0> () != b.get<0> ())
|
|
return a.get<0> () < b.get<0> ();
|
|
|
|
// 2) More liquidity (higher volume) is better
|
|
if (a.get<2> () != b.get<2> ())
|
|
return a.get<2> () > b.get<2> ();
|
|
|
|
// 3) Shorter paths are better
|
|
if (a.get<1> () != b.get<1> ())
|
|
return a.get<1> () < b.get<1> ();
|
|
|
|
// 4) Tie breaker
|
|
return a.get<3> () > b.get<3> ();
|
|
}
|
|
|
|
// Return true, if path is a default path with an element.
|
|
// A path is a default path if it is implied via src, dst, send, and sendmax.
|
|
bool Pathfinder::bDefaultPath (const STPath& spPath)
|
|
{
|
|
if (2 >= spPath.mPath.size ())
|
|
{
|
|
// Empty path is a default. Don't need to add it to return set.
|
|
WriteLog (lsTRACE, Pathfinder) << "findPaths: empty path: direct";
|
|
|
|
return true;
|
|
}
|
|
|
|
if (!mPsDefault)
|
|
{
|
|
// No default path.
|
|
// There might not be a direct credit line or there may be no implied nodes
|
|
// in send and sendmax.
|
|
|
|
return false; // Didn't generate a default path. So can't match.
|
|
}
|
|
|
|
PathState::pointer pspCurrent = boost::make_shared<PathState> (mDstAmount, mSrcAmount);
|
|
|
|
if (pspCurrent)
|
|
{
|
|
bool bDefault;
|
|
LedgerEntrySet lesActive (mLedger, tapNONE);
|
|
|
|
WriteLog (lsTRACE, Pathfinder) << boost::str (boost::format ("bDefaultPath> mSrcAmount=%s mDstAmount=%s")
|
|
% mSrcAmount.getFullText ()
|
|
% mDstAmount.getFullText ());
|
|
|
|
// Expand the current path.
|
|
pspCurrent->setExpanded (lesActive, spPath, mDstAccountID, mSrcAccountID);
|
|
// XXX Need to report or act on errors returned in pspCurrent->terStatus.
|
|
|
|
// Determine if expanded current path is the default.
|
|
// When path is a default (implied). Don't need to add it to return set.
|
|
bDefault = pspCurrent->vpnNodes == mPsDefault->vpnNodes;
|
|
|
|
WriteLog (lsTRACE, Pathfinder) << "bDefaultPath: expanded path: " << pspCurrent->getJson ();
|
|
WriteLog (lsTRACE, Pathfinder) << "bDefaultPath: source path: " << spPath.getJson (0);
|
|
WriteLog (lsTRACE, Pathfinder) << "bDefaultPath: default path: " << mPsDefault->getJson ();
|
|
|
|
return bDefault;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
typedef std::pair<int, uint160> candidate_t;
|
|
static bool candCmp (uint32 seq, const candidate_t& first, const candidate_t& second)
|
|
{
|
|
if (first.first < second.first)
|
|
return false;
|
|
|
|
if (first.first > second.first)
|
|
return true;
|
|
|
|
return (first.first ^ seq) < (second.first ^ seq);
|
|
}
|
|
|
|
static int getEffectiveLength (const STPath& spPath)
|
|
{
|
|
// don't count exchanges to non-XRP currencies twice (only count the forced issuer account node)
|
|
int length = 0;
|
|
|
|
for (std::vector<STPathElement>::const_iterator it = spPath.begin (); it != spPath.end (); ++it)
|
|
{
|
|
if (it->isAccount () || it->getCurrency ().isZero ())
|
|
++length;
|
|
}
|
|
|
|
return length;
|
|
}
|
|
|
|
Pathfinder::Pathfinder (RippleLineCache::ref cache,
|
|
const RippleAddress& uSrcAccountID, const RippleAddress& uDstAccountID,
|
|
const uint160& uSrcCurrencyID, const uint160& uSrcIssuerID, const STAmount& saDstAmount, bool& bValid)
|
|
: mSrcAccountID (uSrcAccountID.getAccountID ()),
|
|
mDstAccountID (uDstAccountID.getAccountID ()),
|
|
mDstAmount (saDstAmount),
|
|
mSrcCurrencyID (uSrcCurrencyID),
|
|
mSrcIssuerID (uSrcIssuerID),
|
|
mSrcAmount (uSrcCurrencyID, uSrcIssuerID, 1u, 0, true),
|
|
mLedger (cache->getLedger ()), mRLCache (cache)
|
|
{
|
|
|
|
if (((mSrcAccountID == mDstAccountID) && (mSrcCurrencyID == mDstAmount.getCurrency ())) || mDstAmount.isZero ())
|
|
{
|
|
// no need to send to same account with same currency, must send non-zero
|
|
bValid = false;
|
|
mLedger.reset ();
|
|
return;
|
|
}
|
|
|
|
bValid = true;
|
|
|
|
getApp().getOrderBookDB ().setup (mLedger);
|
|
|
|
m_loadEvent = getApp().getJobQueue ().getLoadEvent (jtPATH_FIND, "FindPath");
|
|
|
|
// Construct the default path for later comparison.
|
|
|
|
PathState::pointer psDefault = boost::make_shared<PathState> (mDstAmount, mSrcAmount);
|
|
|
|
if (psDefault)
|
|
{
|
|
// Build the default path.
|
|
// Later, reject anything that expands to the default path as the default is sufficient.
|
|
|
|
LedgerEntrySet lesActive (mLedger, tapNONE);
|
|
|
|
WriteLog (lsTRACE, Pathfinder) << boost::str (boost::format ("Pathfinder> mSrcAmount=%s mDstAmount=%s")
|
|
% mSrcAmount.getFullText ()
|
|
% mDstAmount.getFullText ());
|
|
|
|
psDefault->setExpanded (lesActive, STPath (), mDstAccountID, mSrcAccountID);
|
|
|
|
if (tesSUCCESS == psDefault->terStatus)
|
|
{
|
|
// The default path works, remember it.
|
|
WriteLog (lsTRACE, Pathfinder) << "Pathfinder: default path: " << psDefault->getJson ();
|
|
|
|
mPsDefault = psDefault;
|
|
}
|
|
else
|
|
{
|
|
// The default path doesn't work.
|
|
WriteLog (lsTRACE, Pathfinder) << "Pathfinder: default path: NONE: " << transToken (psDefault->terStatus);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If possible, returns a single path.
|
|
// --> iMaxSteps: Maximum nodes in paths to return.
|
|
// --> iMaxPaths: Maximum number of paths to return.
|
|
// <-- retPathSet: founds paths not including default paths.
|
|
// Returns true if found paths.
|
|
//
|
|
// When generating a path set blindly, don't allow the empty path, it is implied by default.
|
|
// When generating a path set for estimates, allow an empty path instead of no paths to indicate a path exists. The caller will
|
|
// need to strip the empty path when submitting the transaction.
|
|
//
|
|
// Assumes rippling (not XRP to XRP)
|
|
//
|
|
// Leaves to the caller figuring out overall liquidity.
|
|
// Optimization opportunity: For some simple cases, this routine has figured out the overall liquidity.
|
|
bool Pathfinder::findPaths (const unsigned int iMaxSteps, const unsigned int iMaxPaths, STPathSet& spsDst)
|
|
{
|
|
bool bFound = false; // True, iff found a path.
|
|
|
|
WriteLog (lsTRACE, Pathfinder) << boost::str (boost::format ("findPaths> mSrcAccountID=%s mDstAccountID=%s mDstAmount=%s mSrcCurrencyID=%s mSrcIssuerID=%s")
|
|
% RippleAddress::createHumanAccountID (mSrcAccountID)
|
|
% RippleAddress::createHumanAccountID (mDstAccountID)
|
|
% mDstAmount.getFullText ()
|
|
% STAmount::createHumanCurrency (mSrcCurrencyID)
|
|
% RippleAddress::createHumanAccountID (mSrcIssuerID)
|
|
);
|
|
|
|
if (!mLedger)
|
|
{
|
|
WriteLog (lsDEBUG, Pathfinder) << "findPaths< no ledger";
|
|
|
|
return false;
|
|
}
|
|
|
|
LedgerEntrySet lesActive (mLedger, tapNONE);
|
|
boost::unordered_map<uint160, AccountItems::pointer> aiMap;
|
|
|
|
SLE::pointer sleSrc = lesActive.entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (mSrcAccountID));
|
|
|
|
if (!sleSrc)
|
|
{
|
|
WriteLog (lsDEBUG, Pathfinder) << boost::str (boost::format ("findPaths< no source"));
|
|
|
|
return false;
|
|
}
|
|
|
|
SLE::pointer sleDst = lesActive.entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (mDstAccountID));
|
|
|
|
if (!sleDst)
|
|
{
|
|
WriteLog (lsDEBUG, Pathfinder) << boost::str (boost::format ("findPaths< no dest"));
|
|
|
|
return false;
|
|
}
|
|
|
|
std::vector<STPath> vspResults;
|
|
std::queue<STPath> qspExplore; // Path stubs to explore.
|
|
|
|
STPath spSeed;
|
|
bool bForcedIssuer = !!mSrcCurrencyID && mSrcIssuerID != mSrcAccountID; // Source forced an issuer.
|
|
|
|
// The end is the cursor, start at the source account.
|
|
STPathElement speEnd (mSrcAccountID,
|
|
mSrcCurrencyID,
|
|
!!mSrcCurrencyID
|
|
? mSrcAccountID // Non-XRP, start with self as issuer.
|
|
: ACCOUNT_XRP);
|
|
|
|
// Build a path of one element: the source.
|
|
spSeed.addElement (speEnd);
|
|
|
|
if (bForcedIssuer)
|
|
{
|
|
// Add forced source issuer to seed, via issuer's account.
|
|
STPathElement speIssuer (mSrcIssuerID, mSrcCurrencyID, mSrcIssuerID);
|
|
|
|
spSeed.addElement (speEnd);
|
|
}
|
|
|
|
// Push the seed path to explore.
|
|
qspExplore.push (spSeed);
|
|
|
|
while (qspExplore.size ()) // Have paths to explore?
|
|
{
|
|
STPath spPath = qspExplore.front ();
|
|
|
|
qspExplore.pop (); // Pop the first path from the queue.
|
|
|
|
speEnd = spPath.mPath.back (); // Get the last node from the path.
|
|
|
|
if (!speEnd.mCurrencyID // Tail output is XRP.
|
|
&& !mDstAmount.getCurrency ()) // Which is dst currency.
|
|
{
|
|
// Done, cursor produces XRP and dest wants XRP.
|
|
|
|
// Remove implied source.
|
|
spPath.mPath.erase (spPath.mPath.begin ());
|
|
|
|
if (bForcedIssuer)
|
|
{
|
|
// Remove implied source issuer.
|
|
spPath.mPath.erase (spPath.mPath.begin ());
|
|
}
|
|
|
|
if (spPath.size ())
|
|
{
|
|
// There is an actual path element.
|
|
WriteLog (lsTRACE, Pathfinder) << "findPaths: adding path: " << spPath.getJson (0);
|
|
|
|
vspResults.push_back (spPath); // Potential result.
|
|
}
|
|
else
|
|
{
|
|
WriteLog (lsWARNING, Pathfinder) << "findPaths: empty path: XRP->XRP";
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (ShouldLog (lsTRACE, Pathfinder))
|
|
{
|
|
WriteLog (lsTRACE, Pathfinder) << boost::str (boost::format ("findPaths: spe: %s/%s: %s amt: %s")
|
|
% RippleAddress::createHumanAccountID (speEnd.mAccountID)
|
|
% RippleAddress::createHumanAccountID (speEnd.mIssuerID)
|
|
% RippleAddress::createHumanAccountID (mDstAccountID)
|
|
% RippleAddress::createHumanAccountID (mDstAmount.getIssuer ()));
|
|
|
|
WriteLog (lsTRACE, Pathfinder) << "findPaths: finish? account: " << (speEnd.mAccountID == mDstAccountID);
|
|
WriteLog (lsTRACE, Pathfinder) << "findPaths: finish? currency: " << (speEnd.mCurrencyID == mDstAmount.getCurrency ());
|
|
WriteLog (lsTRACE, Pathfinder) << "findPaths: finish? issuer: "
|
|
<< RippleAddress::createHumanAccountID (speEnd.mIssuerID)
|
|
<< " / "
|
|
<< RippleAddress::createHumanAccountID (mDstAmount.getIssuer ())
|
|
<< " / "
|
|
<< RippleAddress::createHumanAccountID (mDstAccountID);
|
|
WriteLog (lsTRACE, Pathfinder) << "findPaths: finish? issuer is desired: " << (speEnd.mIssuerID == mDstAmount.getIssuer ());
|
|
}
|
|
|
|
// YYY Allows going through self. Is this wanted?
|
|
if (speEnd.mAccountID == mDstAccountID // Tail is destination account.
|
|
&& speEnd.mCurrencyID == mDstAmount.getCurrency () // With correct output currency.
|
|
&& ( speEnd.mIssuerID == mDstAccountID // Dest always accepts own issuer.
|
|
|| mDstAmount.getIssuer () == mDstAccountID // Any issuer is good.
|
|
|| mDstAmount.getIssuer () == speEnd.mIssuerID)) // The desired issuer.
|
|
{
|
|
// Done, found a path to the destination.
|
|
// Cursor on the dest account with correct currency and issuer.
|
|
|
|
if (bDefaultPath (spPath))
|
|
{
|
|
WriteLog (lsTRACE, Pathfinder) << "findPaths: dropping: default path: " << spPath.getJson (0);
|
|
|
|
bFound = true;
|
|
}
|
|
else
|
|
{
|
|
// Remove implied nodes.
|
|
|
|
spPath.mPath.erase (spPath.mPath.begin ());
|
|
|
|
if (bForcedIssuer)
|
|
{
|
|
// Remove implied source issuer.
|
|
spPath.mPath.erase (spPath.mPath.begin ());
|
|
}
|
|
|
|
spPath.mPath.erase (spPath.mPath.begin () + spPath.mPath.size () - 1);
|
|
|
|
vspResults.push_back (spPath); // Potential result.
|
|
|
|
WriteLog (lsDEBUG, Pathfinder) << "findPaths: adding path: " << spPath.getJson (0);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
bool bContinued = false; // True, if wasn't a dead end.
|
|
|
|
WriteLog (lsTRACE, Pathfinder) <<
|
|
boost::str (boost::format ("findPaths: cursor: %s - %s/%s")
|
|
% RippleAddress::createHumanAccountID (speEnd.mAccountID)
|
|
% STAmount::createHumanCurrency (speEnd.mCurrencyID)
|
|
% RippleAddress::createHumanAccountID (speEnd.mIssuerID));
|
|
|
|
int length = getEffectiveLength (spPath.mPath);
|
|
|
|
if (length >= iMaxSteps)
|
|
{
|
|
// Path is at maximum size. Don't want to add more.
|
|
|
|
WriteLog (lsTRACE, Pathfinder)
|
|
<< boost::str (boost::format ("findPaths: dropping: path would exceed max steps"));
|
|
|
|
continue;
|
|
}
|
|
|
|
bool isLast = (length == (iMaxSteps - 1));
|
|
|
|
if (!speEnd.mCurrencyID)
|
|
{
|
|
// Cursor is for XRP, continue with qualifying books: XRP -> non-XRP
|
|
std::vector<OrderBook::pointer> xrpBooks;
|
|
getApp().getOrderBookDB ().getBooksByTakerPays (ACCOUNT_XRP, CURRENCY_XRP, xrpBooks);
|
|
BOOST_FOREACH (OrderBook::ref book, xrpBooks)
|
|
{
|
|
// New end is an order book with the currency and issuer.
|
|
|
|
if (!spPath.hasSeen (ACCOUNT_XRP, book->getCurrencyOut (), book->getIssuerOut ()) &&
|
|
!matchesOrigin (book->getCurrencyOut (), book->getIssuerOut ()) &&
|
|
(!isLast ||
|
|
(book->getCurrencyOut () == mDstAmount.getCurrency () &&
|
|
book->getIssuerOut () == mDstAccountID)))
|
|
{
|
|
// Not a order book already in path.
|
|
STPath spNew (spPath);
|
|
STPathElement speBook (ACCOUNT_XRP, book->getCurrencyOut (), book->getIssuerOut ());
|
|
STPathElement speAccount (book->getIssuerOut (), book->getCurrencyOut (), book->getIssuerOut ());
|
|
|
|
spNew.mPath.push_back (speBook); // Add the order book.
|
|
spNew.mPath.push_back (speAccount); // Add the account and currency
|
|
|
|
WriteLog (lsDEBUG, Pathfinder)
|
|
<< boost::str (boost::format ("findPaths: XRP -> %s/%s")
|
|
// % STAmount::createHumanCurrency(book->getCurrencyOut())
|
|
// % RippleAddress::createHumanAccountID(book->getIssuerOut())
|
|
% STAmount::createHumanCurrency (speBook.mCurrencyID)
|
|
% RippleAddress::createHumanAccountID (speBook.mIssuerID));
|
|
|
|
qspExplore.push (spNew);
|
|
|
|
bContinued = true;
|
|
}
|
|
}
|
|
|
|
CondLog (!bContinued, lsDEBUG, Pathfinder)
|
|
<< boost::str (boost::format ("findPaths: XRP -> dead end"));
|
|
}
|
|
else
|
|
{
|
|
// Last element is for non-XRP, continue by adding ripple lines and order books.
|
|
|
|
// Create new paths for each outbound account not already in the path.
|
|
|
|
SLE::pointer sleEnd = lesActive.entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (speEnd.mAccountID));
|
|
|
|
CondLog (!sleEnd, lsDEBUG, Pathfinder)
|
|
<< boost::str (boost::format ("findPaths: tail: %s/%s : ")
|
|
% RippleAddress::createHumanAccountID (speEnd.mAccountID)
|
|
% RippleAddress::createHumanAccountID (speEnd.mIssuerID));
|
|
|
|
if (sleEnd)
|
|
{
|
|
// On a non-XRP account:
|
|
// True, the cursor requires the next node to be authorized.
|
|
bool bRequireAuth = isSetBit (sleEnd->getFieldU32 (sfFlags), lsfRequireAuth);
|
|
bool dstCurrency = speEnd.mCurrencyID == mDstAmount.getCurrency ();
|
|
|
|
AccountItems& rippleLines (mRLCache->getRippleLines (speEnd.mAccountID));
|
|
|
|
std::vector< std::pair<int, uint160> > candidates;
|
|
candidates.reserve (rippleLines.getItems ().size ());
|
|
|
|
BOOST_FOREACH (AccountItem::ref item, rippleLines.getItems ())
|
|
{
|
|
RippleState* rspEntry = (RippleState*) item.get ();
|
|
const uint160& uPeerID = rspEntry->getAccountIDPeer ();
|
|
|
|
if (speEnd.mCurrencyID != rspEntry->getLimit ().getCurrency ())
|
|
{
|
|
// wrong currency
|
|
nothing ();
|
|
}
|
|
else if (spPath.hasSeen (uPeerID, speEnd.mCurrencyID, uPeerID) ||
|
|
((uPeerID == mSrcAccountID) && (uPeerID != mDstAccountID)))
|
|
{
|
|
// Peer is in path already. Ignore it to avoid a loop.
|
|
WriteLog (lsTRACE, Pathfinder) <<
|
|
boost::str (boost::format ("findPaths: SEEN: %s/%s -> %s/%s")
|
|
% RippleAddress::createHumanAccountID (speEnd.mAccountID)
|
|
% STAmount::createHumanCurrency (speEnd.mCurrencyID)
|
|
% RippleAddress::createHumanAccountID (uPeerID)
|
|
% STAmount::createHumanCurrency (speEnd.mCurrencyID));
|
|
}
|
|
else if (isLast && (!dstCurrency || (uPeerID != mDstAccountID)))
|
|
{
|
|
nothing ();
|
|
}
|
|
else if (!rspEntry->getBalance ().isPositive () // No IOUs to send.
|
|
&& (!rspEntry->getLimitPeer () // Peer does not extend credit.
|
|
|| -rspEntry->getBalance () >= rspEntry->getLimitPeer () // No credit left.
|
|
|| (bRequireAuth && !rspEntry->getAuth ()))) // Not authorized to hold credit.
|
|
{
|
|
// Path has no credit left. Ignore it.
|
|
WriteLog (lsTRACE, Pathfinder) <<
|
|
boost::str (boost::format ("findPaths: No credit: %s/%s -> %s/%s balance=%s limit=%s")
|
|
% RippleAddress::createHumanAccountID (speEnd.mAccountID)
|
|
% STAmount::createHumanCurrency (speEnd.mCurrencyID)
|
|
% RippleAddress::createHumanAccountID (uPeerID)
|
|
% STAmount::createHumanCurrency (speEnd.mCurrencyID)
|
|
% rspEntry->getBalance ().getFullText ()
|
|
% rspEntry->getLimitPeer ().getFullText ()
|
|
);
|
|
}
|
|
else if (dstCurrency && (uPeerID == mDstAccountID))
|
|
{
|
|
// never skip the destination node
|
|
candidates.push_back (std::make_pair (1000000, uPeerID));
|
|
}
|
|
else
|
|
{
|
|
// save this candidate
|
|
int out = getPathsOut (speEnd.mCurrencyID, uPeerID, dstCurrency, mDstAccountID);
|
|
|
|
if (out != 0)
|
|
candidates.push_back (std::make_pair (out, uPeerID));
|
|
else
|
|
WriteLog(lsTRACE, Pathfinder) << "findPaths: " << RippleAddress::createHumanAccountID(uPeerID) << " has no paths out";
|
|
}
|
|
}
|
|
|
|
if (!candidates.empty ())
|
|
{
|
|
std::sort (candidates.begin (), candidates.end (),
|
|
BIND_TYPE (candCmp, mLedger->getLedgerSeq (), P_1, P_2));
|
|
|
|
int count = candidates.size ();
|
|
|
|
if ((count > 10) && (speEnd.mAccountID != mSrcAccountID)) // try more paths from source
|
|
count = 10;
|
|
else if (count > 50)
|
|
count = 50;
|
|
|
|
std::vector< std::pair<int, uint160> >::iterator it = candidates.begin ();
|
|
|
|
while (count-- != 0)
|
|
{
|
|
STPath spNew (spPath);
|
|
STPathElement speNew (it->second, speEnd.mCurrencyID, it->second);
|
|
|
|
spNew.mPath.push_back (speNew);
|
|
qspExplore.push (spNew);
|
|
|
|
bContinued = true;
|
|
|
|
WriteLog (lsTRACE, Pathfinder) <<
|
|
boost::str (boost::format ("findPaths: push explore: %s/%s -> %s/%s")
|
|
% STAmount::createHumanCurrency (speEnd.mCurrencyID)
|
|
% RippleAddress::createHumanAccountID (speEnd.mAccountID)
|
|
% STAmount::createHumanCurrency (speEnd.mCurrencyID)
|
|
% RippleAddress::createHumanAccountID (it->second));
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// XXX Flip argument order to norm. (currency, issuer)
|
|
std::vector<OrderBook::pointer> books;
|
|
getApp().getOrderBookDB ().getBooksByTakerPays (speEnd.mIssuerID, speEnd.mCurrencyID, books);
|
|
|
|
BOOST_FOREACH (OrderBook::ref book, books)
|
|
{
|
|
if (!spPath.hasSeen (ACCOUNT_XRP, book->getCurrencyOut (), book->getIssuerOut ()) &&
|
|
!matchesOrigin (book->getCurrencyOut (), book->getIssuerOut ()) &&
|
|
(!isLast ||
|
|
(book->getCurrencyOut () == mDstAmount.getCurrency () &&
|
|
book->getIssuerOut () == mDstAccountID)))
|
|
{
|
|
// A book we haven't seen before. Add it.
|
|
STPath spNew (spPath);
|
|
STPathElement speBook (ACCOUNT_XRP, book->getCurrencyOut (), book->getIssuerOut (),
|
|
book->getCurrencyIn () != book->getCurrencyOut ());
|
|
|
|
spNew.mPath.push_back (speBook); // Add the order book.
|
|
|
|
if (!!book->getCurrencyOut ())
|
|
{
|
|
// For non-XRP out, don't end on the book, add the issuing account.
|
|
STPathElement speAccount (book->getIssuerOut (), book->getCurrencyOut (), book->getIssuerOut ());
|
|
spNew.mPath.push_back (speAccount); // Add the account and currency
|
|
}
|
|
|
|
qspExplore.push (spNew);
|
|
|
|
bContinued = true;
|
|
|
|
WriteLog (lsTRACE, Pathfinder) <<
|
|
boost::str (boost::format ("findPaths: push book: %s/%s -> %s/%s")
|
|
% STAmount::createHumanCurrency (speEnd.mCurrencyID)
|
|
% RippleAddress::createHumanAccountID (speEnd.mIssuerID)
|
|
% STAmount::createHumanCurrency (book->getCurrencyOut ())
|
|
% RippleAddress::createHumanAccountID (book->getIssuerOut ()));
|
|
}
|
|
}
|
|
|
|
CondLog (!bContinued, lsTRACE, Pathfinder)
|
|
<< boost::str (boost::format ("findPaths: dropping: non-XRP -> dead end"));
|
|
}
|
|
}
|
|
|
|
unsigned int iLimit = std::min (iMaxPaths, (unsigned int) vspResults.size ());
|
|
|
|
// Only filter, sort, and limit if have non-default paths.
|
|
if (iLimit)
|
|
{
|
|
std::vector<path_LQ_t> vMap;
|
|
|
|
// Build map of quality to entry.
|
|
for (int i = vspResults.size (); i--;)
|
|
{
|
|
STAmount saMaxAmountAct;
|
|
STAmount saDstAmountAct;
|
|
std::vector<PathState::pointer> vpsExpanded;
|
|
STPathSet spsPaths;
|
|
STPath& spCurrent = vspResults[i];
|
|
|
|
spsPaths.addPath (spCurrent); // Just checking the current path.
|
|
|
|
TER terResult;
|
|
|
|
try
|
|
{
|
|
LedgerEntrySet lesSandbox (lesActive.duplicate ());
|
|
|
|
terResult = RippleCalc::rippleCalc (
|
|
lesSandbox,
|
|
saMaxAmountAct,
|
|
saDstAmountAct,
|
|
vpsExpanded,
|
|
mSrcAmount, // --> amount to send max.
|
|
mDstAmount, // --> amount to deliver.
|
|
mDstAccountID,
|
|
mSrcAccountID,
|
|
spsPaths,
|
|
true, // --> bPartialPayment: Allow, it might contribute.
|
|
false, // --> bLimitQuality: Assume normal transaction.
|
|
true, // --> bNoRippleDirect: Providing the only path.
|
|
true); // --> bStandAlone: Don't need to delete unfundeds.
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
WriteLog (lsINFO, Pathfinder) << "findPaths: Caught throw: " << e.what ();
|
|
|
|
terResult = tefEXCEPTION;
|
|
}
|
|
|
|
if (tesSUCCESS == terResult)
|
|
{
|
|
uint64 uQuality = STAmount::getRate (saDstAmountAct, saMaxAmountAct);
|
|
|
|
WriteLog (lsDEBUG, Pathfinder)
|
|
<< boost::str (boost::format ("findPaths: quality: %d: %s")
|
|
% uQuality
|
|
% spCurrent.getJson (0));
|
|
|
|
vMap.push_back (path_LQ_t (uQuality, spCurrent.mPath.size (), saDstAmountAct, i));
|
|
}
|
|
else
|
|
{
|
|
WriteLog (lsDEBUG, Pathfinder)
|
|
<< boost::str (boost::format ("findPaths: dropping: %s: %s")
|
|
% transToken (terResult)
|
|
% spCurrent.getJson (0));
|
|
}
|
|
}
|
|
|
|
if (vMap.size ())
|
|
{
|
|
std::sort (vMap.begin (), vMap.end (), bQualityCmp); // Lower is better and should be first.
|
|
|
|
STAmount remaining = mDstAmount;
|
|
|
|
if (bFound)
|
|
{
|
|
// must subtract liquidity in default path from remaining amount
|
|
try
|
|
{
|
|
STAmount saMaxAmountAct, saDstAmountAct;
|
|
std::vector<PathState::pointer> vpsExpanded;
|
|
LedgerEntrySet lesSandbox (lesActive.duplicate ());
|
|
|
|
TER result = RippleCalc::rippleCalc (
|
|
lesSandbox,
|
|
saMaxAmountAct,
|
|
saDstAmountAct,
|
|
vpsExpanded,
|
|
mSrcAmount,
|
|
mDstAmount,
|
|
mDstAccountID,
|
|
mSrcAccountID,
|
|
STPathSet (),
|
|
true, // allow partial payment
|
|
false,
|
|
false, // don't suppress default paths, that's the point
|
|
true);
|
|
|
|
if (tesSUCCESS == result)
|
|
{
|
|
WriteLog (lsDEBUG, Pathfinder) << "Default path contributes: " << saDstAmountAct;
|
|
remaining -= saDstAmountAct;
|
|
}
|
|
else
|
|
{
|
|
WriteLog (lsDEBUG, Pathfinder) << "Default path fails: " << transToken (result);
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
WriteLog (lsDEBUG, Pathfinder) << "Default path causes exception";
|
|
}
|
|
}
|
|
|
|
for (int i = 0, iPathsLeft = iMaxPaths; (iPathsLeft > 0) && (i < vMap.size ()); ++i)
|
|
{
|
|
path_LQ_t& lqt = vMap[i];
|
|
|
|
if ((iPathsLeft != 1) || (lqt.get<2> () >= remaining))
|
|
{
|
|
// last path must fill
|
|
--iPathsLeft;
|
|
remaining -= lqt.get<2> ();
|
|
spsDst.addPath (vspResults[lqt.get<3> ()]);
|
|
}
|
|
else
|
|
WriteLog (lsDEBUG, Pathfinder) << "Skipping a non-filling path: " << vspResults[lqt.get<3> ()].getJson (0);
|
|
}
|
|
|
|
if (remaining.isPositive ())
|
|
{
|
|
bFound = false;
|
|
WriteLog (lsINFO, Pathfinder) << "Paths could not send " << remaining << " of " << mDstAmount;
|
|
}
|
|
else
|
|
bFound = true;
|
|
|
|
WriteLog (lsDEBUG, Pathfinder) << boost::str (boost::format ("findPaths: RESULTS: %s") % spsDst.getJson (0));
|
|
}
|
|
else
|
|
{
|
|
WriteLog (lsDEBUG, Pathfinder) << boost::str (boost::format ("findPaths: RESULTS: non-defaults filtered away"));
|
|
}
|
|
}
|
|
|
|
WriteLog (lsDEBUG, Pathfinder) << boost::str (boost::format ("findPaths< bFound=%d") % bFound);
|
|
|
|
return bFound;
|
|
}
|
|
|
|
boost::unordered_set<uint160> usAccountSourceCurrencies (const RippleAddress& raAccountID, Ledger::ref lrLedger,
|
|
bool includeXRP)
|
|
{
|
|
boost::unordered_set<uint160> usCurrencies;
|
|
|
|
// YYY Only bother if they are above reserve
|
|
if (includeXRP)
|
|
usCurrencies.insert (uint160 (CURRENCY_XRP));
|
|
|
|
// List of ripple lines.
|
|
AccountItems rippleLines (raAccountID.getAccountID (), lrLedger, AccountItem::pointer (new RippleState ()));
|
|
|
|
BOOST_FOREACH (AccountItem::ref item, rippleLines.getItems ())
|
|
{
|
|
RippleState* rspEntry = (RippleState*) item.get ();
|
|
const STAmount& saBalance = rspEntry->getBalance ();
|
|
|
|
// Filter out non
|
|
if (saBalance.isPositive () // Have IOUs to send.
|
|
|| (rspEntry->getLimitPeer () // Peer extends credit.
|
|
&& ((-saBalance) < rspEntry->getLimitPeer ()))) // Credit left.
|
|
{
|
|
usCurrencies.insert (saBalance.getCurrency ());
|
|
}
|
|
}
|
|
|
|
usCurrencies.erase (CURRENCY_BAD);
|
|
return usCurrencies;
|
|
}
|
|
|
|
boost::unordered_set<uint160> usAccountDestCurrencies (const RippleAddress& raAccountID, Ledger::ref lrLedger,
|
|
bool includeXRP)
|
|
{
|
|
boost::unordered_set<uint160> usCurrencies;
|
|
|
|
if (includeXRP)
|
|
usCurrencies.insert (uint160 (CURRENCY_XRP)); // Even if account doesn't exist
|
|
|
|
// List of ripple lines.
|
|
AccountItems rippleLines (raAccountID.getAccountID (), lrLedger, AccountItem::pointer (new RippleState ()));
|
|
|
|
BOOST_FOREACH (AccountItem::ref item, rippleLines.getItems ())
|
|
{
|
|
RippleState* rspEntry = (RippleState*) item.get ();
|
|
const STAmount& saBalance = rspEntry->getBalance ();
|
|
|
|
if (saBalance < rspEntry->getLimit ()) // Can take more
|
|
usCurrencies.insert (saBalance.getCurrency ());
|
|
}
|
|
|
|
usCurrencies.erase (CURRENCY_BAD);
|
|
return usCurrencies;
|
|
}
|
|
|
|
bool Pathfinder::matchesOrigin (const uint160& currency, const uint160& issuer)
|
|
{
|
|
return (currency == mSrcCurrencyID) && (issuer == mSrcIssuerID);
|
|
}
|
|
|
|
int Pathfinder::getPathsOut (const uint160& currencyID, const uint160& accountID,
|
|
bool isDstCurrency, const uint160& dstAccount)
|
|
{
|
|
#ifdef C11X
|
|
std::pair<const uint160&, const uint160&> accountCurrency (currencyID, accountID);
|
|
#else
|
|
std::pair<uint160, uint160> accountCurrency (currencyID, accountID);
|
|
#endif
|
|
boost::unordered_map<std::pair<uint160, uint160>, int>::iterator it = mPOMap.find (accountCurrency);
|
|
|
|
if (it != mPOMap.end ())
|
|
return it->second;
|
|
|
|
int aFlags = mLedger->getSLEi(Ledger::getAccountRootIndex(accountID))->getFieldU32(sfFlags);
|
|
bool const bAuthRequired = (aFlags & lsfRequireAuth) != 0;
|
|
|
|
int count = 0;
|
|
AccountItems& rippleLines (mRLCache->getRippleLines (accountID));
|
|
BOOST_FOREACH (AccountItem::ref item, rippleLines.getItems ())
|
|
{
|
|
RippleState* rspEntry = (RippleState*) item.get ();
|
|
|
|
if (currencyID != rspEntry->getLimit ().getCurrency ())
|
|
nothing ();
|
|
else if (!rspEntry->getBalance ().isPositive () &&
|
|
(!rspEntry->getLimitPeer ()
|
|
|| -rspEntry->getBalance () >= rspEntry->getLimitPeer ()
|
|
|| (bAuthRequired && !rspEntry->getAuth ())))
|
|
nothing ();
|
|
else if (isDstCurrency && (dstAccount == rspEntry->getAccountIDPeer ()))
|
|
count += 10000; // count a path to the destination extra
|
|
else
|
|
++count;
|
|
}
|
|
mPOMap[accountCurrency] = count;
|
|
return count;
|
|
}
|
|
|
|
// vim:ts=4
|