mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-26 05:55:51 +00:00
3158 lines
102 KiB
C++
3158 lines
102 KiB
C++
// TODO:
|
||
// - Do automatic bridging via XRP.
|
||
//
|
||
// OPTIMIZE: When calculating path increment, note if increment consumes all liquidity. No need to revesit path in the future if
|
||
// all liquidity is used.
|
||
//
|
||
|
||
#include <boost/foreach.hpp>
|
||
#include <boost/format.hpp>
|
||
#include <boost/tuple/tuple_comparison.hpp>
|
||
|
||
#include "RippleCalc.h"
|
||
#include "Log.h"
|
||
|
||
#include "../json/writer.h"
|
||
|
||
SETUP_LOG();
|
||
|
||
std::size_t hash_value(const aciSource& asValue)
|
||
{
|
||
std::size_t seed = 0;
|
||
|
||
asValue.get<0>().hash_combine(seed);
|
||
asValue.get<1>().hash_combine(seed);
|
||
asValue.get<2>().hash_combine(seed);
|
||
|
||
return seed;
|
||
}
|
||
|
||
// Compare the non-calculated fields.
|
||
bool PaymentNode::operator==(const PaymentNode& pnOther) const {
|
||
return pnOther.uFlags == uFlags
|
||
&& pnOther.uAccountID == uAccountID
|
||
&& pnOther.uCurrencyID == uCurrencyID
|
||
&& pnOther.uIssuerID == uIssuerID;
|
||
}
|
||
|
||
//
|
||
// PathState implementation
|
||
//
|
||
|
||
// Return true, iff lhs has less priority than rhs.
|
||
bool PathState::lessPriority(PathState& lhs, PathState& rhs)
|
||
{
|
||
// First rank is quality.
|
||
if (lhs.uQuality != rhs.uQuality)
|
||
return lhs.uQuality > rhs.uQuality; // Bigger is worse.
|
||
|
||
// Second rank is best quantity.
|
||
if (lhs.saOutPass != rhs.saOutPass)
|
||
return lhs.saOutPass < rhs.saOutPass; // Smaller is worse.
|
||
|
||
// Third rank is path index.
|
||
return lhs.mIndex > rhs.mIndex; // Bigger is worse.
|
||
}
|
||
|
||
// Make sure last path node delivers to uAccountID: uCurrencyID from uIssuerID.
|
||
//
|
||
// If the unadded next node as specified by arguments would not work as is, then add the necessary nodes so it would work.
|
||
//
|
||
// Rules:
|
||
// - Currencies must be converted via an offer.
|
||
// - A node names it's output.
|
||
// - A ripple nodes output issuer must be the node's account or the next node's account.
|
||
// - Offers can only go directly to another offer if the currency and issuer are an exact match.
|
||
// - Real issuers must be specified for non-XRP.
|
||
TER PathState::pushImply(
|
||
const uint160& uAccountID, // --> Delivering to this account.
|
||
const uint160& uCurrencyID, // --> Delivering this currency.
|
||
const uint160& uIssuerID) // --> Delivering this issuer.
|
||
{
|
||
const PaymentNode& pnPrv = vpnNodes.back();
|
||
TER terResult = tesSUCCESS;
|
||
|
||
cLog(lsTRACE) << "pushImply> "
|
||
<< RippleAddress::createHumanAccountID(uAccountID)
|
||
<< " " << STAmount::createHumanCurrency(uCurrencyID)
|
||
<< " " << RippleAddress::createHumanAccountID(uIssuerID);
|
||
|
||
if (pnPrv.uCurrencyID != uCurrencyID)
|
||
{
|
||
// Currency is different, need to convert via an offer.
|
||
|
||
terResult = pushNode( // Offer.
|
||
!!uCurrencyID
|
||
? STPathElement::typeCurrency | STPathElement::typeIssuer
|
||
: STPathElement::typeCurrency,
|
||
ACCOUNT_XRP, // Placeholder for offers.
|
||
uCurrencyID, // The offer's output is what is now wanted.
|
||
uIssuerID);
|
||
}
|
||
|
||
const PaymentNode& pnBck = vpnNodes.back();
|
||
|
||
// For ripple, non-XRP, ensure the issuer is on at least one side of the transaction.
|
||
if (tesSUCCESS == terResult
|
||
&& !!uCurrencyID // Not XRP.
|
||
&& (pnBck.uAccountID != uIssuerID // Previous is not issuing own IOUs.
|
||
&& uAccountID != uIssuerID)) // Current is not receiving own IOUs.
|
||
{
|
||
// Need to ripple through uIssuerID's account.
|
||
|
||
terResult = pushNode(
|
||
STPathElement::typeAccount | STPathElement::typeCurrency | STPathElement::typeIssuer,
|
||
uIssuerID, // Intermediate account is the needed issuer.
|
||
uCurrencyID,
|
||
uIssuerID);
|
||
}
|
||
|
||
cLog(lsTRACE) << boost::str(boost::format("pushImply< : %s") % transToken(terResult));
|
||
|
||
return terResult;
|
||
}
|
||
|
||
// Append a node and insert before it any implied nodes.
|
||
// Offers may go back to back.
|
||
// <-- terResult: tesSUCCESS, temBAD_PATH, terNO_LINE, tecPATH_DRY
|
||
TER PathState::pushNode(
|
||
const int iType,
|
||
const uint160& uAccountID,
|
||
const uint160& uCurrencyID,
|
||
const uint160& uIssuerID)
|
||
{
|
||
PaymentNode pnCur;
|
||
const bool bFirst = vpnNodes.empty();
|
||
const PaymentNode& pnPrv = bFirst ? PaymentNode() : vpnNodes.back();
|
||
// true, iff node is a ripple account. false, iff node is an offer node.
|
||
const bool bAccount = isSetBit(iType, STPathElement::typeAccount);
|
||
// true, iff currency supplied.
|
||
// Currency is specified for the output of the current node.
|
||
const bool bCurrency = isSetBit(iType, STPathElement::typeCurrency);
|
||
// Issuer is specified for the output of the current node.
|
||
const bool bIssuer = isSetBit(iType, STPathElement::typeIssuer);
|
||
TER terResult = tesSUCCESS;
|
||
|
||
cLog(lsTRACE) << "pushNode> "
|
||
<< iType
|
||
<< ": " << (bAccount ? RippleAddress::createHumanAccountID(uAccountID) : "-")
|
||
<< " " << (bCurrency ? STAmount::createHumanCurrency(uCurrencyID) : "-")
|
||
<< "/" << (bIssuer ? RippleAddress::createHumanAccountID(uIssuerID) : "-");
|
||
|
||
pnCur.uFlags = iType;
|
||
pnCur.uCurrencyID = bCurrency ? uCurrencyID : pnPrv.uCurrencyID;
|
||
|
||
if (iType & ~STPathElement::typeValidBits)
|
||
{
|
||
cLog(lsDEBUG) << "pushNode: bad bits.";
|
||
|
||
terResult = temBAD_PATH;
|
||
}
|
||
else if (bIssuer && !pnCur.uCurrencyID)
|
||
{
|
||
cLog(lsDEBUG) << "pushNode: issuer specified for XRP.";
|
||
|
||
terResult = temBAD_PATH;
|
||
}
|
||
else if (bIssuer && !uIssuerID)
|
||
{
|
||
cLog(lsDEBUG) << "pushNode: specified bad issuer.";
|
||
|
||
terResult = temBAD_PATH;
|
||
}
|
||
else if (bAccount)
|
||
{
|
||
// Account link
|
||
|
||
pnCur.uAccountID = uAccountID;
|
||
pnCur.uIssuerID = bIssuer
|
||
? uIssuerID
|
||
: !!pnCur.uCurrencyID
|
||
? uAccountID
|
||
: ACCOUNT_XRP;
|
||
pnCur.saRevRedeem = STAmount(uCurrencyID, uAccountID);
|
||
pnCur.saRevIssue = STAmount(uCurrencyID, uAccountID);
|
||
|
||
if (bFirst)
|
||
{
|
||
// The first node is always correct as is.
|
||
|
||
nothing();
|
||
}
|
||
else if (!uAccountID)
|
||
{
|
||
cLog(lsDEBUG) << "pushNode: specified bad account.";
|
||
|
||
terResult = temBAD_PATH;
|
||
}
|
||
else
|
||
{
|
||
// Add required intermediate nodes to deliver to current account.
|
||
cLog(lsTRACE) << "pushNode: imply for account.";
|
||
|
||
terResult = pushImply(
|
||
pnCur.uAccountID, // Current account.
|
||
pnCur.uCurrencyID, // Wanted currency.
|
||
!!pnCur.uCurrencyID ? uAccountID : ACCOUNT_XRP); // Account as wanted issuer.
|
||
|
||
// Note: pnPrv may no longer be the immediately previous node.
|
||
}
|
||
|
||
if (tesSUCCESS == terResult && !vpnNodes.empty())
|
||
{
|
||
const PaymentNode& pnBck = vpnNodes.back();
|
||
bool bBckAccount = isSetBit(pnBck.uFlags, STPathElement::typeAccount);
|
||
|
||
if (bBckAccount)
|
||
{
|
||
SLE::pointer sleRippleState = lesEntries.entryCache(ltRIPPLE_STATE, Ledger::getRippleStateIndex(pnBck.uAccountID, pnCur.uAccountID, pnPrv.uCurrencyID));
|
||
|
||
if (!sleRippleState)
|
||
{
|
||
cLog(lsTRACE) << "pushNode: No credit line between "
|
||
<< RippleAddress::createHumanAccountID(pnBck.uAccountID)
|
||
<< " and "
|
||
<< RippleAddress::createHumanAccountID(pnCur.uAccountID)
|
||
<< " for "
|
||
<< STAmount::createHumanCurrency(pnCur.uCurrencyID)
|
||
<< "." ;
|
||
|
||
cLog(lsTRACE) << getJson();
|
||
|
||
terResult = terNO_LINE;
|
||
}
|
||
else
|
||
{
|
||
cLog(lsTRACE) << "pushNode: Credit line found between "
|
||
<< RippleAddress::createHumanAccountID(pnBck.uAccountID)
|
||
<< " and "
|
||
<< RippleAddress::createHumanAccountID(pnCur.uAccountID)
|
||
<< " for "
|
||
<< STAmount::createHumanCurrency(pnCur.uCurrencyID)
|
||
<< "." ;
|
||
|
||
STAmount saOwed = lesEntries.rippleOwed(pnCur.uAccountID, pnBck.uAccountID, pnCur.uCurrencyID);
|
||
|
||
if (!saOwed.isPositive() && -saOwed >= lesEntries.rippleLimit(pnCur.uAccountID, pnBck.uAccountID, pnCur.uCurrencyID))
|
||
{
|
||
terResult = tecPATH_DRY;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (tesSUCCESS == terResult)
|
||
vpnNodes.push_back(pnCur);
|
||
}
|
||
else
|
||
{
|
||
// Offer link
|
||
// Offers bridge a change in currency & issuer or just a change in issuer.
|
||
pnCur.uIssuerID = bIssuer
|
||
? uIssuerID
|
||
: !!pnCur.uCurrencyID
|
||
? !!pnPrv.uIssuerID
|
||
? pnPrv.uIssuerID // Default to previous issuer
|
||
: pnPrv.uAccountID // Or previous account if no previous issuer.
|
||
: ACCOUNT_XRP;
|
||
pnCur.saRateMax = saZero;
|
||
|
||
if (!!pnCur.uCurrencyID != !!pnCur.uIssuerID)
|
||
{
|
||
cLog(lsDEBUG) << "pushNode: currency is inconsistent with issuer.";
|
||
|
||
terResult = temBAD_PATH;
|
||
}
|
||
else if (!!pnPrv.uAccountID)
|
||
{
|
||
// Previous is an account.
|
||
cLog(lsTRACE) << "pushNode: imply for offer.";
|
||
|
||
// Insert intermediary issuer account if needed.
|
||
terResult = pushImply(
|
||
ACCOUNT_XRP, // Rippling, but offer's don't have an account.
|
||
pnPrv.uCurrencyID,
|
||
pnPrv.uIssuerID);
|
||
}
|
||
|
||
if (tesSUCCESS == terResult)
|
||
{
|
||
vpnNodes.push_back(pnCur);
|
||
}
|
||
}
|
||
cLog(lsTRACE) << boost::str(boost::format("pushNode< : %s") % transToken(terResult));
|
||
|
||
return terResult;
|
||
}
|
||
|
||
// Set to an expanded path.
|
||
//
|
||
// terStatus = tesSUCCESS, temBAD_PATH, terNO_LINE, or temBAD_PATH_LOOP
|
||
void PathState::setExpanded(
|
||
const LedgerEntrySet& lesSource,
|
||
const STPath& spSourcePath,
|
||
const uint160& uReceiverID,
|
||
const uint160& uSenderID
|
||
)
|
||
{
|
||
uQuality = 1; // Mark path as active.
|
||
|
||
const uint160 uMaxCurrencyID = saInReq.getCurrency();
|
||
const uint160 uMaxIssuerID = saInReq.getIssuer();
|
||
|
||
const uint160 uOutCurrencyID = saOutReq.getCurrency();
|
||
const uint160 uOutIssuerID = saOutReq.getIssuer();
|
||
const uint160 uSenderIssuerID = !!uMaxCurrencyID ? uSenderID : ACCOUNT_XRP; // Sender is always issuer for non-XRP.
|
||
|
||
lesEntries = lesSource.duplicate();
|
||
|
||
terStatus = tesSUCCESS;
|
||
|
||
// XRP with issuer is malformed.
|
||
if ((!uMaxCurrencyID && !!uMaxIssuerID) || (!uOutCurrencyID && !!uOutIssuerID))
|
||
terStatus = temBAD_PATH;
|
||
|
||
// Push sending node.
|
||
// For non-XRP, issuer is always sending account.
|
||
// - Trying to expand, not-compact.
|
||
// - Every issuer will be traversed through.
|
||
if (tesSUCCESS == terStatus)
|
||
terStatus = pushNode(
|
||
!!uMaxCurrencyID
|
||
? STPathElement::typeAccount | STPathElement::typeCurrency | STPathElement::typeIssuer
|
||
: STPathElement::typeAccount | STPathElement::typeCurrency,
|
||
uSenderID,
|
||
uMaxCurrencyID, // Max specifes the currency.
|
||
uSenderIssuerID);
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("PathState: pushed: account=%s currency=%s issuer=%s")
|
||
% RippleAddress::createHumanAccountID(uSenderID)
|
||
% STAmount::createHumanCurrency(uMaxCurrencyID)
|
||
% RippleAddress::createHumanAccountID(uSenderIssuerID));
|
||
|
||
if (tesSUCCESS == terStatus
|
||
&& uMaxIssuerID != uSenderIssuerID) { // Issuer was not same as sender.
|
||
// May have an implied account node.
|
||
// - If it was XRP, then issuers would have matched.
|
||
|
||
// Figure out next node properties for implied node.
|
||
const uint160 uNxtCurrencyID = spSourcePath.size()
|
||
? spSourcePath.getElement(0).getCurrency() // Use next node.
|
||
: uOutCurrencyID; // Use send.
|
||
const uint160 uNxtAccountID = spSourcePath.size()
|
||
? spSourcePath.getElement(0).getAccountID()
|
||
: !!uOutCurrencyID
|
||
? uOutIssuerID == uReceiverID
|
||
? uReceiverID
|
||
: uOutIssuerID // Use implied node.
|
||
: ACCOUNT_XRP;
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("PathState: implied check: uNxtCurrencyID=%s uNxtAccountID=%s")
|
||
% RippleAddress::createHumanAccountID(uNxtCurrencyID)
|
||
% RippleAddress::createHumanAccountID(uNxtAccountID));
|
||
|
||
// Can't just use push implied, because it can't compensate for next account.
|
||
if (!uNxtCurrencyID // Next is XRP, offer next. Must go through issuer.
|
||
|| uMaxCurrencyID != uNxtCurrencyID // Next is different currency, offer next...
|
||
|| uMaxIssuerID != uNxtAccountID) // Next is not implied issuer
|
||
{
|
||
cLog(lsDEBUG) << boost::str(boost::format("PathState: sender implied: account=%s currency=%s issuer=%s")
|
||
% RippleAddress::createHumanAccountID(uMaxIssuerID)
|
||
% RippleAddress::createHumanAccountID(uMaxCurrencyID)
|
||
% RippleAddress::createHumanAccountID(uMaxIssuerID));
|
||
// Add account implied by SendMax.
|
||
terStatus = pushNode(
|
||
!!uMaxCurrencyID
|
||
? STPathElement::typeAccount | STPathElement::typeCurrency | STPathElement::typeIssuer
|
||
: STPathElement::typeAccount | STPathElement::typeCurrency,
|
||
uMaxIssuerID,
|
||
uMaxCurrencyID,
|
||
uMaxIssuerID);
|
||
}
|
||
}
|
||
|
||
BOOST_FOREACH(const STPathElement& speElement, spSourcePath)
|
||
{
|
||
if (tesSUCCESS == terStatus)
|
||
{
|
||
cLog(lsDEBUG) << boost::str(boost::format("PathState: element in path:"));
|
||
terStatus = pushNode(speElement.getNodeType(), speElement.getAccountID(), speElement.getCurrency(), speElement.getIssuerID());
|
||
}
|
||
}
|
||
|
||
const PaymentNode& pnPrv = vpnNodes.back();
|
||
|
||
if (tesSUCCESS == terStatus
|
||
&& !!uOutCurrencyID // Next is not XRP
|
||
&& uOutIssuerID != uReceiverID // Out issuer is not reciever
|
||
&& (pnPrv.uCurrencyID != uOutCurrencyID // Previous will be an offer.
|
||
|| pnPrv.uAccountID != uOutIssuerID)) // Need the implied issuer.
|
||
{
|
||
// Add implied account.
|
||
cLog(lsDEBUG) << boost::str(boost::format("PathState: receiver implied: account=%s currency=%s issuer=%s")
|
||
% RippleAddress::createHumanAccountID(uOutIssuerID)
|
||
% RippleAddress::createHumanAccountID(uOutCurrencyID)
|
||
% RippleAddress::createHumanAccountID(uOutIssuerID));
|
||
terStatus = pushNode(
|
||
!!uOutCurrencyID
|
||
? STPathElement::typeAccount | STPathElement::typeCurrency | STPathElement::typeIssuer
|
||
: STPathElement::typeAccount | STPathElement::typeCurrency,
|
||
uOutIssuerID,
|
||
uOutCurrencyID,
|
||
uOutIssuerID);
|
||
}
|
||
|
||
if (tesSUCCESS == terStatus)
|
||
{
|
||
// Create receiver node.
|
||
// Last node is always an account.
|
||
|
||
terStatus = pushNode(
|
||
!!uOutCurrencyID
|
||
? STPathElement::typeAccount | STPathElement::typeCurrency | STPathElement::typeIssuer
|
||
: STPathElement::typeAccount | STPathElement::typeCurrency,
|
||
uReceiverID, // Receive to output
|
||
uOutCurrencyID, // Desired currency
|
||
uReceiverID);
|
||
}
|
||
|
||
if (tesSUCCESS == terStatus)
|
||
{
|
||
// Look for first mention of source in nodes and detect loops.
|
||
// Note: The output is not allowed to be a source.
|
||
|
||
const unsigned int uNodes = vpnNodes.size();
|
||
|
||
for (unsigned int uNode = 0; tesSUCCESS == terStatus && uNode != uNodes; ++uNode)
|
||
{
|
||
const PaymentNode& pnCur = vpnNodes[uNode];
|
||
|
||
if (!!pnCur.uAccountID)
|
||
{
|
||
// Source is a ripple line
|
||
nothing();
|
||
}
|
||
else if (!umForward.insert(std::make_pair(boost::make_tuple(pnCur.uAccountID, pnCur.uCurrencyID, pnCur.uIssuerID), uNode)).second)
|
||
{
|
||
// Failed to insert. Have a loop.
|
||
cLog(lsDEBUG) << boost::str(boost::format("PathState: loop detected: %s")
|
||
% getJson());
|
||
|
||
terStatus = temBAD_PATH_LOOP;
|
||
}
|
||
}
|
||
}
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("PathState: in=%s/%s out=%s/%s %s")
|
||
% STAmount::createHumanCurrency(uMaxCurrencyID)
|
||
% RippleAddress::createHumanAccountID(uMaxIssuerID)
|
||
% STAmount::createHumanCurrency(uOutCurrencyID)
|
||
% RippleAddress::createHumanAccountID(uOutIssuerID)
|
||
% getJson());
|
||
}
|
||
|
||
// Set to a canonical path.
|
||
// - Remove extra elements
|
||
// - Assumes path is expanded.
|
||
//
|
||
// We do canonicalization to:
|
||
// - Prevent waste in the ledger.
|
||
// - Allow longer paths to be specified than would otherwise be allowed.
|
||
//
|
||
// Optimization theory:
|
||
// - Can omit elements that the expansion routine derives.
|
||
// - Can pack some elements into other elements.
|
||
//
|
||
// Rules:
|
||
// - SendMax if not specified, defaults currency to send and if not sending XRP defaults issuer to sender.
|
||
// - All paths start with the sender account.
|
||
// - Currency and issuer is from SendMax.
|
||
// - All paths end with the destination account.
|
||
//
|
||
// Optimization:
|
||
// - An XRP output implies an offer node or destination node is next.
|
||
// - A change in currency implies an offer node.
|
||
// - A change in issuer...
|
||
void PathState::setCanonical(
|
||
const PathState& psExpanded
|
||
)
|
||
{
|
||
saInAct = psExpanded.saInAct;
|
||
saOutAct = psExpanded.saOutAct;
|
||
|
||
const uint160 uMaxCurrencyID = saInAct.getCurrency();
|
||
const uint160 uMaxIssuerID = saInAct.getIssuer();
|
||
|
||
const uint160 uOutCurrencyID = saOutAct.getCurrency();
|
||
const uint160 uOutIssuerID = saOutAct.getIssuer();
|
||
|
||
unsigned int uNode = 0;
|
||
|
||
unsigned int uEnd = psExpanded.vpnNodes.size(); // The node, indexed by 0, not to include.
|
||
|
||
uint160 uDstAccountID = psExpanded.vpnNodes[uEnd].uAccountID;
|
||
|
||
uint160 uAccountID = psExpanded.vpnNodes[0].uAccountID;
|
||
uint160 uCurrencyID = uMaxCurrencyID;
|
||
uint160 uIssuerID = uMaxIssuerID;
|
||
|
||
// Node 0 is a composit of the sending account and saInAct.
|
||
++uNode; // skip node 0
|
||
|
||
// Last node is implied: Always skip last node
|
||
--uEnd; // skip last node
|
||
|
||
// saInAct
|
||
// - currency is always the same as vpnNodes[0].
|
||
#if 1
|
||
if (uNode != uEnd && uMaxIssuerID != uAccountID)
|
||
{
|
||
// saInAct issuer is not the sender. This forces an implied node.
|
||
// cLog(lsDEBUG) << boost::str(boost::format("setCanonical: in diff: uNode=%d uEnd=%d") % uNode % uEnd);
|
||
|
||
// skip node 1
|
||
|
||
uIssuerID = psExpanded.vpnNodes[uNode].uIssuerID;
|
||
|
||
++uNode;
|
||
}
|
||
#else
|
||
if (uNode != uEnd)
|
||
{ // Have another node
|
||
bool bKeep = false;
|
||
|
||
if (uMaxIssuerID != uAccountID)
|
||
{
|
||
}
|
||
if (uMaxCurrencyID) // Not sending XRP.
|
||
{
|
||
// Node 1 must be an account.
|
||
|
||
if (uMaxIssuerID != uAccountID)
|
||
{
|
||
// Node 1 is required to specify issuer.
|
||
|
||
bKeep = true;
|
||
}
|
||
else
|
||
{
|
||
// Node 1 must be an account
|
||
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Node 1 must be an order book.
|
||
|
||
bKeep = true;
|
||
}
|
||
|
||
if (bKeep)
|
||
{
|
||
uCurrencyID = psExpanded.vpnNodes[uNode].uCurrencyID;
|
||
uIssuerID = psExpanded.vpnNodes[uNode].uIssuerID;
|
||
++uNode; // Keep it.
|
||
}
|
||
}
|
||
#endif
|
||
|
||
if (uNode != uEnd && !!uOutCurrencyID && uOutIssuerID != uDstAccountID)
|
||
{
|
||
// cLog(lsDEBUG) << boost::str(boost::format("setCanonical: out diff: uNode=%d uEnd=%d") % uNode % uEnd);
|
||
// The next to last node is saOutAct if an issuer different from receiver is supplied.
|
||
// The next to last node can be implied.
|
||
|
||
--uEnd;
|
||
}
|
||
|
||
const PaymentNode& pnEnd = psExpanded.vpnNodes[uEnd];
|
||
|
||
if (uNode != uEnd
|
||
&& !pnEnd.uAccountID && pnEnd.uCurrencyID == uOutCurrencyID && pnEnd.uIssuerID == uOutIssuerID)
|
||
{
|
||
// The current end node is an offer converting to saOutAct's currency and issuer and can be implied.
|
||
// cLog(lsDEBUG) << boost::str(boost::format("setCanonical: out offer: uNode=%d uEnd=%d") % uNode % uEnd);
|
||
|
||
--uEnd;
|
||
}
|
||
|
||
// Do not include uEnd.
|
||
for (; uNode != uEnd; ++uNode)
|
||
{
|
||
// cLog(lsDEBUG) << boost::str(boost::format("setCanonical: loop: uNode=%d uEnd=%d") % uNode % uEnd);
|
||
const PaymentNode& pnPrv = psExpanded.vpnNodes[uNode-1];
|
||
const PaymentNode& pnCur = psExpanded.vpnNodes[uNode];
|
||
const PaymentNode& pnNxt = psExpanded.vpnNodes[uNode+1];
|
||
|
||
const bool bCurAccount = isSetBit(pnCur.uFlags, STPathElement::typeAccount);
|
||
|
||
bool bSkip = false;
|
||
|
||
if (bCurAccount)
|
||
{
|
||
// Currently at an account.
|
||
|
||
// Output is non-XRP and issuer is account.
|
||
if (!!pnCur.uCurrencyID && pnCur.uIssuerID == pnCur.uAccountID)
|
||
{
|
||
// Account issues itself.
|
||
// XXX Not good enough. Previous account must mention it.
|
||
|
||
bSkip = true;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Currently at an offer.
|
||
const bool bPrvAccount = isSetBit(pnPrv.uFlags, STPathElement::typeAccount);
|
||
const bool bNxtAccount = isSetBit(pnNxt.uFlags, STPathElement::typeAccount);
|
||
|
||
if (bPrvAccount && bNxtAccount // Offer surrounded by accounts.
|
||
&& pnPrv.uCurrencyID != pnNxt.uCurrencyID)
|
||
{
|
||
// Offer can be implied by currency change.
|
||
// XXX What abount issuer?
|
||
|
||
bSkip = true;
|
||
}
|
||
}
|
||
|
||
if (!bSkip)
|
||
{
|
||
// Copy node
|
||
PaymentNode pnNew;
|
||
|
||
bool bSetAccount = bCurAccount;
|
||
bool bSetCurrency = uCurrencyID != pnCur.uCurrencyID;
|
||
// XXX What if we need the next account because we want to skip it?
|
||
bool bSetIssuer = !uCurrencyID && uIssuerID != pnCur.uIssuerID;
|
||
|
||
pnNew.uFlags = (bSetAccount ? STPathElement::typeAccount : 0)
|
||
| (bSetCurrency ? STPathElement::typeCurrency : 0)
|
||
| (bSetIssuer ? STPathElement::typeIssuer : 0);
|
||
|
||
if (bSetAccount)
|
||
pnNew.uAccountID = pnCur.uAccountID;
|
||
|
||
if (bSetCurrency)
|
||
{
|
||
pnNew.uCurrencyID = pnCur.uCurrencyID;
|
||
uCurrencyID = pnNew.uCurrencyID;
|
||
}
|
||
|
||
if (bSetIssuer)
|
||
pnNew.uIssuerID = pnCur.uIssuerID;
|
||
// XXX ^^^ What about setting uIssuerID?
|
||
|
||
if (bSetCurrency && !uCurrencyID)
|
||
uIssuerID.zero();
|
||
|
||
vpnNodes.push_back(pnNew);
|
||
}
|
||
}
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("setCanonical: in=%s/%s out=%s/%s %s")
|
||
% STAmount::createHumanCurrency(uMaxCurrencyID)
|
||
% RippleAddress::createHumanAccountID(uMaxIssuerID)
|
||
% STAmount::createHumanCurrency(uOutCurrencyID)
|
||
% RippleAddress::createHumanAccountID(uOutIssuerID)
|
||
% getJson());
|
||
}
|
||
|
||
// Build a canonicalized STPathSet from a vector of PathStates.
|
||
void RippleCalc::setCanonical(STPathSet& spsDst, const std::vector<PathState::pointer>& vpsExpanded, bool bKeepDefault)
|
||
{
|
||
// cLog(lsDEBUG) << boost::str(boost::format("SET: setCanonical> %d") % vpsExpanded.size());
|
||
|
||
BOOST_FOREACH(PathState::ref pspExpanded, vpsExpanded)
|
||
{
|
||
// Obvious defaults have 2 nodes when expanded.
|
||
if (bKeepDefault || 2 != pspExpanded->vpnNodes.size())
|
||
{
|
||
PathState psCanonical(*pspExpanded, false); // Doesn't copy.
|
||
|
||
// cLog(lsDEBUG) << boost::str(boost::format("SET: setCanonical: %d %d %s") % bKeepDirect % pspExpanded->vpnNodes.size() % pspExpanded->getJson());
|
||
|
||
psCanonical.setCanonical(*pspExpanded); // Convert.
|
||
|
||
// Non-obvious defaults have 0 nodes when canonicalized.
|
||
if (bKeepDefault || psCanonical.vpnNodes.size())
|
||
{
|
||
STPath spCanonical;
|
||
|
||
BOOST_FOREACH(const PaymentNode& pnElem, psCanonical.vpnNodes)
|
||
{
|
||
STPathElement speElem(pnElem.uFlags, pnElem.uAccountID, pnElem.uCurrencyID, pnElem.uIssuerID);
|
||
|
||
spCanonical.addElement(speElem);
|
||
}
|
||
|
||
spsDst.addPath(spCanonical);
|
||
}
|
||
}
|
||
}
|
||
|
||
// cLog(lsDEBUG) << boost::str(boost::format("SET: setCanonical< %d") % spsDst.size());
|
||
}
|
||
|
||
Json::Value PathState::getJson() const
|
||
{
|
||
Json::Value jvPathState(Json::objectValue);
|
||
Json::Value jvNodes(Json::arrayValue);
|
||
|
||
BOOST_FOREACH(const PaymentNode& pnNode, vpnNodes)
|
||
{
|
||
Json::Value jvNode(Json::objectValue);
|
||
|
||
Json::Value jvFlags(Json::arrayValue);
|
||
|
||
if (pnNode.uFlags & STPathElement::typeAccount)
|
||
jvFlags.append("account");
|
||
|
||
jvNode["flags"] = jvFlags;
|
||
|
||
if (pnNode.uFlags & STPathElement::typeAccount)
|
||
jvNode["account"] = RippleAddress::createHumanAccountID(pnNode.uAccountID);
|
||
|
||
if (!!pnNode.uCurrencyID)
|
||
jvNode["currency"] = STAmount::createHumanCurrency(pnNode.uCurrencyID);
|
||
|
||
if (!!pnNode.uIssuerID)
|
||
jvNode["issuer"] = RippleAddress::createHumanAccountID(pnNode.uIssuerID);
|
||
|
||
if (pnNode.saRevRedeem)
|
||
jvNode["rev_redeem"] = pnNode.saRevRedeem.getFullText();
|
||
|
||
if (pnNode.saRevIssue)
|
||
jvNode["rev_issue"] = pnNode.saRevIssue.getFullText();
|
||
|
||
if (pnNode.saRevDeliver)
|
||
jvNode["rev_deliver"] = pnNode.saRevDeliver.getFullText();
|
||
|
||
if (pnNode.saFwdRedeem)
|
||
jvNode["fwd_redeem"] = pnNode.saFwdRedeem.getFullText();
|
||
|
||
if (pnNode.saFwdIssue)
|
||
jvNode["fwd_issue"] = pnNode.saFwdIssue.getFullText();
|
||
|
||
if (pnNode.saFwdDeliver)
|
||
jvNode["fwd_deliver"] = pnNode.saFwdDeliver.getFullText();
|
||
|
||
jvNodes.append(jvNode);
|
||
}
|
||
|
||
jvPathState["status"] = terStatus;
|
||
jvPathState["index"] = mIndex;
|
||
jvPathState["nodes"] = jvNodes;
|
||
|
||
if (saInReq)
|
||
jvPathState["in_req"] = saInReq.getJson(0);
|
||
|
||
if (saInAct)
|
||
jvPathState["in_act"] = saInAct.getJson(0);
|
||
|
||
if (saInPass)
|
||
jvPathState["in_pass"] = saInPass.getJson(0);
|
||
|
||
if (saOutReq)
|
||
jvPathState["out_req"] = saOutReq.getJson(0);
|
||
|
||
if (saOutAct)
|
||
jvPathState["out_act"] = saOutAct.getJson(0);
|
||
|
||
if (saOutPass)
|
||
jvPathState["out_pass"] = saOutPass.getJson(0);
|
||
|
||
if (uQuality)
|
||
jvPathState["uQuality"] = boost::str(boost::format("%d") % uQuality);
|
||
|
||
return jvPathState;
|
||
}
|
||
|
||
//
|
||
// RippleCalc implementation
|
||
//
|
||
|
||
// If needed, advance to next funded offer.
|
||
// - Automatically advances to first offer.
|
||
// - Set bEntryAdvance to advance to next entry.
|
||
// <-- uOfferIndex : 0=end of list.
|
||
TER RippleCalc::calcNodeAdvance(
|
||
const unsigned int uNode, // 0 < uNode < uLast
|
||
PathState& psCur,
|
||
const bool bMultiQuality,
|
||
const bool bReverse)
|
||
{
|
||
PaymentNode& pnPrv = psCur.vpnNodes[uNode-1];
|
||
PaymentNode& pnCur = psCur.vpnNodes[uNode];
|
||
|
||
const uint160& uPrvCurrencyID = pnPrv.uCurrencyID;
|
||
const uint160& uPrvIssuerID = pnPrv.uIssuerID;
|
||
const uint160& uCurCurrencyID = pnCur.uCurrencyID;
|
||
const uint160& uCurIssuerID = pnCur.uIssuerID;
|
||
|
||
uint256& uDirectTip = pnCur.uDirectTip;
|
||
uint256& uDirectEnd = pnCur.uDirectEnd;
|
||
bool& bDirectAdvance = pnCur.bDirectAdvance;
|
||
SLE::pointer& sleDirectDir = pnCur.sleDirectDir;
|
||
STAmount& saOfrRate = pnCur.saOfrRate;
|
||
|
||
bool& bEntryAdvance = pnCur.bEntryAdvance;
|
||
unsigned int& uEntry = pnCur.uEntry;
|
||
uint256& uOfferIndex = pnCur.uOfferIndex;
|
||
SLE::pointer& sleOffer = pnCur.sleOffer;
|
||
uint160& uOfrOwnerID = pnCur.uOfrOwnerID;
|
||
STAmount& saOfferFunds = pnCur.saOfferFunds;
|
||
STAmount& saTakerPays = pnCur.saTakerPays;
|
||
STAmount& saTakerGets = pnCur.saTakerGets;
|
||
bool& bFundsDirty = pnCur.bFundsDirty;
|
||
|
||
TER terResult = tesSUCCESS;
|
||
|
||
cLog(lsDEBUG) << "calcNodeAdvance";
|
||
|
||
int loopCount = 0;
|
||
do
|
||
{
|
||
if (++loopCount > 20)
|
||
{
|
||
cLog(lsWARNING) << "Loop count exceeded";
|
||
return tefEXCEPTION;
|
||
}
|
||
|
||
bool bDirectDirDirty = false;
|
||
|
||
if (!uDirectTip)
|
||
{
|
||
// Need to initialize current node.
|
||
|
||
uDirectTip = Ledger::getBookBase(uPrvCurrencyID, uPrvIssuerID, uCurCurrencyID, uCurIssuerID);
|
||
uDirectEnd = Ledger::getQualityNext(uDirectTip);
|
||
|
||
sleDirectDir = lesActive.entryCache(ltDIR_NODE, uDirectTip);
|
||
bDirectAdvance = !sleDirectDir;
|
||
bDirectDirDirty = true;
|
||
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeAdvance: Initialize node: uDirectTip=%s uDirectEnd=%s bDirectAdvance=%d") % uDirectTip % uDirectEnd % bDirectAdvance);
|
||
}
|
||
|
||
if (bDirectAdvance)
|
||
{
|
||
// Get next quality.
|
||
uDirectTip = lesActive.getLedger()->getNextLedgerIndex(uDirectTip, uDirectEnd);
|
||
bDirectDirDirty = true;
|
||
bDirectAdvance = false;
|
||
|
||
if (!!uDirectTip)
|
||
{
|
||
// Have another quality directory.
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeAdvance: Quality advance: uDirectTip=%s") % uDirectTip);
|
||
|
||
sleDirectDir = lesActive.entryCache(ltDIR_NODE, uDirectTip);
|
||
}
|
||
else if (bReverse)
|
||
{
|
||
cLog(lsTRACE) << "calcNodeAdvance: No more offers.";
|
||
|
||
uOfferIndex = 0;
|
||
break;
|
||
}
|
||
else
|
||
{
|
||
// No more offers. Should be done rather than fall off end of book.
|
||
cLog(lsWARNING) << "calcNodeAdvance: Unreachable: Fell off end of order book.";
|
||
assert(false);
|
||
|
||
terResult = tefEXCEPTION;
|
||
}
|
||
}
|
||
|
||
if (bDirectDirDirty)
|
||
{
|
||
saOfrRate = STAmount::setRate(Ledger::getQuality(uDirectTip)); // For correct ratio
|
||
uEntry = 0;
|
||
bEntryAdvance = true;
|
||
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeAdvance: directory dirty: saOfrRate=%s") % saOfrRate);
|
||
}
|
||
|
||
if (!bEntryAdvance)
|
||
{
|
||
if (bFundsDirty)
|
||
{
|
||
saTakerPays = sleOffer->getFieldAmount(sfTakerPays);
|
||
saTakerGets = sleOffer->getFieldAmount(sfTakerGets);
|
||
|
||
saOfferFunds = lesActive.accountFunds(uOfrOwnerID, saTakerGets); // Funds left.
|
||
bFundsDirty = false;
|
||
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeAdvance: funds dirty: saOfrRate=%s") % saOfrRate);
|
||
}
|
||
else
|
||
{
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeAdvance: as is"));
|
||
nothing();
|
||
}
|
||
}
|
||
else if (!lesActive.dirNext(uDirectTip, sleDirectDir, uEntry, uOfferIndex))
|
||
{
|
||
// Failed to find an entry in directory.
|
||
|
||
uOfferIndex = 0;
|
||
|
||
// Do another cur directory iff bMultiQuality
|
||
if (bMultiQuality)
|
||
{
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeAdvance: next quality"));
|
||
bDirectAdvance = true;
|
||
}
|
||
else if (!bReverse)
|
||
{
|
||
cLog(lsWARNING) << boost::str(boost::format("calcNodeAdvance: unreachable: ran out of offers"));
|
||
assert(false); // Can't run out of offers in forward direction.
|
||
terResult = tefEXCEPTION;
|
||
}
|
||
else
|
||
bEntryAdvance = false;
|
||
}
|
||
else
|
||
{
|
||
// Got a new offer.
|
||
sleOffer = lesActive.entryCache(ltOFFER, uOfferIndex);
|
||
uOfrOwnerID = sleOffer->getFieldAccount(sfAccount).getAccountID();
|
||
saTakerPays = sleOffer->getFieldAmount(sfTakerPays);
|
||
saTakerGets = sleOffer->getFieldAmount(sfTakerGets);
|
||
|
||
const aciSource asLine = boost::make_tuple(uOfrOwnerID, uCurCurrencyID, uCurIssuerID);
|
||
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeAdvance: uOfrOwnerID=%s") % RippleAddress::createHumanAccountID(uOfrOwnerID));
|
||
|
||
if (sleOffer->isFieldPresent(sfExpiration) && sleOffer->getFieldU32(sfExpiration) <= lesActive.getLedger()->getParentCloseTimeNC())
|
||
{
|
||
// Offer is expired.
|
||
cLog(lsTRACE) << "calcNodeAdvance: expired offer";
|
||
|
||
assert(musUnfundedFound.find(uOfferIndex) != musUnfundedFound.end()); // Verify reverse found it too.
|
||
bEntryAdvance = true;
|
||
continue;
|
||
}
|
||
else if (!saTakerPays.isPositive() || !saTakerGets.isPositive())
|
||
{
|
||
// Offer is has bad amounts.
|
||
cLog(lsWARNING) << boost::str(boost::format("calcNodeAdvance: NON-POSITIVE: saTakerPays=%s saTakerGets=%s")
|
||
% saTakerPays % saTakerGets);
|
||
|
||
// assert(musUnfundedFound.find(uOfferIndex) != musUnfundedFound.end()); // Verify reverse found it too.
|
||
bEntryAdvance = true;
|
||
continue;
|
||
}
|
||
|
||
// Allowed to access source from this node?
|
||
// XXX This can get called multiple times for same source in a row, caching result would be nice.
|
||
// XXX Going forward could we fund something with a worse quality which was previously skipped? Might need to check
|
||
// quality.
|
||
curIssuerNodeConstIterator itForward = psCur.umForward.find(asLine);
|
||
const bool bFoundForward = itForward != psCur.umForward.end();
|
||
|
||
if (bFoundForward && itForward->second != uNode)
|
||
{
|
||
// Temporarily unfunded. Another node uses this source, ignore in this offer.
|
||
cLog(lsTRACE) << "calcNodeAdvance: temporarily unfunded offer (forward)";
|
||
|
||
bEntryAdvance = true;
|
||
continue;
|
||
}
|
||
|
||
curIssuerNodeConstIterator itPast = mumSource.find(asLine);
|
||
bool bFoundPast = itPast != mumSource.end();
|
||
|
||
if (bFoundPast && itPast->second != uNode)
|
||
{
|
||
// Temporarily unfunded. Another node uses this source, ignore in this offer.
|
||
cLog(lsTRACE) << "calcNodeAdvance: temporarily unfunded offer (past)";
|
||
|
||
bEntryAdvance = true;
|
||
continue;
|
||
}
|
||
|
||
curIssuerNodeConstIterator itReverse = psCur.umReverse.find(asLine);
|
||
bool bFoundReverse = itReverse != psCur.umReverse.end();
|
||
|
||
if (bFoundReverse && itReverse->second != uNode)
|
||
{
|
||
// Temporarily unfunded. Another node uses this source, ignore in this offer.
|
||
cLog(lsTRACE) << "calcNodeAdvance: temporarily unfunded offer (reverse)";
|
||
|
||
bEntryAdvance = true;
|
||
continue;
|
||
}
|
||
|
||
saOfferFunds = lesActive.accountFunds(uOfrOwnerID, saTakerGets); // Funds left.
|
||
|
||
if (!saOfferFunds.isPositive())
|
||
{
|
||
// Offer is unfunded.
|
||
cLog(lsTRACE) << "calcNodeAdvance: unfunded offer";
|
||
|
||
if (bReverse && !bFoundReverse && !bFoundPast)
|
||
{
|
||
// Never mentioned before: found unfunded.
|
||
musUnfundedFound.insert(uOfferIndex); // Mark offer for always deletion.
|
||
}
|
||
|
||
// YYY Could verify offer is correct place for unfundeds.
|
||
bEntryAdvance = true;
|
||
continue;
|
||
}
|
||
|
||
if (bReverse // Need to remember reverse mention.
|
||
&& !bFoundPast // Not mentioned in previous passes.
|
||
&& !bFoundReverse) // Not mentioned for pass.
|
||
{
|
||
// Consider source mentioned by current path state.
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeAdvance: remember=%s/%s/%s")
|
||
% RippleAddress::createHumanAccountID(uOfrOwnerID)
|
||
% STAmount::createHumanCurrency(uCurCurrencyID)
|
||
% RippleAddress::createHumanAccountID(uCurIssuerID));
|
||
|
||
psCur.umReverse.insert(std::make_pair(asLine, uNode));
|
||
}
|
||
|
||
bFundsDirty = false;
|
||
bEntryAdvance = false;
|
||
}
|
||
}
|
||
while (tesSUCCESS == terResult && (bEntryAdvance || bDirectAdvance));
|
||
|
||
if (tesSUCCESS == terResult)
|
||
{
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAdvance: uOfferIndex=%s") % uOfferIndex);
|
||
}
|
||
else
|
||
{
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAdvance: terResult=%s") % transToken(terResult));
|
||
}
|
||
|
||
return terResult;
|
||
}
|
||
|
||
// Between offer nodes, the fee charged may vary. Therefore, process one inbound offer at a time. Propagate the inbound offer's
|
||
// requirements to the previous node. The previous node adjusts the amount output and the amount spent on fees. Continue process
|
||
// till request is satisified while we the rate does not increase past the initial rate.
|
||
TER RippleCalc::calcNodeDeliverRev(
|
||
const unsigned int uNode, // 0 < uNode < uLast
|
||
PathState& psCur,
|
||
const bool bMultiQuality,
|
||
const uint160& uOutAccountID, // --> Output owner's account.
|
||
const STAmount& saOutReq, // --> Funds wanted.
|
||
STAmount& saOutAct) // <-- Funds delivered.
|
||
{
|
||
TER terResult = tesSUCCESS;
|
||
|
||
PaymentNode& pnPrv = psCur.vpnNodes[uNode-1];
|
||
PaymentNode& pnCur = psCur.vpnNodes[uNode];
|
||
|
||
const uint160& uCurIssuerID = pnCur.uIssuerID;
|
||
const uint160& uPrvAccountID = pnPrv.uAccountID;
|
||
const STAmount& saTransferRate = pnCur.saTransferRate;
|
||
|
||
STAmount& saPrvDlvReq = pnPrv.saRevDeliver; // To be set.
|
||
STAmount& saCurDlvFwd = pnCur.saFwdDeliver;
|
||
|
||
uint256& uDirectTip = pnCur.uDirectTip;
|
||
|
||
uDirectTip = 0; // Restart book searching.
|
||
|
||
saCurDlvFwd.zero(saOutReq); // For forward pass zero deliver.
|
||
|
||
saPrvDlvReq.zero(pnPrv.uCurrencyID, pnPrv.uIssuerID);
|
||
saOutAct.zero(saOutReq);
|
||
|
||
int loopCount = 0;
|
||
while (saOutAct != saOutReq) // Did not deliver limit.
|
||
{
|
||
if (++loopCount > 40)
|
||
{
|
||
cLog(lsFATAL) << "loop count exceeded";
|
||
return mOpenLedger ? telFAILED_PROCESSING : tecFAILED_PROCESSING;
|
||
}
|
||
|
||
bool& bEntryAdvance = pnCur.bEntryAdvance;
|
||
STAmount& saOfrRate = pnCur.saOfrRate;
|
||
uint256& uOfferIndex = pnCur.uOfferIndex;
|
||
SLE::pointer& sleOffer = pnCur.sleOffer;
|
||
const uint160& uOfrOwnerID = pnCur.uOfrOwnerID;
|
||
bool& bFundsDirty = pnCur.bFundsDirty;
|
||
STAmount& saOfferFunds = pnCur.saOfferFunds;
|
||
STAmount& saTakerPays = pnCur.saTakerPays;
|
||
STAmount& saTakerGets = pnCur.saTakerGets;
|
||
STAmount& saRateMax = pnCur.saRateMax;
|
||
|
||
terResult = calcNodeAdvance(uNode, psCur, bMultiQuality, true); // If needed, advance to next funded offer.
|
||
|
||
if (tesSUCCESS != terResult || !uOfferIndex)
|
||
{
|
||
// Error or out of offers.
|
||
break;
|
||
}
|
||
|
||
const STAmount saOutFeeRate = uOfrOwnerID == uCurIssuerID || uOutAccountID == uCurIssuerID // Issuer receiving or sending.
|
||
? saOne // No fee.
|
||
: saTransferRate; // Transfer rate of issuer.
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverRev: uOfrOwnerID=%s uOutAccountID=%s uCurIssuerID=%s saTransferRate=%s saOutFeeRate=%s")
|
||
% RippleAddress::createHumanAccountID(uOfrOwnerID)
|
||
% RippleAddress::createHumanAccountID(uOutAccountID)
|
||
% RippleAddress::createHumanAccountID(uCurIssuerID)
|
||
% saTransferRate.getFullText()
|
||
% saOutFeeRate.getFullText());
|
||
|
||
if (!saRateMax)
|
||
{
|
||
// Set initial rate.
|
||
saRateMax = saOutFeeRate;
|
||
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverRev: Set initial rate: saRateMax=%s saOutFeeRate=%s")
|
||
% saRateMax
|
||
% saOutFeeRate);
|
||
}
|
||
else if (saOutFeeRate > saRateMax)
|
||
{
|
||
// Offer exceeds initial rate.
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverRev: Offer exceeds initial rate: saRateMax=%s saOutFeeRate=%s")
|
||
% saRateMax
|
||
% saOutFeeRate);
|
||
|
||
break; // Done. Don't bother looking for smaller saTransferRates.
|
||
}
|
||
else if (saOutFeeRate < saRateMax)
|
||
{
|
||
// Reducing rate. Additional offers will only considered for this increment if they are at least this good.
|
||
|
||
saRateMax = saOutFeeRate;
|
||
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverRev: Reducing rate: saRateMax=%s")
|
||
% saRateMax);
|
||
}
|
||
|
||
// Amount that goes to the taker.
|
||
STAmount saOutPass = std::min(std::min(saOfferFunds, saTakerGets), saOutReq-saOutAct); // Offer maximum out - assuming no out fees.
|
||
// Amount charged to the offer owner.
|
||
// The fee goes to issuer. The fee is paid by offer owner and not passed as a cost to taker.
|
||
STAmount saOutPlusFees = STAmount::multiply(saOutPass, saOutFeeRate); // Offer out with fees.
|
||
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverRev: saOutReq=%s saOutAct=%s saTakerGets=%s saOutPass=%s saOutPlusFees=%s saOfferFunds=%s")
|
||
% saOutReq
|
||
% saOutAct
|
||
% saTakerGets
|
||
% saOutPass
|
||
% saOutPlusFees
|
||
% saOfferFunds);
|
||
|
||
if (saOutPlusFees > saOfferFunds)
|
||
{
|
||
// Offer owner can not cover all fees, compute saOutPass based on saOfferFunds.
|
||
|
||
saOutPlusFees = saOfferFunds;
|
||
saOutPass = STAmount::divide(saOutPlusFees, saOutFeeRate);
|
||
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverRev: Total exceeds fees: saOutPass=%s saOutPlusFees=%s saOfferFunds=%s")
|
||
% saOutPass
|
||
% saOutPlusFees
|
||
% saOfferFunds);
|
||
}
|
||
|
||
// Compute portion of input needed to cover actual output.
|
||
|
||
// XXX This needs to round up!
|
||
STAmount saInPassReq = STAmount::multiply(saOutPass, saOfrRate, saTakerPays);
|
||
STAmount saInPassAct;
|
||
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverRev: saInPassReq=%s saOfrRate=%s saOutPass=%s saOutPlusFees=%s")
|
||
% saInPassReq
|
||
% saOfrRate
|
||
% saOutPass
|
||
% saOutPlusFees);
|
||
|
||
// Find out input amount actually available at current rate.
|
||
if (!!uPrvAccountID)
|
||
{
|
||
// account --> OFFER --> ?
|
||
// Due to node expansion, previous is guaranteed to be the issuer.
|
||
// Previous is the issuer and receiver is an offer, so no fee or quality.
|
||
// Previous is the issuer and has unlimited funds.
|
||
// Offer owner is obtaining IOUs via an offer, so credit line limits are ignored.
|
||
// As limits are ignored, don't need to adjust previous account's balance.
|
||
|
||
saInPassAct = saInPassReq;
|
||
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverRev: account --> OFFER --> ? : saInPassAct=%s")
|
||
% saPrvDlvReq);
|
||
}
|
||
else
|
||
{
|
||
// offer --> OFFER --> ?
|
||
// Chain and compute the previous offer now.
|
||
|
||
terResult = calcNodeDeliverRev(
|
||
uNode-1,
|
||
psCur,
|
||
bMultiQuality,
|
||
uOfrOwnerID,
|
||
saInPassReq,
|
||
saInPassAct);
|
||
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverRev: offer --> OFFER --> ? : saInPassAct=%s")
|
||
% saInPassAct);
|
||
}
|
||
|
||
if (tesSUCCESS != terResult)
|
||
break;
|
||
|
||
if (saInPassAct != saInPassReq)
|
||
{
|
||
// Adjust output to conform to limited input.
|
||
saOutPass = STAmount::divide(saInPassAct, saOfrRate, saTakerGets);
|
||
saOutPlusFees = STAmount::multiply(saOutPass, saOutFeeRate);
|
||
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverRev: adjusted: saOutPass=%s saOutPlusFees=%s")
|
||
% saOutPass
|
||
% saOutPlusFees);
|
||
}
|
||
|
||
// Funds were spent.
|
||
bFundsDirty = true;
|
||
|
||
// Want to deduct output to limit calculations while computing reverse. Don't actually need to send.
|
||
// Sending could be complicated: could fund a previous offer not yet visited.
|
||
// However, these deductions and adjustments are tenative.
|
||
// Must reset balances when going forward to perform actual transfers.
|
||
terResult = lesActive.accountSend(uOfrOwnerID, uCurIssuerID, saOutPass);
|
||
|
||
if (tesSUCCESS != terResult)
|
||
break;
|
||
|
||
// Adjust offer
|
||
STAmount saTakerGetsNew = saTakerGets - saOutPass;
|
||
STAmount saTakerPaysNew = saTakerPays - saInPassAct;
|
||
|
||
if (saTakerPaysNew.isNegative() || saTakerGetsNew.isNegative())
|
||
{
|
||
cLog(lsWARNING) << boost::str(boost::format("calcNodeDeliverRev: NEGATIVE: saTakerPaysNew=%s saTakerGetsNew=%s")
|
||
% saTakerPaysNew % saTakerGetsNew);
|
||
|
||
terResult = mOpenLedger
|
||
? telFAILED_PROCESSING // Ledger is not final, can vote no.
|
||
: tecFAILED_PROCESSING;
|
||
break;
|
||
}
|
||
|
||
sleOffer->setFieldAmount(sfTakerGets, saTakerGetsNew);
|
||
sleOffer->setFieldAmount(sfTakerPays, saTakerPaysNew);
|
||
|
||
lesActive.entryModify(sleOffer);
|
||
|
||
if (saOutPass == saTakerGets)
|
||
{
|
||
// Offer became unfunded.
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverRev: offer became unfunded."));
|
||
|
||
bEntryAdvance = true;
|
||
}
|
||
|
||
saOutAct += saOutPass;
|
||
saPrvDlvReq += saInPassAct;
|
||
}
|
||
|
||
if (tesSUCCESS == terResult && !saOutAct)
|
||
terResult = tecPATH_DRY;
|
||
|
||
return terResult;
|
||
}
|
||
|
||
// For current offer, get input from deliver/limbo and output to next account or deliver for next offers.
|
||
// <-- pnCur.saFwdDeliver: For calcNodeAccountFwd to know how much went through
|
||
TER RippleCalc::calcNodeDeliverFwd(
|
||
const unsigned int uNode, // 0 < uNode < uLast
|
||
PathState& psCur,
|
||
const bool bMultiQuality,
|
||
const uint160& uInAccountID, // --> Input owner's account.
|
||
const STAmount& saInReq, // --> Amount to deliver.
|
||
STAmount& saInAct, // <-- Amount delivered, this invokation.
|
||
STAmount& saInFees) // <-- Fees charged, this invokation.
|
||
{
|
||
TER terResult = tesSUCCESS;
|
||
|
||
PaymentNode& pnPrv = psCur.vpnNodes[uNode-1];
|
||
PaymentNode& pnCur = psCur.vpnNodes[uNode];
|
||
PaymentNode& pnNxt = psCur.vpnNodes[uNode+1];
|
||
|
||
const uint160& uNxtAccountID = pnNxt.uAccountID;
|
||
const uint160& uCurCurrencyID = pnCur.uCurrencyID;
|
||
const uint160& uCurIssuerID = pnCur.uIssuerID;
|
||
const uint160& uPrvCurrencyID = pnPrv.uCurrencyID;
|
||
const uint160& uPrvIssuerID = pnPrv.uIssuerID;
|
||
const STAmount& saInTransRate = pnPrv.saTransferRate;
|
||
|
||
STAmount& saCurDeliverAct = pnCur.saFwdDeliver; // Zeroed in reverse pass.
|
||
|
||
uint256& uDirectTip = pnCur.uDirectTip;
|
||
|
||
uDirectTip = 0; // Restart book searching.
|
||
|
||
saInAct.zero(saInReq);
|
||
saInFees.zero(saInReq);
|
||
|
||
while (tesSUCCESS == terResult
|
||
&& saInAct + saInFees != saInReq) // Did not deliver all funds.
|
||
{
|
||
// Determine values for pass to adjust saInAct, saInFees, and saCurDeliverAct
|
||
terResult = calcNodeAdvance(uNode, psCur, bMultiQuality, false); // If needed, advance to next funded offer.
|
||
|
||
if (tesSUCCESS == terResult)
|
||
{
|
||
// Doesn't charge input. Input funds are in limbo.
|
||
bool& bEntryAdvance = pnCur.bEntryAdvance;
|
||
STAmount& saOfrRate = pnCur.saOfrRate;
|
||
uint256& uOfferIndex = pnCur.uOfferIndex;
|
||
SLE::pointer& sleOffer = pnCur.sleOffer;
|
||
const uint160& uOfrOwnerID = pnCur.uOfrOwnerID;
|
||
bool& bFundsDirty = pnCur.bFundsDirty;
|
||
STAmount& saOfferFunds = pnCur.saOfferFunds;
|
||
STAmount& saTakerPays = pnCur.saTakerPays;
|
||
STAmount& saTakerGets = pnCur.saTakerGets;
|
||
|
||
const STAmount saInFeeRate = !uPrvCurrencyID // XRP.
|
||
|| uInAccountID == uPrvIssuerID // Sender is issuer.
|
||
|| uOfrOwnerID == uPrvIssuerID // Reciever is issuer.
|
||
? saOne // No fee.
|
||
: saInTransRate; // Transfer rate of issuer.
|
||
|
||
// First calculate assuming no output fees: saInPassAct, saInPassFees, saOutPassAct
|
||
|
||
STAmount saOutFunded = std::min(saOfferFunds, saTakerGets); // Offer maximum out - If there are no out fees.
|
||
STAmount saInFunded = STAmount::multiply(saOutFunded, saOfrRate, saTakerPays); // Offer maximum in - Limited by by payout.
|
||
STAmount saInTotal = STAmount::multiply(saInFunded, saInTransRate); // Offer maximum in with fees.
|
||
STAmount saInSum = std::min(saInTotal, saInReq-saInAct-saInFees); // In limited by remaining.
|
||
STAmount saInPassAct = STAmount::divide(saInSum, saInFeeRate); // In without fees.
|
||
STAmount saOutPassMax = STAmount::divide(saInPassAct, saOfrRate, saTakerGets); // Out limited by in remaining.
|
||
|
||
STAmount saInPassFeesMax = saInSum-saInPassAct;
|
||
|
||
STAmount saOutPassAct; // Will be determined by next node.
|
||
STAmount saInPassFees; // Will be determined by adjusted saInPassAct.
|
||
|
||
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverFwd: uNode=%d saOutFunded=%s saInReq=%s saInAct=%s saInFees=%s saInFunded=%s saInTotal=%s saInSum=%s saInPassAct=%s saOutPassMax=%s")
|
||
% uNode
|
||
% saOutFunded
|
||
% saInReq
|
||
% saInAct
|
||
% saInFees
|
||
% saInFunded
|
||
% saInTotal
|
||
% saInSum
|
||
% saInPassAct
|
||
% saOutPassMax);
|
||
|
||
if (!!uNxtAccountID)
|
||
{
|
||
// ? --> OFFER --> account
|
||
// Input fees: vary based upon the consumed offer's owner.
|
||
// Output fees: none as XRP or the destination account is the issuer.
|
||
|
||
saOutPassAct = saOutPassMax;
|
||
saInPassFees = saInPassFeesMax;
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeDeliverFwd: ? --> OFFER --> account: uOfrOwnerID=%s uNxtAccountID=%s saOutPassAct=%s")
|
||
% RippleAddress::createHumanAccountID(uOfrOwnerID)
|
||
% RippleAddress::createHumanAccountID(uNxtAccountID)
|
||
% saOutPassAct.getFullText());
|
||
|
||
// Output: Debit offer owner, send XRP or non-XPR to next account.
|
||
terResult = lesActive.accountSend(uOfrOwnerID, uNxtAccountID, saOutPassAct);
|
||
|
||
if (tesSUCCESS != terResult)
|
||
break;
|
||
}
|
||
else
|
||
{
|
||
// ? --> OFFER --> offer
|
||
// Offer to offer means current order book's output currency and issuer match next order book's input current and
|
||
// issuer.
|
||
// Output fees: possible if issuer has fees and is not on either side.
|
||
STAmount saOutPassFees;
|
||
|
||
// Output fees vary as the next nodes offer owners may vary.
|
||
// Therefore, immediately push through output for current offer.
|
||
terResult = RippleCalc::calcNodeDeliverFwd(
|
||
uNode+1,
|
||
psCur,
|
||
bMultiQuality,
|
||
uOfrOwnerID, // --> Current holder.
|
||
saOutPassMax, // --> Amount available.
|
||
saOutPassAct, // <-- Amount delivered.
|
||
saOutPassFees); // <-- Fees charged.
|
||
|
||
if (tesSUCCESS != terResult)
|
||
break;
|
||
|
||
if (saOutPassAct == saOutPassMax)
|
||
{
|
||
// No fees and entire output amount.
|
||
|
||
saInPassFees = saInPassFeesMax;
|
||
}
|
||
else
|
||
{
|
||
// Fraction of output amount.
|
||
// Output fees are paid by offer owner and not passed to previous.
|
||
saInPassAct = STAmount::multiply(saOutPassAct, saOfrRate, saInReq);
|
||
saInPassFees = std::min(saInPassFeesMax, STAmount::multiply(saInPassAct, saInFeeRate));
|
||
}
|
||
|
||
// Do outbound debiting.
|
||
// Send to issuer/limbo total amount including fees (issuer gets fees).
|
||
lesActive.accountSend(uOfrOwnerID, !!uCurCurrencyID ? uCurIssuerID : ACCOUNT_XRP, saOutPassAct+saOutPassFees);
|
||
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverFwd: ? --> OFFER --> offer: saOutPassAct=%s saOutPassFees=%s")
|
||
% saOutPassAct
|
||
% saOutPassFees);
|
||
}
|
||
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeDeliverFwd: uNode=%d saTakerGets=%s saTakerPays=%s saInPassAct=%s saInPassFees=%s saOutPassAct=%s saOutFunded=%s")
|
||
% uNode
|
||
% saTakerGets
|
||
% saTakerPays
|
||
% saInPassAct
|
||
% saInPassFees
|
||
% saOutPassAct
|
||
% saOutFunded);
|
||
|
||
// Funds were spent.
|
||
bFundsDirty = true;
|
||
|
||
// Do inbound crediting.
|
||
// Credit offer owner from in issuer/limbo (input transfer fees left with owner).
|
||
terResult = lesActive.accountSend(!!uPrvCurrencyID ? uInAccountID : ACCOUNT_XRP, uOfrOwnerID, saInPassAct);
|
||
|
||
if (tesSUCCESS != terResult)
|
||
break;
|
||
|
||
// Adjust offer
|
||
// Fees are considered paid from a seperate budget and are not named in the offer.
|
||
STAmount saTakerGetsNew = saTakerGets - saOutPassAct;
|
||
STAmount saTakerPaysNew = saTakerPays - saInPassAct;
|
||
|
||
if (saTakerPaysNew.isNegative() || saTakerGetsNew.isNegative())
|
||
{
|
||
cLog(lsWARNING) << boost::str(boost::format("calcNodeDeliverFwd: NEGATIVE: saTakerPaysNew=%s saTakerGetsNew=%s")
|
||
% saTakerPaysNew % saTakerGetsNew);
|
||
|
||
terResult = mOpenLedger
|
||
? telFAILED_PROCESSING // Ledger is not final, can vote no.
|
||
: tecFAILED_PROCESSING;
|
||
break;
|
||
}
|
||
|
||
sleOffer->setFieldAmount(sfTakerGets, saTakerGetsNew);
|
||
sleOffer->setFieldAmount(sfTakerPays, saTakerPaysNew);
|
||
|
||
lesActive.entryModify(sleOffer);
|
||
|
||
if (saOutPassAct == saOutFunded)
|
||
{
|
||
// Offer became unfunded.
|
||
psCur.vUnfundedBecame.push_back(uOfferIndex);
|
||
bEntryAdvance = true;
|
||
}
|
||
|
||
saInAct += saInPassAct;
|
||
saInFees += saInPassFees;
|
||
|
||
// Adjust amount available to next node.
|
||
saCurDeliverAct += saOutPassAct;
|
||
}
|
||
}
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeDeliverFwd< uNode=%d saInAct=%s saInFees=%s")
|
||
% uNode
|
||
% saInAct
|
||
% saInFees);
|
||
|
||
return terResult;
|
||
}
|
||
|
||
// Called to drive from the last offer node in a chain.
|
||
TER RippleCalc::calcNodeOfferRev(
|
||
const unsigned int uNode, // 0 < uNode < uLast
|
||
PathState& psCur,
|
||
const bool bMultiQuality)
|
||
{
|
||
TER terResult;
|
||
|
||
PaymentNode& pnCur = psCur.vpnNodes[uNode];
|
||
PaymentNode& pnNxt = psCur.vpnNodes[uNode+1];
|
||
|
||
if (!!pnNxt.uAccountID)
|
||
{
|
||
// Next is an account node, resolve current offer node's deliver.
|
||
STAmount saDeliverAct;
|
||
|
||
terResult = calcNodeDeliverRev(
|
||
uNode,
|
||
psCur,
|
||
bMultiQuality,
|
||
|
||
pnNxt.uAccountID,
|
||
pnCur.saRevDeliver,
|
||
saDeliverAct);
|
||
}
|
||
else
|
||
{
|
||
// Next is an offer. Deliver has already been resolved.
|
||
terResult = tesSUCCESS;
|
||
}
|
||
|
||
return terResult;
|
||
}
|
||
|
||
// Called to drive the from the first offer node in a chain.
|
||
// - Offer input is in issuer/limbo.
|
||
// - Current offers consumed.
|
||
// - Current offer owners debited.
|
||
// - Transfer fees credited to issuer.
|
||
// - Payout to issuer or limbo.
|
||
// - Deliver is set without transfer fees.
|
||
TER RippleCalc::calcNodeOfferFwd(
|
||
const unsigned int uNode, // 0 < uNode < uLast
|
||
PathState& psCur,
|
||
const bool bMultiQuality
|
||
)
|
||
{
|
||
TER terResult;
|
||
PaymentNode& pnPrv = psCur.vpnNodes[uNode-1];
|
||
|
||
if (!!pnPrv.uAccountID)
|
||
{
|
||
// Previous is an account node, resolve its deliver.
|
||
STAmount saInAct;
|
||
STAmount saInFees;
|
||
|
||
terResult = calcNodeDeliverFwd(
|
||
uNode,
|
||
psCur,
|
||
bMultiQuality,
|
||
pnPrv.uAccountID,
|
||
pnPrv.saFwdDeliver, // Previous is sending this much.
|
||
saInAct,
|
||
saInFees);
|
||
|
||
assert(tesSUCCESS != terResult || pnPrv.saFwdDeliver == saInAct+saInFees);
|
||
}
|
||
else
|
||
{
|
||
// Previous is an offer. Deliver has already been resolved.
|
||
terResult = tesSUCCESS;
|
||
}
|
||
|
||
return terResult;
|
||
|
||
}
|
||
|
||
// Compute how much might flow for the node for the pass. Don not actually adjust balances.
|
||
// uQualityIn -> uQualityOut
|
||
// saPrvReq -> saCurReq
|
||
// sqPrvAct -> saCurAct
|
||
// This is a minimizing routine: moving in reverse it propagates the send limit to the sender, moving forward it propagates the
|
||
// actual send toward the receiver.
|
||
// This routine works backwards:
|
||
// - cur is the driver: it calculates previous wants based on previous credit limits and current wants.
|
||
// This routine works forwards:
|
||
// - prv is the driver: it calculates current deliver based on previous delivery limits and current wants.
|
||
// This routine is called one or two times for a node in a pass. If called once, it will work and set a rate. If called again,
|
||
// the new work must not worsen the previous rate.
|
||
// XXX Deal with uQualityIn or uQualityOut = 0
|
||
void RippleCalc::calcNodeRipple(
|
||
const uint32 uQualityIn,
|
||
const uint32 uQualityOut,
|
||
const STAmount& saPrvReq, // --> in limit including fees, <0 = unlimited
|
||
const STAmount& saCurReq, // --> out limit (driver)
|
||
STAmount& saPrvAct, // <-> in limit including achieved so far: <-- <= -->
|
||
STAmount& saCurAct, // <-> out limit including achieved : <-- <= -->
|
||
uint64& uRateMax)
|
||
{
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeRipple> uQualityIn=%d uQualityOut=%d saPrvReq=%s saCurReq=%s saPrvAct=%s saCurAct=%s")
|
||
% uQualityIn
|
||
% uQualityOut
|
||
% saPrvReq.getFullText()
|
||
% saCurReq.getFullText()
|
||
% saPrvAct.getFullText()
|
||
% saCurAct.getFullText());
|
||
|
||
assert(saPrvReq.getCurrency() == saCurReq.getCurrency());
|
||
|
||
const bool bPrvUnlimited = saPrvReq.isNegative();
|
||
const STAmount saPrv = bPrvUnlimited ? STAmount(saPrvReq) : saPrvReq-saPrvAct;
|
||
const STAmount saCur = saCurReq-saCurAct;
|
||
|
||
#if 0
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeRipple: bPrvUnlimited=%d saPrv=%s saCur=%s")
|
||
% bPrvUnlimited
|
||
% saPrv.getFullText()
|
||
% saCur.getFullText());
|
||
#endif
|
||
|
||
if (uQualityIn >= uQualityOut)
|
||
{
|
||
// No fee.
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeRipple: No fees"));
|
||
|
||
// Only process if we are not worsing previously processed.
|
||
if (!uRateMax || STAmount::uRateOne <= uRateMax)
|
||
{
|
||
// Limit amount to transfer if need.
|
||
STAmount saTransfer = bPrvUnlimited ? saCur : std::min(saPrv, saCur);
|
||
|
||
// In reverse, we want to propagate the limited cur to prv and set actual cur.
|
||
// In forward, we want to propagate the limited prv to cur and set actual prv.
|
||
saPrvAct += saTransfer;
|
||
saCurAct += saTransfer;
|
||
|
||
// If no rate limit, set rate limit to avoid combining with something with a worse rate.
|
||
if (!uRateMax)
|
||
uRateMax = STAmount::uRateOne;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Fee.
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeRipple: Fee"));
|
||
|
||
uint64 uRate = STAmount::getRate(STAmount(uQualityIn), STAmount(uQualityOut));
|
||
|
||
if (!uRateMax || uRate <= uRateMax)
|
||
{
|
||
const uint160 uCurrencyID = saCur.getCurrency();
|
||
const uint160 uCurIssuerID = saCur.getIssuer();
|
||
// const uint160 uPrvIssuerID = saPrv.getIssuer();
|
||
|
||
STAmount saCurIn = STAmount::divide(STAmount::multiply(saCur, uQualityOut, uCurrencyID, uCurIssuerID), uQualityIn, uCurrencyID, uCurIssuerID);
|
||
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeRipple: bPrvUnlimited=%d saPrv=%s saCurIn=%s") % bPrvUnlimited % saPrv.getFullText() % saCurIn.getFullText());
|
||
if (bPrvUnlimited || saCurIn <= saPrv)
|
||
{
|
||
// All of cur. Some amount of prv.
|
||
saCurAct += saCur;
|
||
saPrvAct += saCurIn;
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeRipple:3c: saCurReq=%s saPrvAct=%s") % saCurReq.getFullText() % saPrvAct.getFullText());
|
||
}
|
||
else
|
||
{
|
||
// A part of cur. All of prv. (cur as driver)
|
||
STAmount saCurOut = STAmount::divide(STAmount::multiply(saPrv, uQualityIn, uCurrencyID, uCurIssuerID), uQualityOut, uCurrencyID, uCurIssuerID);
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeRipple:4: saCurReq=%s") % saCurReq.getFullText());
|
||
|
||
saCurAct += saCurOut;
|
||
saPrvAct = saPrvReq;
|
||
|
||
if (!uRateMax)
|
||
uRateMax = uRate;
|
||
}
|
||
}
|
||
}
|
||
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeRipple< uQualityIn=%d uQualityOut=%d saPrvReq=%s saCurReq=%s saPrvAct=%s saCurAct=%s")
|
||
% uQualityIn
|
||
% uQualityOut
|
||
% saPrvReq.getFullText()
|
||
% saCurReq.getFullText()
|
||
% saPrvAct.getFullText()
|
||
% saCurAct.getFullText());
|
||
}
|
||
|
||
// Calculate saPrvRedeemReq, saPrvIssueReq, saPrvDeliver from saCur...
|
||
// Based on required deliverable, propagate redeem, issue, and deliver requests to the previous node.
|
||
// Inflate amount requested by required fees.
|
||
// Reedems are limited based on IOUs previous has on hand.
|
||
// Issues are limited based on credit limits and amount owed.
|
||
// No account balance adjustments as we don't know how much is going to actually be pushed through yet.
|
||
// <-- tesSUCCESS or tecPATH_DRY
|
||
TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, const bool bMultiQuality)
|
||
{
|
||
TER terResult = tesSUCCESS;
|
||
const unsigned int uLast = psCur.vpnNodes.size() - 1;
|
||
|
||
uint64 uRateMax = 0;
|
||
|
||
PaymentNode& pnPrv = psCur.vpnNodes[uNode ? uNode-1 : 0];
|
||
PaymentNode& pnCur = psCur.vpnNodes[uNode];
|
||
PaymentNode& pnNxt = psCur.vpnNodes[uNode == uLast ? uLast : uNode+1];
|
||
|
||
// Current is allowed to redeem to next.
|
||
const bool bPrvAccount = !uNode || isSetBit(pnPrv.uFlags, STPathElement::typeAccount);
|
||
const bool bNxtAccount = uNode == uLast || isSetBit(pnNxt.uFlags, STPathElement::typeAccount);
|
||
|
||
const uint160& uCurAccountID = pnCur.uAccountID;
|
||
const uint160& uPrvAccountID = bPrvAccount ? pnPrv.uAccountID : uCurAccountID;
|
||
const uint160& uNxtAccountID = bNxtAccount ? pnNxt.uAccountID : uCurAccountID; // Offers are always issue.
|
||
|
||
const uint160& uCurrencyID = pnCur.uCurrencyID;
|
||
|
||
// XXX Don't look up quality for XRP
|
||
const uint32 uQualityIn = uNode ? lesActive.rippleQualityIn(uCurAccountID, uPrvAccountID, uCurrencyID) : QUALITY_ONE;
|
||
const uint32 uQualityOut = uNode != uLast ? lesActive.rippleQualityOut(uCurAccountID, uNxtAccountID, uCurrencyID) : QUALITY_ONE;
|
||
|
||
// For bPrvAccount
|
||
const STAmount saPrvOwed = bPrvAccount && uNode // Previous account is owed.
|
||
? lesActive.rippleOwed(uCurAccountID, uPrvAccountID, uCurrencyID)
|
||
: STAmount(uCurrencyID, uCurAccountID);
|
||
|
||
const STAmount saPrvLimit = bPrvAccount && uNode // Previous account may owe.
|
||
? lesActive.rippleLimit(uCurAccountID, uPrvAccountID, uCurrencyID)
|
||
: STAmount(uCurrencyID, uCurAccountID);
|
||
|
||
const STAmount saNxtOwed = bNxtAccount && uNode != uLast // Next account is owed.
|
||
? lesActive.rippleOwed(uCurAccountID, uNxtAccountID, uCurrencyID)
|
||
: STAmount(uCurrencyID, uCurAccountID);
|
||
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeAccountRev> uNode=%d/%d uPrvAccountID=%s uCurAccountID=%s uNxtAccountID=%s uCurrencyID=%s uQualityIn=%d uQualityOut=%d saPrvOwed=%s saPrvLimit=%s")
|
||
% uNode
|
||
% uLast
|
||
% RippleAddress::createHumanAccountID(uPrvAccountID)
|
||
% RippleAddress::createHumanAccountID(uCurAccountID)
|
||
% RippleAddress::createHumanAccountID(uNxtAccountID)
|
||
% STAmount::createHumanCurrency(uCurrencyID)
|
||
% uQualityIn
|
||
% uQualityOut
|
||
% saPrvOwed.getFullText()
|
||
% saPrvLimit.getFullText());
|
||
|
||
// Previous can redeem the owed IOUs it holds.
|
||
const STAmount saPrvRedeemReq = saPrvOwed.isPositive() ? saPrvOwed : STAmount(uCurrencyID, 0);
|
||
STAmount& saPrvRedeemAct = pnPrv.saRevRedeem;
|
||
|
||
// Previous can issue up to limit minus whatever portion of limit already used (not including redeemable amount).
|
||
const STAmount saPrvIssueReq = saPrvOwed.isNegative() ? saPrvLimit+saPrvOwed : saPrvLimit;
|
||
STAmount& saPrvIssueAct = pnPrv.saRevIssue;
|
||
|
||
// For !bPrvAccount
|
||
const STAmount saPrvDeliverReq = STAmount(uCurrencyID, uCurAccountID, -1); // Unlimited.
|
||
STAmount& saPrvDeliverAct = pnPrv.saRevDeliver;
|
||
|
||
// For bNxtAccount
|
||
const STAmount& saCurRedeemReq = pnCur.saRevRedeem;
|
||
STAmount saCurRedeemAct(saCurRedeemReq.getCurrency(), saCurRedeemReq.getIssuer());
|
||
|
||
const STAmount& saCurIssueReq = pnCur.saRevIssue;
|
||
STAmount saCurIssueAct(saCurIssueReq.getCurrency(), saCurIssueReq.getIssuer()); // Track progress.
|
||
|
||
// For !bNxtAccount
|
||
const STAmount& saCurDeliverReq = pnCur.saRevDeliver;
|
||
STAmount saCurDeliverAct(saCurDeliverReq.getCurrency(), saCurDeliverReq.getIssuer());
|
||
|
||
cLog(lsTRACE) << boost::str(boost::format("calcNodeAccountRev: saPrvRedeemReq=%s saPrvIssueReq=%s saCurRedeemReq=%s saCurIssueReq=%s saNxtOwed=%s")
|
||
% saPrvRedeemReq.getFullText()
|
||
% saPrvIssueReq.getFullText()
|
||
% saCurRedeemReq.getFullText()
|
||
% saCurIssueReq.getFullText()
|
||
% saNxtOwed.getFullText());
|
||
|
||
cLog(lsTRACE) << psCur.getJson();
|
||
|
||
assert(!saCurRedeemReq || (-saNxtOwed) >= saCurRedeemReq); // Current redeem req can't be more than IOUs on hand.
|
||
assert(!saCurIssueReq // If not issuing, fine.
|
||
|| !saNxtOwed.isNegative() // saNxtOwed >= 0: Sender not holding next IOUs, saNxtOwed < 0: Sender holding next IOUs.
|
||
|| -saNxtOwed == saCurRedeemReq); // If issue req, then redeem req must consume all owed.
|
||
|
||
if (!uNode)
|
||
{
|
||
// ^ --> ACCOUNT --> account|offer
|
||
// Nothing to do, there is no previous to adjust.
|
||
|
||
nothing();
|
||
}
|
||
else if (bPrvAccount && bNxtAccount)
|
||
{
|
||
if (uNode == uLast)
|
||
{
|
||
// account --> ACCOUNT --> $
|
||
// Overall deliverable.
|
||
const STAmount& saCurWantedReq = bPrvAccount
|
||
? std::min(psCur.saOutReq-psCur.saOutAct, saPrvLimit+saPrvOwed) // If previous is an account, limit.
|
||
: psCur.saOutReq-psCur.saOutAct; // Previous is an offer, no limit: redeem own IOUs.
|
||
STAmount saCurWantedAct(saCurWantedReq.getCurrency(), saCurWantedReq.getIssuer());
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: account --> ACCOUNT --> $ : saCurWantedReq=%s")
|
||
% saCurWantedReq.getFullText());
|
||
|
||
// Calculate redeem
|
||
if (saPrvRedeemReq) // Previous has IOUs to redeem.
|
||
{
|
||
// Redeem at 1:1
|
||
|
||
saCurWantedAct = std::min(saPrvRedeemReq, saCurWantedReq);
|
||
saPrvRedeemAct = saCurWantedAct;
|
||
|
||
uRateMax = STAmount::uRateOne;
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: Redeem at 1:1 saPrvRedeemReq=%s (available) saPrvRedeemAct=%s uRateMax=%s")
|
||
% saPrvRedeemReq.getFullText()
|
||
% saPrvRedeemAct.getFullText()
|
||
% STAmount::saFromRate(uRateMax).getText());
|
||
}
|
||
else
|
||
{
|
||
saPrvRedeemAct.zero(saCurWantedAct);
|
||
}
|
||
|
||
// Calculate issuing.
|
||
saPrvIssueAct.zero(saCurWantedAct);
|
||
|
||
if (saCurWantedReq != saCurWantedAct // Need more.
|
||
&& saPrvIssueReq) // Will accept IOUs from prevous.
|
||
{
|
||
// Rate: quality in : 1.0
|
||
|
||
// If we previously redeemed and this has a poorer rate, this won't be included the current increment.
|
||
calcNodeRipple(uQualityIn, QUALITY_ONE, saPrvIssueReq, saCurWantedReq, saPrvIssueAct, saCurWantedAct, uRateMax);
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: Issuing: Rate: quality in : 1.0 saPrvIssueAct=%s saCurWantedAct=%s")
|
||
% saPrvIssueAct.getFullText()
|
||
% saCurWantedAct.getFullText());
|
||
}
|
||
|
||
if (!saCurWantedAct)
|
||
{
|
||
// Must have processed something.
|
||
terResult = tecPATH_DRY;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// ^|account --> ACCOUNT --> account
|
||
saPrvRedeemAct.zero(saCurRedeemReq);
|
||
saPrvIssueAct.zero(saCurRedeemReq);
|
||
|
||
// redeem (part 1) -> redeem
|
||
if (saCurRedeemReq // Next wants IOUs redeemed.
|
||
&& saPrvRedeemReq) // Previous has IOUs to redeem.
|
||
{
|
||
// Rate : 1.0 : quality out
|
||
calcNodeRipple(QUALITY_ONE, uQualityOut, saPrvRedeemReq, saCurRedeemReq, saPrvRedeemAct, saCurRedeemAct, uRateMax);
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: Rate : 1.0 : quality out saPrvRedeemAct=%s saCurRedeemAct=%s")
|
||
% saPrvRedeemAct.getFullText()
|
||
% saCurRedeemAct.getFullText());
|
||
}
|
||
|
||
// issue (part 1) -> redeem
|
||
if (saCurRedeemReq != saCurRedeemAct // Next wants more IOUs redeemed.
|
||
&& saPrvRedeemAct == saPrvRedeemReq) // Previous has no IOUs to redeem remaining.
|
||
{
|
||
// Rate: quality in : quality out
|
||
calcNodeRipple(uQualityIn, uQualityOut, saPrvIssueReq, saCurRedeemReq, saPrvIssueAct, saCurRedeemAct, uRateMax);
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: Rate: quality in : quality out: saPrvIssueAct=%s saCurRedeemAct=%s")
|
||
% saPrvIssueAct.getFullText()
|
||
% saCurRedeemAct.getFullText());
|
||
}
|
||
|
||
// redeem (part 2) -> issue.
|
||
if (saCurIssueReq // Next wants IOUs issued.
|
||
&& saCurRedeemAct == saCurRedeemReq // Can only issue if completed redeeming.
|
||
&& saPrvRedeemAct != saPrvRedeemReq) // Did not complete redeeming previous IOUs.
|
||
{
|
||
// Rate : 1.0 : transfer_rate
|
||
calcNodeRipple(QUALITY_ONE, lesActive.rippleTransferRate(uCurAccountID), saPrvRedeemReq, saCurIssueReq, saPrvRedeemAct, saCurIssueAct, uRateMax);
|
||
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: Rate : 1.0 : transfer_rate: saPrvRedeemAct=%s saCurIssueAct=%s")
|
||
% saPrvRedeemAct.getFullText()
|
||
% saCurIssueAct.getFullText());
|
||
}
|
||
|
||
// issue (part 2) -> issue
|
||
if (saCurIssueReq != saCurIssueAct // Need wants more IOUs issued.
|
||
&& saCurRedeemAct == saCurRedeemReq // Can only issue if completed redeeming.
|
||
&& saPrvRedeemReq == saPrvRedeemAct // Previously redeemed all owed IOUs.
|
||
&& saPrvIssueReq) // Previous can issue.
|
||
{
|
||
// Rate: quality in : 1.0
|
||
calcNodeRipple(uQualityIn, QUALITY_ONE, saPrvIssueReq, saCurIssueReq, saPrvIssueAct, saCurIssueAct, uRateMax);
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: Rate: quality in : 1.0: saPrvIssueAct=%s saCurIssueAct=%s")
|
||
% saPrvIssueAct.getFullText()
|
||
% saCurIssueAct.getFullText());
|
||
}
|
||
|
||
if (!saCurRedeemAct && !saCurIssueAct)
|
||
{
|
||
// Did not make progress.
|
||
terResult = tecPATH_DRY;
|
||
}
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: ^|account --> ACCOUNT --> account : saCurRedeemReq=%s saCurIssueReq=%s saPrvOwed=%s saCurRedeemAct=%s saCurIssueAct=%s")
|
||
% saCurRedeemReq.getFullText()
|
||
% saCurIssueReq.getFullText()
|
||
% saPrvOwed.getFullText()
|
||
% saCurRedeemAct.getFullText()
|
||
% saCurIssueAct.getFullText());
|
||
}
|
||
}
|
||
else if (bPrvAccount && !bNxtAccount)
|
||
{
|
||
// account --> ACCOUNT --> offer
|
||
// Note: deliver is always issue as ACCOUNT is the issuer for the offer input.
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: account --> ACCOUNT --> offer"));
|
||
|
||
saPrvRedeemAct.zero(saCurRedeemReq);
|
||
saPrvIssueAct.zero(saCurRedeemReq);
|
||
|
||
// redeem -> deliver/issue.
|
||
if (saPrvOwed.isPositive() // Previous has IOUs to redeem.
|
||
&& saCurDeliverReq) // Need some issued.
|
||
{
|
||
// Rate : 1.0 : transfer_rate
|
||
calcNodeRipple(QUALITY_ONE, lesActive.rippleTransferRate(uCurAccountID), saPrvRedeemReq, saCurDeliverReq, saPrvRedeemAct, saCurDeliverAct, uRateMax);
|
||
}
|
||
|
||
// issue -> deliver/issue
|
||
if (saPrvRedeemReq == saPrvRedeemAct // Previously redeemed all owed.
|
||
&& saCurDeliverReq != saCurDeliverAct) // Still need some issued.
|
||
{
|
||
// Rate: quality in : 1.0
|
||
calcNodeRipple(uQualityIn, QUALITY_ONE, saPrvIssueReq, saCurDeliverReq, saPrvIssueAct, saCurDeliverAct, uRateMax);
|
||
}
|
||
|
||
if (!saCurDeliverAct)
|
||
{
|
||
// Must want something.
|
||
terResult = tecPATH_DRY;
|
||
}
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: saCurDeliverReq=%s saCurDeliverAct=%s saPrvOwed=%s")
|
||
% saCurDeliverReq.getFullText()
|
||
% saCurDeliverAct.getFullText()
|
||
% saPrvOwed.getFullText());
|
||
}
|
||
else if (!bPrvAccount && bNxtAccount)
|
||
{
|
||
saPrvDeliverAct.zero(saCurRedeemReq);
|
||
|
||
if (uNode == uLast)
|
||
{
|
||
// offer --> ACCOUNT --> $
|
||
const STAmount& saCurWantedReq = bPrvAccount
|
||
? std::min(psCur.saOutReq-psCur.saOutAct, saPrvLimit+saPrvOwed) // If previous is an account, limit.
|
||
: psCur.saOutReq-psCur.saOutAct; // Previous is an offer, no limit: redeem own IOUs.
|
||
STAmount saCurWantedAct(saCurWantedReq.getCurrency(), saCurWantedReq.getIssuer());
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: offer --> ACCOUNT --> $ : saCurWantedReq=%s")
|
||
% saCurWantedReq.getFullText());
|
||
|
||
// Rate: quality in : 1.0
|
||
calcNodeRipple(uQualityIn, QUALITY_ONE, saPrvDeliverReq, saCurWantedReq, saPrvDeliverAct, saCurWantedAct, uRateMax);
|
||
|
||
if (!saCurWantedAct)
|
||
{
|
||
// Must have processed something.
|
||
terResult = tecPATH_DRY;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// offer --> ACCOUNT --> account
|
||
// Note: offer is always delivering(redeeming) as account is issuer.
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: offer --> ACCOUNT --> account"));
|
||
|
||
// deliver -> redeem
|
||
if (saCurRedeemReq) // Next wants us to redeem.
|
||
{
|
||
// Rate : 1.0 : quality out
|
||
calcNodeRipple(QUALITY_ONE, uQualityOut, saPrvDeliverReq, saCurRedeemReq, saPrvDeliverAct, saCurRedeemAct, uRateMax);
|
||
}
|
||
|
||
// deliver -> issue.
|
||
if (saCurRedeemReq == saCurRedeemAct // Can only issue if previously redeemed all.
|
||
&& saCurIssueReq) // Need some issued.
|
||
{
|
||
// Rate : 1.0 : transfer_rate
|
||
calcNodeRipple(QUALITY_ONE, lesActive.rippleTransferRate(uCurAccountID), saPrvDeliverReq, saCurIssueReq, saPrvDeliverAct, saCurIssueAct, uRateMax);
|
||
}
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: saCurRedeemReq=%s saCurIssueAct=%s saCurIssueReq=%s saPrvDeliverAct=%s")
|
||
% saCurRedeemReq.getFullText()
|
||
% saCurRedeemAct.getFullText()
|
||
% saCurIssueReq.getFullText()
|
||
% saPrvDeliverAct.getFullText());
|
||
|
||
if (!saPrvDeliverAct)
|
||
{
|
||
// Must want something.
|
||
terResult = tecPATH_DRY;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// offer --> ACCOUNT --> offer
|
||
// deliver/redeem -> deliver/issue.
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: offer --> ACCOUNT --> offer"));
|
||
|
||
saPrvDeliverAct.zero(saCurRedeemReq);
|
||
|
||
// Rate : 1.0 : transfer_rate
|
||
calcNodeRipple(QUALITY_ONE, lesActive.rippleTransferRate(uCurAccountID), saPrvDeliverReq, saCurDeliverReq, saPrvDeliverAct, saCurDeliverAct, uRateMax);
|
||
|
||
if (!saCurDeliverAct)
|
||
{
|
||
// Must want something.
|
||
terResult = tecPATH_DRY;
|
||
}
|
||
}
|
||
|
||
return terResult;
|
||
}
|
||
|
||
// The reverse pass has been narrowing by credit available and inflating by fees as it worked backwards.
|
||
// Now, for the current account node, take the actual amount from previous and adjust forward balances.
|
||
//
|
||
// Perform balance adjustments between previous and current node.
|
||
// - The previous node: specifies what to push through to current.
|
||
// - All of previous output is consumed.
|
||
// Then, compute current node's output for next node.
|
||
// - Current node: specify what to push through to next.
|
||
// - Output to next node is computed as input minus quality or transfer fee.
|
||
// - If next node is an offer and output is non-XRP then we are the issuer and do not need to push funds.
|
||
// - If next node is an offer and output is XRP then we need to deliver funds to limbo.
|
||
TER RippleCalc::calcNodeAccountFwd(
|
||
const unsigned int uNode, // 0 <= uNode <= uLast
|
||
PathState& psCur,
|
||
const bool bMultiQuality)
|
||
{
|
||
TER terResult = tesSUCCESS;
|
||
const unsigned int uLast = psCur.vpnNodes.size() - 1;
|
||
|
||
uint64 uRateMax = 0;
|
||
|
||
PaymentNode& pnPrv = psCur.vpnNodes[uNode ? uNode-1 : 0];
|
||
PaymentNode& pnCur = psCur.vpnNodes[uNode];
|
||
PaymentNode& pnNxt = psCur.vpnNodes[uNode == uLast ? uLast : uNode+1];
|
||
|
||
const bool bPrvAccount = isSetBit(pnPrv.uFlags, STPathElement::typeAccount);
|
||
const bool bNxtAccount = isSetBit(pnNxt.uFlags, STPathElement::typeAccount);
|
||
|
||
const uint160& uCurAccountID = pnCur.uAccountID;
|
||
const uint160& uPrvAccountID = bPrvAccount ? pnPrv.uAccountID : uCurAccountID;
|
||
const uint160& uNxtAccountID = bNxtAccount ? pnNxt.uAccountID : uCurAccountID; // Offers are always issue.
|
||
|
||
// const uint160& uCurIssuerID = pnCur.uIssuerID;
|
||
|
||
const uint160& uCurrencyID = pnCur.uCurrencyID;
|
||
|
||
uint32 uQualityIn = uNode ? lesActive.rippleQualityIn(uCurAccountID, uPrvAccountID, uCurrencyID) : QUALITY_ONE;
|
||
uint32 uQualityOut = uNode == uLast ? lesActive.rippleQualityOut(uCurAccountID, uNxtAccountID, uCurrencyID) : QUALITY_ONE;
|
||
|
||
// When looking backward (prv) for req we care about what we just calculated: use fwd
|
||
// When looking forward (cur) for req we care about what was desired: use rev
|
||
|
||
// For bNxtAccount
|
||
const STAmount& saPrvRedeemReq = pnPrv.saFwdRedeem;
|
||
STAmount saPrvRedeemAct(saPrvRedeemReq.getCurrency(), saPrvRedeemReq.getIssuer());
|
||
|
||
const STAmount& saPrvIssueReq = pnPrv.saFwdIssue;
|
||
STAmount saPrvIssueAct(saPrvIssueReq.getCurrency(), saPrvIssueReq.getIssuer());
|
||
|
||
// For !bPrvAccount
|
||
const STAmount& saPrvDeliverReq = pnPrv.saFwdDeliver;
|
||
STAmount saPrvDeliverAct(saPrvDeliverReq.getCurrency(), saPrvDeliverReq.getIssuer());
|
||
|
||
// For bNxtAccount
|
||
const STAmount& saCurRedeemReq = pnCur.saRevRedeem;
|
||
STAmount& saCurRedeemAct = pnCur.saFwdRedeem;
|
||
|
||
const STAmount& saCurIssueReq = pnCur.saRevIssue;
|
||
STAmount& saCurIssueAct = pnCur.saFwdIssue;
|
||
|
||
// For !bNxtAccount
|
||
const STAmount& saCurDeliverReq = pnCur.saRevDeliver;
|
||
STAmount& saCurDeliverAct = pnCur.saFwdDeliver;
|
||
|
||
// For !uNode
|
||
STAmount& saCurSendMaxPass = psCur.saInPass; // Report how much pass sends.
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd> uNode=%d/%d saPrvRedeemReq=%s saPrvIssueReq=%s saPrvDeliverReq=%s saCurRedeemReq=%s saCurIssueReq=%s saCurDeliverReq=%s")
|
||
% uNode
|
||
% uLast
|
||
% saPrvRedeemReq.getFullText()
|
||
% saPrvIssueReq.getFullText()
|
||
% saPrvDeliverReq.getFullText()
|
||
% saCurRedeemReq.getFullText()
|
||
% saCurIssueReq.getFullText()
|
||
% saCurDeliverReq.getFullText());
|
||
|
||
// Ripple through account.
|
||
|
||
if (bPrvAccount && bNxtAccount)
|
||
{
|
||
// Next is an account, must be rippling.
|
||
|
||
if (!uNode)
|
||
{
|
||
// ^ --> ACCOUNT --> account
|
||
|
||
// First node, calculate amount to ripple based on what is available.
|
||
|
||
saCurRedeemAct = saCurRedeemReq;
|
||
|
||
if (!psCur.saInReq.isNegative())
|
||
{
|
||
// Limit by send max.
|
||
saCurRedeemAct = std::min(saCurRedeemAct, psCur.saInReq-psCur.saInAct);
|
||
}
|
||
|
||
saCurSendMaxPass = saCurRedeemAct;
|
||
|
||
saCurIssueAct = saCurRedeemAct == saCurRedeemReq // Fully redeemed.
|
||
? saCurIssueReq
|
||
: STAmount(saCurIssueReq);
|
||
|
||
if (!!saCurIssueAct && !psCur.saInReq.isNegative())
|
||
{
|
||
// Limit by send max.
|
||
saCurIssueAct = std::min(saCurIssueAct, psCur.saInReq-psCur.saInAct-saCurRedeemAct);
|
||
}
|
||
|
||
saCurSendMaxPass += saCurIssueAct;
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: ^ --> ACCOUNT --> account : saInReq=%s saInAct=%s saCurRedeemAct=%s saCurIssueReq=%s saCurIssueAct=%s saCurSendMaxPass=%s")
|
||
% psCur.saInReq.getFullText()
|
||
% psCur.saInAct.getFullText()
|
||
% saCurRedeemAct.getFullText()
|
||
% saCurIssueReq.getFullText()
|
||
% saCurIssueAct.getFullText()
|
||
% saCurSendMaxPass.getFullText());
|
||
}
|
||
else if (uNode == uLast)
|
||
{
|
||
// account --> ACCOUNT --> $
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: account --> ACCOUNT --> $ : uPrvAccountID=%s uCurAccountID=%s saPrvRedeemReq=%s saPrvIssueReq=%s")
|
||
% RippleAddress::createHumanAccountID(uPrvAccountID)
|
||
% RippleAddress::createHumanAccountID(uCurAccountID)
|
||
% saPrvRedeemReq.getFullText()
|
||
% saPrvIssueReq.getFullText());
|
||
|
||
// Last node. Accept all funds. Calculate amount actually to credit.
|
||
|
||
STAmount& saCurReceive = psCur.saOutPass;
|
||
|
||
STAmount saIssueCrd = uQualityIn >= QUALITY_ONE
|
||
? saPrvIssueReq // No fee.
|
||
: STAmount::multiply(saPrvIssueReq, uQualityIn, uCurrencyID, saPrvIssueReq.getIssuer()); // Fee.
|
||
|
||
// Amount to credit.
|
||
saCurReceive = saPrvRedeemReq+saIssueCrd;
|
||
|
||
// Actually receive.
|
||
terResult = lesActive.rippleCredit(uPrvAccountID, uCurAccountID, saPrvRedeemReq+saPrvIssueReq, false);
|
||
}
|
||
else
|
||
{
|
||
// account --> ACCOUNT --> account
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: account --> ACCOUNT --> account"));
|
||
|
||
saCurRedeemAct.zero(saCurRedeemReq);
|
||
saCurIssueAct.zero(saCurIssueReq);
|
||
|
||
// Previous redeem part 1: redeem -> redeem
|
||
if (saPrvRedeemReq && saCurRedeemReq) // Previous wants to redeem.
|
||
{
|
||
// Rate : 1.0 : quality out
|
||
calcNodeRipple(QUALITY_ONE, uQualityOut, saPrvRedeemReq, saCurRedeemReq, saPrvRedeemAct, saCurRedeemAct, uRateMax);
|
||
}
|
||
|
||
// Previous issue part 1: issue -> redeem
|
||
if (saPrvIssueReq != saPrvIssueAct // Previous wants to issue.
|
||
&& saCurRedeemReq != saCurRedeemAct) // Current has more to redeem to next.
|
||
{
|
||
// Rate: quality in : quality out
|
||
calcNodeRipple(uQualityIn, uQualityOut, saPrvIssueReq, saCurRedeemReq, saPrvIssueAct, saCurRedeemAct, uRateMax);
|
||
}
|
||
|
||
// Previous redeem part 2: redeem -> issue.
|
||
if (saPrvRedeemReq != saPrvRedeemAct // Previous still wants to redeem.
|
||
&& saCurRedeemReq == saCurRedeemAct // Current redeeming is done can issue.
|
||
&& saCurIssueReq) // Current wants to issue.
|
||
{
|
||
// Rate : 1.0 : transfer_rate
|
||
calcNodeRipple(QUALITY_ONE, lesActive.rippleTransferRate(uCurAccountID), saPrvRedeemReq, saCurIssueReq, saPrvRedeemAct, saCurIssueAct, uRateMax);
|
||
}
|
||
|
||
// Previous issue part 2 : issue -> issue
|
||
if (saPrvIssueReq != saPrvIssueAct // Previous wants to issue.
|
||
&& saCurRedeemReq == saCurRedeemAct) // Current redeeming is done can issue.
|
||
{
|
||
// Rate: quality in : 1.0
|
||
calcNodeRipple(uQualityIn, QUALITY_ONE, saPrvIssueReq, saCurIssueReq, saPrvIssueAct, saCurIssueAct, uRateMax);
|
||
}
|
||
|
||
// Adjust prv --> cur balance : take all inbound
|
||
terResult = lesActive.rippleCredit(uPrvAccountID, uCurAccountID, saPrvRedeemReq + saPrvIssueReq, false);
|
||
}
|
||
}
|
||
else if (bPrvAccount && !bNxtAccount)
|
||
{
|
||
// Current account is issuer to next offer.
|
||
// Determine deliver to offer amount.
|
||
// Don't adjust outbound balances- keep funds with issuer as limbo.
|
||
// If issuer hold's an offer owners inbound IOUs, there is no fee and redeem/issue will transparently happen.
|
||
|
||
if (uNode)
|
||
{
|
||
// Non-XRP, current node is the issuer.
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: account --> ACCOUNT --> offer"));
|
||
|
||
saCurDeliverAct.zero(saCurDeliverReq);
|
||
|
||
// redeem -> issue/deliver.
|
||
// Previous wants to redeem.
|
||
// Current is issuing to an offer so leave funds in account as "limbo".
|
||
if (saPrvRedeemReq) // Previous wants to redeem.
|
||
{
|
||
// Rate : 1.0 : transfer_rate
|
||
// XXX Is having the transfer rate here correct?
|
||
calcNodeRipple(QUALITY_ONE, lesActive.rippleTransferRate(uCurAccountID), saPrvRedeemReq, saCurDeliverReq, saPrvRedeemAct, saCurDeliverAct, uRateMax);
|
||
}
|
||
|
||
// issue -> issue/deliver
|
||
if (saPrvRedeemReq == saPrvRedeemAct // Previous done redeeming: Previous has no IOUs.
|
||
&& saPrvIssueReq) // Previous wants to issue. To next must be ok.
|
||
{
|
||
// Rate: quality in : 1.0
|
||
calcNodeRipple(uQualityIn, QUALITY_ONE, saPrvIssueReq, saCurDeliverReq, saPrvIssueAct, saCurDeliverAct, uRateMax);
|
||
}
|
||
|
||
// Adjust prv --> cur balance : take all inbound
|
||
terResult = lesActive.rippleCredit(uPrvAccountID, uCurAccountID, saPrvRedeemReq + saPrvIssueReq, false);
|
||
}
|
||
else
|
||
{
|
||
// Delivering amount requested from downstream.
|
||
saCurDeliverAct = saCurDeliverReq;
|
||
|
||
// If limited, then limit by send max and available.
|
||
if (!psCur.saInReq.isNegative())
|
||
{
|
||
// Limit by send max.
|
||
saCurDeliverAct = std::min(saCurDeliverAct, psCur.saInReq-psCur.saInAct);
|
||
|
||
// Limit XRP by available. No limit for non-XRP as issuer.
|
||
if (!uCurAccountID)
|
||
saCurDeliverAct = std::min(saCurDeliverAct, lesActive.accountHolds(uCurAccountID, CURRENCY_XRP, ACCOUNT_XRP));
|
||
|
||
}
|
||
saCurSendMaxPass = saCurDeliverAct; // Record amount sent for pass.
|
||
|
||
if (!!uCurrencyID)
|
||
{
|
||
// Non-XRP, current node is the issuer.
|
||
// We could be delivering to multiple accounts, so we don't know which ripple balance will be adjusted. Assume
|
||
// just issuing.
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: ^ --> ACCOUNT -- !XRP --> offer"));
|
||
|
||
// As the issuer, would only issue.
|
||
// Don't need to actually deliver. As from delivering leave in the issuer as limbo.
|
||
nothing();
|
||
}
|
||
else
|
||
{
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: ^ --> ACCOUNT -- XRP --> offer"));
|
||
|
||
// Deliver XRP to limbo.
|
||
terResult = lesActive.accountSend(uCurAccountID, ACCOUNT_XRP, saCurDeliverAct);
|
||
}
|
||
}
|
||
}
|
||
else if (!bPrvAccount && bNxtAccount)
|
||
{
|
||
if (uNode == uLast)
|
||
{
|
||
// offer --> ACCOUNT --> $
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: offer --> ACCOUNT --> $ : %s") % saPrvDeliverReq.getFullText());
|
||
|
||
STAmount& saCurReceive = psCur.saOutPass;
|
||
|
||
// Amount to credit.
|
||
saCurReceive = saPrvDeliverReq;
|
||
|
||
// No income balance adjustments necessary. The paying side inside the offer paid to this account.
|
||
}
|
||
else
|
||
{
|
||
// offer --> ACCOUNT --> account
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: offer --> ACCOUNT --> account"));
|
||
|
||
saCurRedeemAct.zero(saCurRedeemReq);
|
||
saCurIssueAct.zero(saCurIssueReq);
|
||
|
||
// deliver -> redeem
|
||
if (saPrvDeliverReq && saCurRedeemReq) // Previous wants to deliver and can current redeem.
|
||
{
|
||
// Rate : 1.0 : quality out
|
||
calcNodeRipple(QUALITY_ONE, uQualityOut, saPrvDeliverReq, saCurRedeemReq, saPrvDeliverAct, saCurRedeemAct, uRateMax);
|
||
}
|
||
|
||
// deliver -> issue
|
||
// Wants to redeem and current would and can issue.
|
||
if (saPrvDeliverReq != saPrvDeliverAct // Previous still wants to deliver.
|
||
&& saCurRedeemReq == saCurRedeemAct // Current has more to redeem to next.
|
||
&& saCurIssueReq) // Current wants issue.
|
||
{
|
||
// Rate : 1.0 : transfer_rate
|
||
calcNodeRipple(QUALITY_ONE, lesActive.rippleTransferRate(uCurAccountID), saPrvDeliverReq, saCurIssueReq, saPrvDeliverAct, saCurIssueAct, uRateMax);
|
||
}
|
||
|
||
// No income balance adjustments necessary. The paying side inside the offer paid and the next link will receive.
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// offer --> ACCOUNT --> offer
|
||
// deliver/redeem -> deliver/issue.
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: offer --> ACCOUNT --> offer"));
|
||
|
||
saCurDeliverAct.zero(saCurDeliverReq);
|
||
|
||
if (saPrvDeliverReq // Previous wants to deliver
|
||
&& saCurIssueReq) // Current wants issue.
|
||
{
|
||
// Rate : 1.0 : transfer_rate
|
||
calcNodeRipple(QUALITY_ONE, lesActive.rippleTransferRate(uCurAccountID), saPrvDeliverReq, saCurDeliverReq, saPrvDeliverAct, saCurDeliverAct, uRateMax);
|
||
}
|
||
|
||
// No income balance adjustments necessary. The paying side inside the offer paid and the next link will receive.
|
||
}
|
||
|
||
return terResult;
|
||
}
|
||
|
||
TER RippleCalc::calcNodeFwd(const unsigned int uNode, PathState& psCur, const bool bMultiQuality)
|
||
{
|
||
const PaymentNode& pnCur = psCur.vpnNodes[uNode];
|
||
const bool bCurAccount = isSetBit(pnCur.uFlags, STPathElement::typeAccount);
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeFwd> uNode=%d") % uNode);
|
||
|
||
TER terResult = bCurAccount
|
||
? calcNodeAccountFwd(uNode, psCur, bMultiQuality)
|
||
: calcNodeOfferFwd(uNode, psCur, bMultiQuality);
|
||
|
||
if (tesSUCCESS == terResult && uNode + 1 != psCur.vpnNodes.size())
|
||
{
|
||
terResult = calcNodeFwd(uNode+1, psCur, bMultiQuality);
|
||
}
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeFwd< uNode=%d terResult=%d") % uNode % terResult);
|
||
|
||
return terResult;
|
||
}
|
||
|
||
// Calculate a node and its previous nodes.
|
||
// From the destination work in reverse towards the source calculating how much must be asked for.
|
||
// Then work forward, figuring out how much can actually be delivered.
|
||
// <-- terResult: tesSUCCESS or tecPATH_DRY
|
||
// <-> pnNodes:
|
||
// --> [end]saWanted.mAmount
|
||
// --> [all]saWanted.mCurrency
|
||
// --> [all]saAccount
|
||
// <-> [0]saWanted.mAmount : --> limit, <-- actual
|
||
TER RippleCalc::calcNodeRev(const unsigned int uNode, PathState& psCur, const bool bMultiQuality)
|
||
{
|
||
PaymentNode& pnCur = psCur.vpnNodes[uNode];
|
||
const bool bCurAccount = isSetBit(pnCur.uFlags, STPathElement::typeAccount);
|
||
TER terResult;
|
||
|
||
// Do current node reverse.
|
||
const uint160& uCurIssuerID = pnCur.uIssuerID;
|
||
STAmount& saTransferRate = pnCur.saTransferRate;
|
||
|
||
saTransferRate = STAmount::saFromRate(lesActive.rippleTransferRate(uCurIssuerID));
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("calcNodeRev> uNode=%d uIssuerID=%s saTransferRate=%s")
|
||
% uNode
|
||
% RippleAddress::createHumanAccountID(uCurIssuerID)
|
||
% saTransferRate.getFullText());
|
||
|
||
terResult = bCurAccount
|
||
? calcNodeAccountRev(uNode, psCur, bMultiQuality)
|
||
: calcNodeOfferRev(uNode, psCur, bMultiQuality);
|
||
|
||
// Do previous.
|
||
if (tesSUCCESS != terResult)
|
||
{
|
||
// Error, don't continue.
|
||
nothing();
|
||
}
|
||
else if (uNode)
|
||
{
|
||
// Continue in reverse.
|
||
|
||
terResult = calcNodeRev(uNode-1, psCur, bMultiQuality);
|
||
}
|
||
|
||
cLog(lsINFO) << boost::str(boost::format("calcNodeRev< uNode=%d terResult=%s/%d") % uNode % transToken(terResult) % terResult);
|
||
|
||
return terResult;
|
||
}
|
||
|
||
// Calculate the next increment of a path.
|
||
// The increment is what can satisfy a portion or all of the requested output at the best quality.
|
||
// <-- psCur.uQuality
|
||
void RippleCalc::pathNext(PathState::ref psrCur, const bool bMultiQuality, const LedgerEntrySet& lesCheckpoint, LedgerEntrySet& lesCurrent)
|
||
{
|
||
// The next state is what is available in preference order.
|
||
// This is calculated when referenced accounts changed.
|
||
const unsigned int uLast = psrCur->vpnNodes.size() - 1;
|
||
|
||
psrCur->bConsumed = false;
|
||
|
||
// YYY This clearing should only be needed for nice logging.
|
||
psrCur->saInPass = STAmount(psrCur->saInReq.getCurrency(), psrCur->saInReq.getIssuer());
|
||
psrCur->saOutPass = STAmount(psrCur->saOutReq.getCurrency(), psrCur->saOutReq.getIssuer());
|
||
|
||
psrCur->vUnfundedBecame.clear();
|
||
psrCur->umReverse.clear();
|
||
|
||
cLog(lsDEBUG) << "pathNext: Path In: " << psrCur->getJson();
|
||
|
||
assert(psrCur->vpnNodes.size() >= 2);
|
||
|
||
lesCurrent = lesCheckpoint; // Restore from checkpoint.
|
||
lesCurrent.bumpSeq(); // Begin ledger varance.
|
||
|
||
psrCur->terStatus = calcNodeRev(uLast, *psrCur, bMultiQuality);
|
||
|
||
cLog(lsDEBUG) << "pathNext: Path after reverse: " << psrCur->getJson();
|
||
|
||
if (tesSUCCESS == psrCur->terStatus)
|
||
{
|
||
// Do forward.
|
||
lesCurrent = lesCheckpoint; // Restore from checkpoint.
|
||
lesCurrent.bumpSeq(); // Begin ledger varance.
|
||
|
||
psrCur->terStatus = calcNodeFwd(0, *psrCur, bMultiQuality);
|
||
}
|
||
|
||
if (tesSUCCESS == psrCur->terStatus)
|
||
{
|
||
tLog(!psrCur->saInPass || !psrCur->saOutPass, lsDEBUG)
|
||
<< boost::str(boost::format("pathNext: Error calcNodeFwd reported success for nothing: saOutPass=%s saInPass=%s")
|
||
% psrCur->saOutPass.getFullText()
|
||
% psrCur->saInPass.getFullText());
|
||
|
||
if (!psrCur->saOutPass || !psrCur->saInPass)
|
||
throw std::runtime_error("Made no progress.");
|
||
|
||
psrCur->uQuality = STAmount::getRate(psrCur->saOutPass, psrCur->saInPass); // Calculate relative quality.
|
||
|
||
cLog(lsDEBUG) << "pathNext: Path after forward: " << psrCur->getJson();
|
||
}
|
||
else
|
||
{
|
||
psrCur->uQuality = 0;
|
||
}
|
||
}
|
||
|
||
// <-- TER: Only returns tepPATH_PARTIAL if !bPartialPayment.
|
||
TER RippleCalc::rippleCalc(
|
||
// Compute paths vs this ledger entry set. Up to caller to actually apply to ledger.
|
||
LedgerEntrySet& lesActive, // <-> --> = Fee already applied to src balance.
|
||
STAmount& saMaxAmountAct, // <-- The computed input amount.
|
||
STAmount& saDstAmountAct, // <-- The computed output amount.
|
||
std::vector<PathState::pointer>& vpsExpanded,
|
||
// Issuer:
|
||
// XRP: ACCOUNT_XRP
|
||
// non-XRP: uSrcAccountID (for any issuer) or another account with trust node.
|
||
const STAmount& saMaxAmountReq, // --> -1 = no limit.
|
||
|
||
// Issuer:
|
||
// XRP: ACCOUNT_XRP
|
||
// non-XRP: uDstAccountID (for any issuer) or another account with trust node.
|
||
const STAmount& saDstAmountReq,
|
||
|
||
const uint160& uDstAccountID,
|
||
const uint160& uSrcAccountID,
|
||
const STPathSet& spsPaths,
|
||
const bool bPartialPayment,
|
||
const bool bLimitQuality,
|
||
const bool bNoRippleDirect,
|
||
const bool bStandAlone, // True, not to delete unfundeds.
|
||
const bool bOpenLedger
|
||
)
|
||
{
|
||
RippleCalc rc(lesActive, bOpenLedger);
|
||
|
||
TER terResult = temUNCERTAIN;
|
||
|
||
// YYY Might do basic checks on src and dst validity as per doPayment.
|
||
|
||
if (bNoRippleDirect && spsPaths.isEmpty())
|
||
{
|
||
cLog(lsINFO) << "rippleCalc: Invalid transaction: No paths and direct ripple not allowed.";
|
||
|
||
return temRIPPLE_EMPTY;
|
||
}
|
||
|
||
// Incrementally search paths.
|
||
|
||
// bNoRippleDirect is a slight misnomer, it really means make no ripple default path.
|
||
if (!bNoRippleDirect)
|
||
{
|
||
// Build a default path. Use saDstAmountReq and saMaxAmountReq to imply nodes.
|
||
// XXX Might also make a XRP bridge by default.
|
||
|
||
PathState::pointer pspDirect = boost::make_shared<PathState>(saDstAmountReq, saMaxAmountReq, lesActive.getLedgerRef());
|
||
|
||
if (!pspDirect)
|
||
return temUNKNOWN;
|
||
|
||
pspDirect->setExpanded(lesActive, STPath(), uDstAccountID, uSrcAccountID);
|
||
|
||
pspDirect->setIndex(vpsExpanded.size());
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("rippleCalc: Build direct: status: %s")
|
||
% transToken(pspDirect->terStatus));
|
||
// Return if malformed.
|
||
if (isTemMalformed(pspDirect->terStatus))
|
||
return pspDirect->terStatus;
|
||
|
||
if (tesSUCCESS == pspDirect->terStatus)
|
||
{
|
||
// Had a success.
|
||
terResult = tesSUCCESS;
|
||
|
||
vpsExpanded.push_back(pspDirect);
|
||
}
|
||
else if (terNO_LINE != pspDirect->terStatus)
|
||
{
|
||
terResult = pspDirect->terStatus;
|
||
}
|
||
}
|
||
|
||
cLog(lsTRACE) << "rippleCalc: Paths in set: " << spsPaths.size();
|
||
|
||
int iIndex = 0;
|
||
BOOST_FOREACH(const STPath& spPath, spsPaths)
|
||
{
|
||
PathState::pointer pspExpanded = boost::make_shared<PathState>(saDstAmountReq, saMaxAmountReq, lesActive.getLedgerRef());
|
||
|
||
if (!pspExpanded)
|
||
return temUNKNOWN;
|
||
|
||
pspExpanded->setExpanded(lesActive, spPath, uDstAccountID, uSrcAccountID);
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("rippleCalc: Build path: %d: status: %s")
|
||
% ++iIndex
|
||
% transToken(pspExpanded->terStatus));
|
||
|
||
// Return, if the path specification was malformed.
|
||
if (isTemMalformed(pspExpanded->terStatus))
|
||
return pspExpanded->terStatus;
|
||
|
||
if (tesSUCCESS == pspExpanded->terStatus) {
|
||
terResult = tesSUCCESS; // Had a success.
|
||
|
||
pspExpanded->setIndex(vpsExpanded.size());
|
||
|
||
vpsExpanded.push_back(pspExpanded);
|
||
}
|
||
else if (terNO_LINE != pspExpanded->terStatus)
|
||
{
|
||
terResult = pspExpanded->terStatus;
|
||
}
|
||
}
|
||
|
||
if (tesSUCCESS != terResult)
|
||
{
|
||
return terResult == temUNCERTAIN ? terNO_LINE : terResult;
|
||
}
|
||
else
|
||
{
|
||
terResult = temUNCERTAIN;
|
||
}
|
||
|
||
saMaxAmountAct = STAmount(saMaxAmountReq.getCurrency(), saMaxAmountReq.getIssuer());
|
||
saDstAmountAct = STAmount(saDstAmountReq.getCurrency(), saDstAmountReq.getIssuer());
|
||
|
||
const LedgerEntrySet lesBase = lesActive; // Checkpoint with just fees paid.
|
||
const uint64 uQualityLimit = bLimitQuality ? STAmount::getRate(saDstAmountReq, saMaxAmountReq) : 0;
|
||
// When processing, don't want to complicate directory walking with deletion.
|
||
std::vector<uint256> vuUnfundedBecame; // Offers that became unfunded.
|
||
|
||
int iPass = 0;
|
||
while (temUNCERTAIN == terResult)
|
||
{
|
||
int iBest = -1;
|
||
const LedgerEntrySet lesCheckpoint = lesActive;
|
||
int iDry = 0;
|
||
bool bMultiQuality = false; // True, if ever computed multi-quality.
|
||
|
||
// Find the best path.
|
||
BOOST_FOREACH(PathState::ref pspCur, vpsExpanded)
|
||
{
|
||
if (pspCur->uQuality)
|
||
{
|
||
bMultiQuality = 1 == vpsExpanded.size()-iDry, // Computing the only non-dry path, compute multi-quality.
|
||
pspCur->saInAct = saMaxAmountAct; // Update to current amount processed.
|
||
pspCur->saOutAct = saDstAmountAct;
|
||
|
||
rc.pathNext(pspCur, bMultiQuality, lesCheckpoint, lesActive); // Compute increment.
|
||
cLog(lsDEBUG) << boost::str(boost::format("rippleCalc: AFTER: mIndex=%d uQuality=%d rate=%s")
|
||
% pspCur->mIndex
|
||
% pspCur->uQuality
|
||
% STAmount::saFromRate(pspCur->uQuality));
|
||
|
||
if (!pspCur->uQuality) {
|
||
// Path was dry.
|
||
|
||
++iDry;
|
||
}
|
||
else
|
||
{
|
||
tLog(!pspCur->saInPass || !pspCur->saOutPass, lsDEBUG)
|
||
<< boost::str(boost::format("rippleCalc: better: uQuality=%s saInPass=%s saOutPass=%s")
|
||
% STAmount::saFromRate(pspCur->uQuality)
|
||
% pspCur->saInPass.getFullText()
|
||
% pspCur->saOutPass.getFullText());
|
||
|
||
assert(!!pspCur->saInPass && !!pspCur->saOutPass);
|
||
|
||
if ((!bLimitQuality || pspCur->uQuality <= uQualityLimit) // Quality is not limted or increment has allowed quality.
|
||
&& (iBest < 0 // Best is not yet set.
|
||
|| PathState::lessPriority(*vpsExpanded[iBest], *pspCur))) // Current is better than set.
|
||
{
|
||
cLog(lsDEBUG) << boost::str(boost::format("rippleCalc: better: mIndex=%d uQuality=%s rate=%s saInPass=%s saOutPass=%s")
|
||
% pspCur->mIndex
|
||
% pspCur->uQuality
|
||
% STAmount::saFromRate(pspCur->uQuality)
|
||
% pspCur->saInPass.getFullText()
|
||
% pspCur->saOutPass.getFullText());
|
||
|
||
lesActive.swapWith(pspCur->lesEntries); // For the path, save ledger state.
|
||
iBest = pspCur->getIndex();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (sLog(lsDEBUG))
|
||
{
|
||
cLog(lsDEBUG) << boost::str(boost::format("rippleCalc: Summary: Pass: %d Dry: %d Paths: %d") % ++iPass % iDry % vpsExpanded.size());
|
||
BOOST_FOREACH(PathState::ref pspCur, vpsExpanded)
|
||
{
|
||
cLog(lsDEBUG) << boost::str(boost::format("rippleCalc: Summary: %d rate: %s quality:%d best: %d consumed: %d")
|
||
% pspCur->mIndex
|
||
% STAmount::saFromRate(pspCur->uQuality)
|
||
% pspCur->uQuality
|
||
% (iBest == pspCur->getIndex())
|
||
% pspCur->bConsumed);
|
||
}
|
||
}
|
||
|
||
if (iBest >= 0)
|
||
{
|
||
// Apply best path.
|
||
PathState::pointer pspBest = vpsExpanded[iBest];
|
||
|
||
cLog(lsDEBUG) << boost::str(boost::format("rippleCalc: best: uQuality=%s saInPass=%s saOutPass=%s")
|
||
% STAmount::saFromRate(pspBest->uQuality)
|
||
% pspBest->saInPass.getFullText()
|
||
% pspBest->saOutPass.getFullText());
|
||
|
||
// Record best pass' offers that became unfunded for deletion on success.
|
||
vuUnfundedBecame.insert(vuUnfundedBecame.end(), pspBest->vUnfundedBecame.begin(), pspBest->vUnfundedBecame.end());
|
||
|
||
// Record best pass' LedgerEntrySet to build off of and potentially return.
|
||
lesActive.swapWith(pspBest->lesEntries);
|
||
|
||
saMaxAmountAct += pspBest->saInPass;
|
||
saDstAmountAct += pspBest->saOutPass;
|
||
|
||
if (pspBest->bConsumed || bMultiQuality)
|
||
{
|
||
++iDry;
|
||
pspBest->uQuality = 0;
|
||
}
|
||
|
||
if (saDstAmountAct == saDstAmountReq)
|
||
{
|
||
// Done. Delivered requested amount.
|
||
|
||
terResult = tesSUCCESS;
|
||
}
|
||
else if (saMaxAmountAct != saMaxAmountReq && iDry != vpsExpanded.size())
|
||
{
|
||
// Have not met requested amount or max send, try to do more. Prepare for next pass.
|
||
|
||
// Merge best pass' umReverse.
|
||
rc.mumSource.insert(pspBest->umReverse.begin(), pspBest->umReverse.end());
|
||
|
||
}
|
||
else if (!bPartialPayment)
|
||
{
|
||
// Have sent maximum allowed. Partial payment not allowed.
|
||
|
||
terResult = tecPATH_PARTIAL;
|
||
}
|
||
else
|
||
{
|
||
// Have sent maximum allowed. Partial payment allowed. Success.
|
||
|
||
terResult = tesSUCCESS;
|
||
}
|
||
}
|
||
// Not done and ran out of paths.
|
||
else if (!bPartialPayment)
|
||
{
|
||
// Partial payment not allowed.
|
||
terResult = tecPATH_PARTIAL;
|
||
}
|
||
// Partial payment ok.
|
||
else if (!saDstAmountAct)
|
||
{
|
||
// No payment at all.
|
||
terResult = tecPATH_DRY;
|
||
}
|
||
else
|
||
{
|
||
terResult = tesSUCCESS;
|
||
}
|
||
}
|
||
|
||
if (!bStandAlone)
|
||
{
|
||
if (tesSUCCESS == terResult)
|
||
{
|
||
// Delete became unfunded offers.
|
||
BOOST_FOREACH(const uint256& uOfferIndex, vuUnfundedBecame)
|
||
{
|
||
if (tesSUCCESS == terResult)
|
||
terResult = lesActive.offerDelete(uOfferIndex);
|
||
}
|
||
}
|
||
|
||
// Delete found unfunded offers.
|
||
BOOST_FOREACH(const uint256& uOfferIndex, rc.musUnfundedFound)
|
||
{
|
||
if (tesSUCCESS == terResult)
|
||
terResult = lesActive.offerDelete(uOfferIndex);
|
||
}
|
||
}
|
||
|
||
return terResult;
|
||
}
|
||
|
||
//
|
||
// Rough cut of automatic bridging.
|
||
//
|
||
|
||
#if 0
|
||
// XXX Need to adjust for fees.
|
||
// Find offers to satisfy pnDst.
|
||
// - Does not adjust any balances as there is at least a forward pass to come.
|
||
// --> pnDst.saWanted: currency and amount wanted
|
||
// --> pnSrc.saIOURedeem.mCurrency: use this before saIOUIssue, limit to use.
|
||
// --> pnSrc.saIOUIssue.mCurrency: use this after saIOURedeem, limit to use.
|
||
// <-- pnDst.saReceive
|
||
// <-- pnDst.saIOUForgive
|
||
// <-- pnDst.saIOUAccept
|
||
// <-- terResult : tesSUCCESS = no error and if !bAllowPartial complelely satisfied wanted.
|
||
// <-> usOffersDeleteAlways:
|
||
// <-> usOffersDeleteOnSuccess:
|
||
TER calcOfferFill(PaymentNode& pnSrc, PaymentNode& pnDst, bool bAllowPartial)
|
||
{
|
||
TER terResult;
|
||
|
||
if (pnDst.saWanted.isNative())
|
||
{
|
||
// Transfer XRP.
|
||
|
||
STAmount saSrcFunds = pnSrc.saAccount->accountHolds(pnSrc.saAccount, uint160(0), uint160(0));
|
||
|
||
if (saSrcFunds && (bAllowPartial || saSrcFunds > pnDst.saWanted))
|
||
{
|
||
pnSrc.saSend = min(saSrcFunds, pnDst.saWanted);
|
||
pnDst.saReceive = pnSrc.saSend;
|
||
}
|
||
else
|
||
{
|
||
terResult = terINSUF_PATH;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Ripple funds.
|
||
|
||
// Redeem to limit.
|
||
terResult = calcOfferFill(
|
||
accountHolds(pnSrc.saAccount, pnDst.saWanted.getCurrency(), pnDst.saWanted.getIssuer()),
|
||
pnSrc.saIOURedeem,
|
||
pnDst.saIOUForgive,
|
||
bAllowPartial);
|
||
|
||
if (tesSUCCESS == terResult)
|
||
{
|
||
// Issue to wanted.
|
||
terResult = calcOfferFill(
|
||
pnDst.saWanted, // As much as wanted is available, limited by credit limit.
|
||
pnSrc.saIOUIssue,
|
||
pnDst.saIOUAccept,
|
||
bAllowPartial);
|
||
}
|
||
|
||
if (tesSUCCESS == terResult && !bAllowPartial)
|
||
{
|
||
STAmount saTotal = pnDst.saIOUForgive + pnSrc.saIOUAccept;
|
||
|
||
if (saTotal != saWanted)
|
||
terResult = terINSUF_PATH;
|
||
}
|
||
}
|
||
|
||
return terResult;
|
||
}
|
||
#endif
|
||
|
||
#if 0
|
||
// Get the next offer limited by funding.
|
||
// - Stop when becomes unfunded.
|
||
void TransactionEngine::calcOfferBridgeNext(
|
||
const uint256& uBookRoot, // --> Which order book to look in.
|
||
const uint256& uBookEnd, // --> Limit of how far to look.
|
||
uint256& uBookDirIndex, // <-> Current directory. <-- 0 = no offer available.
|
||
uint64& uBookDirNode, // <-> Which node. 0 = first.
|
||
unsigned int& uBookDirEntry, // <-> Entry in node. 0 = first.
|
||
STAmount& saOfferIn, // <-- How much to pay in, fee inclusive, to get saOfferOut out.
|
||
STAmount& saOfferOut // <-- How much offer pays out.
|
||
)
|
||
{
|
||
saOfferIn = 0; // XXX currency & issuer
|
||
saOfferOut = 0; // XXX currency & issuer
|
||
|
||
bool bDone = false;
|
||
|
||
while (!bDone)
|
||
{
|
||
uint256 uOfferIndex;
|
||
|
||
// Get uOfferIndex.
|
||
mNodes.dirNext(uBookRoot, uBookEnd, uBookDirIndex, uBookDirNode, uBookDirEntry, uOfferIndex);
|
||
|
||
SLE::pointer sleOffer = entryCache(ltOFFER, uOfferIndex);
|
||
|
||
uint160 uOfferOwnerID = sleOffer->getFieldAccount(sfAccount).getAccountID();
|
||
STAmount saOfferPays = sleOffer->getFieldAmount(sfTakerGets);
|
||
STAmount saOfferGets = sleOffer->getFieldAmount(sfTakerPays);
|
||
|
||
if (sleOffer->isFieldPresent(sfExpiration) && sleOffer->getFieldU32(sfExpiration) <= mLedger->getParentCloseTimeNC())
|
||
{
|
||
// Offer is expired.
|
||
cLog(lsINFO) << "calcOfferFirst: encountered expired offer";
|
||
}
|
||
else
|
||
{
|
||
STAmount saOfferFunds = accountFunds(uOfferOwnerID, saOfferPays);
|
||
// Outbound fees are paid by offer owner.
|
||
// XXX Calculate outbound fee rate.
|
||
|
||
if (saOfferPays.isNative())
|
||
{
|
||
// No additional fees for XRP.
|
||
|
||
nothing();
|
||
}
|
||
else if (saOfferPays.getIssuer() == uOfferOwnerID)
|
||
{
|
||
// Offerer is issue own IOUs.
|
||
// No fees at this exact point, XXX receiving node may charge a fee.
|
||
// XXX Make sure has a credit line with receiver, limit by credit line.
|
||
|
||
nothing();
|
||
// XXX Broken - could be issuing or redeeming or both.
|
||
}
|
||
else
|
||
{
|
||
// Offer must be redeeming IOUs.
|
||
|
||
// No additional
|
||
// XXX Broken
|
||
}
|
||
|
||
if (!saOfferFunds.isPositive())
|
||
{
|
||
// Offer is unfunded.
|
||
cLog(lsINFO) << "calcOfferFirst: offer unfunded: delete";
|
||
}
|
||
else if (saOfferFunds >= saOfferPays)
|
||
{
|
||
// Offer fully funded.
|
||
|
||
// Account transfering funds in to offer always pays inbound fees.
|
||
|
||
saOfferIn = saOfferGets; // XXX Add in fees?
|
||
|
||
saOfferOut = saOfferPays;
|
||
|
||
bDone = true;
|
||
}
|
||
else
|
||
{
|
||
// Offer partially funded.
|
||
|
||
// saOfferIn/saOfferFunds = saOfferGets/saOfferPays
|
||
// XXX Round such that all saOffer funds are exhausted.
|
||
saOfferIn = (saOfferFunds*saOfferGets)/saOfferPays; // XXX Add in fees?
|
||
saOfferOut = saOfferFunds;
|
||
|
||
bDone = true;
|
||
}
|
||
}
|
||
|
||
if (!bDone)
|
||
{
|
||
// musUnfundedFound.insert(uOfferIndex);
|
||
}
|
||
}
|
||
while (bNext);
|
||
}
|
||
#endif
|
||
|
||
#if 0
|
||
// If either currency is not XRP, then also calculates vs XRP bridge.
|
||
// --> saWanted: Limit of how much is wanted out.
|
||
// <-- saPay: How much to pay into the offer.
|
||
// <-- saGot: How much to the offer pays out. Never more than saWanted.
|
||
// Given two value's enforce a minimum:
|
||
// - reverse: prv is maximum to pay in (including fee) - cur is what is wanted: generally, minimizing prv
|
||
// - forward: prv is actual amount to pay in (including fee) - cur is what is wanted: generally, minimizing cur
|
||
// Value in is may be rippled or credited from limbo. Value out is put in limbo.
|
||
// If next is an offer, the amount needed is in cur reedem.
|
||
// XXX What about account mentioned multiple times via offers?
|
||
void TransactionEngine::calcNodeOffer(
|
||
bool bForward,
|
||
bool bMultiQuality, // True, if this is the only active path: we can do multiple qualities in this pass.
|
||
const uint160& uPrvAccountID, // If 0, then funds from previous offer's limbo
|
||
const uint160& uPrvCurrencyID,
|
||
const uint160& uPrvIssuerID,
|
||
const uint160& uCurCurrencyID,
|
||
const uint160& uCurIssuerID,
|
||
|
||
const STAmount& uPrvRedeemReq, // --> In limit.
|
||
STAmount& uPrvRedeemAct, // <-> In limit achived.
|
||
const STAmount& uCurRedeemReq, // --> Out limit. Driver when uCurIssuerID == uNxtIssuerID (offer would redeem to next)
|
||
STAmount& uCurRedeemAct, // <-> Out limit achived.
|
||
|
||
const STAmount& uCurIssueReq, // --> In limit.
|
||
STAmount& uCurIssueAct, // <-> In limit achived.
|
||
const STAmount& uCurIssueReq, // --> Out limit. Driver when uCurIssueReq != uNxtIssuerID (offer would effectively issue or transfer to next)
|
||
STAmount& uCurIssueAct, // <-> Out limit achived.
|
||
|
||
STAmount& saPay,
|
||
STAmount& saGot
|
||
) const
|
||
{
|
||
TER terResult = temUNKNOWN;
|
||
|
||
// Direct: not bridging via XRP
|
||
bool bDirectNext = true; // True, if need to load.
|
||
uint256 uDirectQuality;
|
||
uint256 uDirectTip = Ledger::getBookBase(uGetsCurrency, uGetsIssuerID, uPaysCurrency, uPaysIssuerID);
|
||
uint256 uDirectEnd = Ledger::getQualityNext(uDirectTip);
|
||
|
||
// Bridging: bridging via XRP
|
||
bool bBridge = true; // True, if bridging active. False, missing an offer.
|
||
uint256 uBridgeQuality;
|
||
STAmount saBridgeIn; // Amount available.
|
||
STAmount saBridgeOut;
|
||
|
||
bool bInNext = true; // True, if need to load.
|
||
STAmount saInIn; // Amount available. Consumed in loop. Limited by offer funding.
|
||
STAmount saInOut;
|
||
uint256 uInTip; // Current entry.
|
||
uint256 uInEnd;
|
||
unsigned int uInEntry;
|
||
|
||
bool bOutNext = true;
|
||
STAmount saOutIn;
|
||
STAmount saOutOut;
|
||
uint256 uOutTip;
|
||
uint256 uOutEnd;
|
||
unsigned int uOutEntry;
|
||
|
||
saPay.zero();
|
||
saPay.setCurrency(uPrvCurrencyID);
|
||
saPay.setIssuer(uPrvIssuerID);
|
||
|
||
saNeed = saWanted;
|
||
|
||
if (!uCurCurrencyID && !uPrvCurrencyID)
|
||
{
|
||
// Bridging: Neither currency is XRP.
|
||
uInTip = Ledger::getBookBase(uPrvCurrencyID, uPrvIssuerID, CURRENCY_XRP, ACCOUNT_XRP);
|
||
uInEnd = Ledger::getQualityNext(uInTip);
|
||
uOutTip = Ledger::getBookBase(CURRENCY_XRP, ACCOUNT_XRP, uCurCurrencyID, uCurIssuerID);
|
||
uOutEnd = Ledger::getQualityNext(uInTip);
|
||
}
|
||
|
||
// Find our head offer.
|
||
|
||
bool bRedeeming = false;
|
||
bool bIssuing = false;
|
||
|
||
// The price varies as we change between issuing and transfering, so unless bMultiQuality, we must stick with a mode once it
|
||
// is determined.
|
||
|
||
if (bBridge && (bInNext || bOutNext))
|
||
{
|
||
// Bridging and need to calculate next bridge rate.
|
||
// A bridge can consist of multiple offers. As offer's are consumed, the effective rate changes.
|
||
|
||
if (bInNext)
|
||
{
|
||
// sleInDir = entryCache(ltDIR_NODE, mLedger->getNextLedgerIndex(uInIndex, uInEnd));
|
||
// Get the next funded offer.
|
||
offerBridgeNext(uInIndex, uInEnd, uInEntry, saInIn, saInOut); // Get offer limited by funding.
|
||
bInNext = false;
|
||
}
|
||
|
||
if (bOutNext)
|
||
{
|
||
// sleOutDir = entryCache(ltDIR_NODE, mLedger->getNextLedgerIndex(uOutIndex, uOutEnd));
|
||
offerNext(uOutIndex, uOutEnd, uOutEntry, saOutIn, saOutOut);
|
||
bOutNext = false;
|
||
}
|
||
|
||
if (!uInIndex || !uOutIndex)
|
||
{
|
||
bBridge = false; // No more offers to bridge.
|
||
}
|
||
else
|
||
{
|
||
// Have bridge in and out entries.
|
||
// Calculate bridge rate. Out offer pay ripple fee. In offer fee is added to in cost.
|
||
|
||
saBridgeOut.zero();
|
||
|
||
if (saInOut < saOutIn)
|
||
{
|
||
// Limit by in.
|
||
|
||
// XXX Need to include fees in saBridgeIn.
|
||
saBridgeIn = saInIn; // All of in
|
||
// Limit bridge out: saInOut/saBridgeOut = saOutIn/saOutOut
|
||
// Round such that we would take all of in offer, otherwise would have leftovers.
|
||
saBridgeOut = (saInOut * saOutOut) / saOutIn;
|
||
}
|
||
else if (saInOut > saOutIn)
|
||
{
|
||
// Limit by out, if at all.
|
||
|
||
// XXX Need to include fees in saBridgeIn.
|
||
// Limit bridge in:saInIn/saInOuts = aBridgeIn/saOutIn
|
||
// Round such that would take all of out offer.
|
||
saBridgeIn = (saInIn * saOutIn) / saInOuts;
|
||
saBridgeOut = saOutOut; // All of out.
|
||
}
|
||
else
|
||
{
|
||
// Entries match,
|
||
|
||
// XXX Need to include fees in saBridgeIn.
|
||
saBridgeIn = saInIn; // All of in
|
||
saBridgeOut = saOutOut; // All of out.
|
||
}
|
||
|
||
uBridgeQuality = STAmount::getRate(saBridgeIn, saBridgeOut); // Inclusive of fees.
|
||
}
|
||
}
|
||
|
||
if (bBridge)
|
||
{
|
||
bUseBridge = !uDirectTip || (uBridgeQuality < uDirectQuality)
|
||
}
|
||
else if (!!uDirectTip)
|
||
{
|
||
bUseBridge = false
|
||
}
|
||
else
|
||
{
|
||
// No more offers. Declare success, even if none returned.
|
||
saGot = saWanted-saNeed;
|
||
terResult = tesSUCCESS;
|
||
}
|
||
|
||
if (tesSUCCESS != terResult)
|
||
{
|
||
STAmount& saAvailIn = bUseBridge ? saBridgeIn : saDirectIn;
|
||
STAmount& saAvailOut = bUseBridge ? saBridgeOut : saDirectOut;
|
||
|
||
if (saAvailOut > saNeed)
|
||
{
|
||
// Consume part of offer. Done.
|
||
|
||
saNeed = 0;
|
||
saPay += (saNeed*saAvailIn)/saAvailOut; // Round up, prefer to pay more.
|
||
}
|
||
else
|
||
{
|
||
// Consume entire offer.
|
||
|
||
saNeed -= saAvailOut;
|
||
saPay += saAvailIn;
|
||
|
||
if (bUseBridge)
|
||
{
|
||
// Consume bridge out.
|
||
if (saOutOut == saAvailOut)
|
||
{
|
||
// Consume all.
|
||
saOutOut = 0;
|
||
saOutIn = 0;
|
||
bOutNext = true;
|
||
}
|
||
else
|
||
{
|
||
// Consume portion of bridge out, must be consuming all of bridge in.
|
||
// saOutIn/saOutOut = saSpent/saAvailOut
|
||
// Round?
|
||
saOutIn -= (saOutIn*saAvailOut)/saOutOut;
|
||
saOutOut -= saAvailOut;
|
||
}
|
||
|
||
// Consume bridge in.
|
||
if (saOutIn == saAvailIn)
|
||
{
|
||
// Consume all.
|
||
saInOut = 0;
|
||
saInIn = 0;
|
||
bInNext = true;
|
||
}
|
||
else
|
||
{
|
||
// Consume portion of bridge in, must be consuming all of bridge out.
|
||
// saInIn/saInOut = saAvailIn/saPay
|
||
// Round?
|
||
saInOut -= (saInOut*saAvailIn)/saInIn;
|
||
saInIn -= saAvailIn;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
bDirectNext = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
|
||
// vim:ts=4
|