void OrderBookDB::addOrderBook(Book const& book) { bool toXRP = isXRP (book.out); std::lock_guard <std::recursive_mutex> sl (mLock); if (toXRP) { // We don't want to search through all the to-XRP or from-XRP order // books! for (auto ob: mSourceMap[book.in]) { if (isXRP (ob->getCurrencyOut ())) // also to XRP return; } } else { for (auto ob: mDestMap[book.out]) { if (ob->getCurrencyIn() == book.in.currency && ob->getIssuerIn() == book.in.account) { return; } } } uint256 index = getBookBase(book); auto orderBook = std::make_shared<OrderBook> (index, book); mSourceMap[book.in].push_back (orderBook); mDestMap[book.out].push_back (orderBook); if (toXRP) mXRPBooks.insert(book.in); }
Taker::Taker (CrossType cross_type, ApplyView& view, AccountID const& account, Amounts const& offer, std::uint32_t flags, beast::Journal journal) : BasicTaker (cross_type, account, offer, Quality(offer), flags, calculateRate(view, offer.in.getIssuer(), account), calculateRate(view, offer.out.getIssuer(), account), journal) , view_ (view) , xrp_flow_ (0) , direct_crossings_ (0) , bridge_crossings_ (0) { assert (issue_in () == offer.in.issue ()); assert (issue_out () == offer.out.issue ()); if (journal_.debug) { journal_.debug << "Crossing as: " << to_string (account); if (isXRP (issue_in ())) journal_.debug << " Offer in: " << format_amount (offer.in); else journal_.debug << " Offer in: " << format_amount (offer.in) << " (issuer: " << issue_in ().account << ")"; if (isXRP (issue_out ())) journal_.debug << " Offer out: " << format_amount (offer.out); else journal_.debug << " Offer out: " << format_amount (offer.out) << " (issuer: " << issue_out ().account << ")"; journal_.debug << " Balance: " << format_amount (get_funds (account, offer.in)); } }
void attempt ( bool sell, std::string name, Quality taker_quality, cross_attempt_offer const offer, std::string const funds, Quality cross_quality, cross_attempt_offer const cross, std::string const cross_funds, cross_attempt_offer const flow, Issue const& issue_in, Issue const& issue_out, Rate rate_in = parityRate, Rate rate_out = parityRate) { Amounts taker_offer (parse_amounts ( offer.in, issue_in, offer.out, issue_out)); Amounts cross_offer (parse_amounts ( cross.in, issue_in, cross.out, issue_out)); CrossType cross_type; if (isXRP (issue_out)) cross_type = CrossType::IouToXrp; else if (isXRP (issue_in)) cross_type = CrossType::XrpToIou; else cross_type = CrossType::IouToIou; // FIXME: We are always invoking the IOU-to-IOU taker. We should select // the correct type dynamically. TestTaker taker (cross_type, taker_offer, taker_quality, parse_amount (funds, issue_in), sell ? tfSell : 0, rate_in, rate_out); taker.set_funds (parse_amount (cross_funds, issue_out)); auto result = taker.cross (cross_offer, cross_quality); Amounts const expected (parse_amounts ( flow.in, issue_in, flow.out, issue_out)); BEAST_EXPECT(expected == result); if (expected != result) { log << "Expected: " << format_amount (expected.in) << " : " << format_amount (expected.out) << '\n' << " Actual: " << format_amount (result.in) << " : " << format_amount (result.out) << std::endl; } }
TER Taker::cross (Offer const& offer) { // In direct crossings, at least one leg must not be XRP. if (isXRP (offer.amount ().in) && isXRP (offer.amount ().out)) return tefINTERNAL; auto const amount = do_cross ( offer.amount (), offer.quality (), offer.owner ()); return fill (amount, offer); }
// - 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::pushImpliedNodes ( AccountID const& account, // --> Delivering to this account. Currency const& currency, // --> Delivering this currency. AccountID const& issuer) // --> Delivering this issuer. { TER resultCode = tesSUCCESS; JLOG (j_.trace) << "pushImpliedNodes>" << " " << account << " " << currency << " " << issuer; if (nodes_.back ().issue_.currency != currency) { // Currency is different, need to convert via an offer from an order // book. xrpAccount() does double duty as signaling "this is an order // book". // Corresponds to "Implies an offer directory" in the diagram, currently // at http://goo.gl/Uj3HAB. auto type = isXRP(currency) ? STPathElement::typeCurrency : STPathElement::typeCurrency | STPathElement::typeIssuer; // The offer's output is what is now wanted. // xrpAccount() is a placeholder for offers. resultCode = pushNode (type, xrpAccount(), currency, issuer); } // For ripple, non-XRP, ensure the issuer is on at least one side of the // transaction. if (resultCode == tesSUCCESS && !isXRP(currency) && nodes_.back ().account_ != issuer // Previous is not issuing own IOUs. && account != issuer) // Current is not receiving own IOUs. { // Need to ripple through issuer's account. // Case "Implies an another node: (pushImpliedNodes)" in the document. // Intermediate account is the needed issuer. resultCode = pushNode ( STPathElement::typeAll, issuer, currency, issuer); } JLOG (j_.trace) << "pushImpliedNodes< : " << transToken (resultCode); return resultCode; }
TER Taker::cross (Offer const& leg1, Offer const& leg2) { // In bridged crossings, XRP must can't be the input to the first leg // or the output of the second leg. if (isXRP (leg1.amount ().in) || isXRP (leg2.amount ().out)) return tefINTERNAL; auto ret = do_cross ( leg1.amount (), leg1.quality (), leg1.owner (), leg2.amount (), leg2.quality (), leg2.owner ()); return fill (ret.first, leg1, ret.second, leg2); }
// Performs funds transfers to fill the given offer and adjusts offer. TER Taker::fill (BasicTaker::Flow const& flow, Offer const& offer) { // adjust offer consume_offer (offer, flow.order); TER result = tesSUCCESS; if (cross_type () != CrossType::XrpToIou) { assert (!isXRP (flow.order.in)); if(result == tesSUCCESS) result = redeemIOU (account (), flow.issuers.in, flow.issuers.in.issue ()); if (result == tesSUCCESS) result = issueIOU (offer.owner (), flow.order.in, flow.order.in.issue ()); } else { assert (isXRP (flow.order.in)); if (result == tesSUCCESS) result = transferXRP (account (), offer.owner (), flow.order.in); } // Now send funds from the account whose offer we're taking if (cross_type () != CrossType::IouToXrp) { assert (!isXRP (flow.order.out)); if(result == tesSUCCESS) result = redeemIOU (offer.owner (), flow.issuers.out, flow.issuers.out.issue ()); if (result == tesSUCCESS) result = issueIOU (account (), flow.order.out, flow.order.out.issue ()); } else { assert (isXRP (flow.order.out)); if (result == tesSUCCESS) result = transferXRP (offer.owner (), account (), flow.order.out); } if (result == tesSUCCESS) direct_crossings_++; return result; }
static void updateHelper (SLE::ref entry, ripple::unordered_set< uint256 >& seen, OrderBookDB::IssueToOrderBook& destMap, OrderBookDB::IssueToOrderBook& sourceMap, ripple::unordered_set< Issue >& XRPBooks, int& books) { if (entry->getType () == ltDIR_NODE && entry->isFieldPresent (sfExchangeRate) && entry->getFieldH256 (sfRootIndex) == entry->getIndex()) { Book book; book.in.currency.copyFrom (entry->getFieldH160 (sfTakerPaysCurrency)); book.in.account.copyFrom (entry->getFieldH160 (sfTakerPaysIssuer)); book.out.account.copyFrom (entry->getFieldH160 (sfTakerGetsIssuer)); book.out.currency.copyFrom (entry->getFieldH160 (sfTakerGetsCurrency)); uint256 index = Ledger::getBookBase (book); if (seen.insert (index).second) { auto orderBook = std::make_shared<OrderBook> (index, book); sourceMap[book.in].push_back (orderBook); destMap[book.out].push_back (orderBook); if (isXRP(book.out)) XRPBooks.insert(book.in); ++books; } } }
TER Taker::redeemIOU ( AccountID const& account, STAmount const& amount, Issue const& issue) { if (isXRP (amount)) Throw<std::logic_error> ("Using redeemIOU with XRP"); if (account == issue.account) return tesSUCCESS; // Transferring zero is equivalent to not doing a transfer if (amount == zero) return tesSUCCESS; // If we are trying to redeem some amount, then the account // must have a credit balance. if (get_funds (account, amount) <= zero) Throw<std::logic_error> ("redeemIOU has no funds to redeem"); auto ret = ripple::redeemIOU (view_, account, amount, issue, journal_); if (get_funds (account, amount) < zero) Throw<std::logic_error> ("redeemIOU redeemed more funds than available"); return ret; }
std::vector<RippleAddress> STTx::getMentionedAccounts () const { std::vector<RippleAddress> accounts; for (auto const& it : *this) { if (auto sa = dynamic_cast<STAccount const*> (&it)) { auto const na = sa->getValueNCA (); if (std::find (accounts.cbegin (), accounts.cend (), na) == accounts.cend ()) accounts.push_back (na); } else if (auto sa = dynamic_cast<STAmount const*> (&it)) { auto const& issuer = sa->getIssuer (); if (isXRP (issuer)) continue; RippleAddress na; na.setAccountID (issuer); if (std::find (accounts.cbegin (), accounts.cend (), na) == accounts.cend ()) accounts.push_back (na); } } return accounts; }
std::uint32_t Taker::calculateRate ( LedgerView& view, AccountID const& issuer, AccountID const& account) { return isXRP (issuer) || (account == issuer) ? QUALITY_ONE : rippleTransferRate (view, issuer); }
std::string to_string (Issue const& ac) { if (isXRP (ac.account)) return to_string (ac.currency); return to_string(ac.account) + "/" + to_string(ac.currency); }
/** Ordered comparison. The assets are ordered first by currency and then by account, if the currency is not XRP. */ int compare (Issue const& lhs, Issue const& rhs) { int diff = compare (lhs.currency, rhs.currency); if (diff != 0) return diff; if (isXRP (lhs.currency)) return 0; return compare (lhs.account, rhs.account); }
void BasicTaker::log_flow (char const* description, Flow const& flow) { if (!journal_.debug) return; journal_.debug << description; if (isXRP (issue_in ())) journal_.debug << " order in: " << format_amount (flow.order.in); else journal_.debug << " order in: " << format_amount (flow.order.in) << " (issuer: " << format_amount (flow.issuers.in) << ")"; if (isXRP (issue_out ())) journal_.debug << " order out: " << format_amount (flow.order.out); else journal_.debug << " order out: " << format_amount (flow.order.out) << " (issuer: " << format_amount (flow.issuers.out) << ")"; }
TER CreateOffer::checkAcceptAsset(IssueRef issue) const { /* Only valid for custom currencies */ assert (!isXRP (issue.currency)); SLE::pointer const issuerAccount = mEngine->entryCache ( ltACCOUNT_ROOT, Ledger::getAccountRootIndex (issue.account)); if (!issuerAccount) { if (m_journal.warning) m_journal.warning << "delay: can't receive IOUs from non-existent issuer: " << to_string (issue.account); return (mParams & tapRETRY) ? terNO_ACCOUNT : tecNO_ISSUER; } if (issuerAccount->getFieldU32 (sfFlags) & lsfRequireAuth) { SLE::pointer const trustLine (mEngine->entryCache ( ltRIPPLE_STATE, Ledger::getRippleStateIndex ( mTxnAccountID, issue.account, issue.currency))); if (!trustLine) { return (mParams & tapRETRY) ? terNO_LINE : tecNO_LINE; } // Entries have a canonical representation, determined by a // lexicographical "greater than" comparison employing strict weak // ordering. Determine which entry we need to access. bool const canonical_gt (mTxnAccountID > issue.account); bool const is_authorized (trustLine->getFieldU32 (sfFlags) & (canonical_gt ? lsfLowAuth : lsfHighAuth)); if (!is_authorized) { if (m_journal.debug) m_journal.debug << "delay: can't receive IOUs from issuer without auth."; return (mParams & tapRETRY) ? terNO_AUTH : tecNO_AUTH; } } return tesSUCCESS; }
Json::Value trust (Account const& account, STAmount const& amount) { if (isXRP(amount)) throw std::runtime_error( "trust() requires IOU"); Json::Value jv; jv[jss::Account] = account.human(); jv[jss::LimitAmount] = amount.getJson(0); jv[jss::TransactionType] = "TrustSet"; jv[jss::Flags] = 0; // tfClearNoRipple; return jv; }
TER Taker::transfer_xrp ( AccountID const& from, AccountID const& to, STAmount const& amount) { if (!isXRP (amount)) throw std::logic_error ("Using transfer_xrp with IOU"); if (from == to) return tesSUCCESS; // Transferring zero is equivalent to not doing a transfer if (amount == zero) return tesSUCCESS; return view_.transfer_xrp (from, to, amount); }
TER Taker::issueIOU ( AccountID const& account, STAmount const& amount, Issue const& issue) { if (isXRP (amount)) Throw<std::logic_error> ("Using issueIOU with XRP"); if (account == issue.account) return tesSUCCESS; // Transferring zero is equivalent to not doing a transfer if (amount == zero) return tesSUCCESS; return ripple::issueIOU (view_, account, amount, issue, journal_); }
TER Taker::issue_iou ( AccountID const& account, STAmount const& amount, Issue const& issue) { if (isXRP (amount)) throw std::logic_error ("Using issue_iou with XRP"); if (account == issue.account) return tesSUCCESS; // Transferring zero is equivalent to not doing a transfer if (amount == zero) return tesSUCCESS; return view_.issue_iou (account, amount, issue); }
TER PathCursor::reverseLiquidity () const { // Every account has a transfer rate for its issuances. // TOMOVE: The account charges // a fee when third parties transfer that account's own issuances. // node.transferRate_ caches the output transfer rate for this node. node().transferRate_ = amountFromRate ( rippleTransferRate (view(), node().issue_.account)); if (node().isAccount ()) return reverseLiquidityForAccount (); // Otherwise the node is an Offer. if (isXRP (nextNode().account_)) { WriteLog (lsTRACE, RippleCalc) << "reverseLiquidityForOffer: " << "OFFER --> offer: nodeIndex_=" << nodeIndex_; return tesSUCCESS; // This control structure ensures deliverNodeReverse is only called for the // rightmost offer in a chain of offers - which means that // deliverNodeReverse has to take all of those offers into consideration. } // Next is an account node, resolve current offer node's deliver. STAmount saDeliverAct; WriteLog (lsTRACE, RippleCalc) << "reverseLiquidityForOffer: OFFER --> account:" << " nodeIndex_=" << nodeIndex_ << " saRevDeliver=" << node().saRevDeliver; // The next node wants the current node to deliver this much: return deliverNodeReverse ( nextNode().account_, node().saRevDeliver, saDeliverAct); }
void balance::operator()(Env const& env) const { if (isXRP(value_.issue())) { auto const sle = env.le(account_); if (none_) { env.test.expect(! sle); } else if (env.test.expect(sle)) { env.test.expect(sle->getFieldAmount( sfBalance) == value_); } } else { auto const sle = env.le( getRippleStateIndex(account_.id(), value_.issue())); if (none_) { env.test.expect(! sle); } else if (env.test.expect(sle)) { auto amount = sle->getFieldAmount(sfBalance); amount.setIssuer( value_.issue().account); if (account_.id() > value_.issue().account) amount.negate(); env.test.expect(amount == value_); } } }
boost::container::flat_set<AccountID> STTx::getMentionedAccounts () const { boost::container::flat_set<AccountID> list; for (auto const& it : *this) { if (auto sa = dynamic_cast<STAccount const*> (&it)) { assert(! sa->isDefault()); if (! sa->isDefault()) list.insert(sa->value()); } else if (auto sa = dynamic_cast<STAmount const*> (&it)) { auto const& issuer = sa->getIssuer (); if (! isXRP (issuer)) list.insert(issuer); } } return list; }
BasicTaker::BasicTaker ( CrossType cross_type, AccountID const& account, Amounts const& amount, Quality const& quality, std::uint32_t flags, std::uint32_t rate_in, std::uint32_t rate_out, beast::Journal journal) : account_ (account) , quality_ (quality) , threshold_ (quality_) , sell_ (flags & tfSell) , original_ (amount) , remaining_ (amount) , issue_in_ (remaining_.in.issue ()) , issue_out_ (remaining_.out.issue ()) , m_rate_in (rate_in) , m_rate_out (rate_out) , cross_type_ (cross_type) , journal_ (journal) { assert (remaining_.in > zero); assert (remaining_.out > zero); assert (m_rate_in != 0); assert (m_rate_out != 0); // If we are dealing with a particular flavor, make sure that it's the // flavor we expect: assert (cross_type != CrossType::XrpToIou || (isXRP (issue_in ()) && !isXRP (issue_out ()))); assert (cross_type != CrossType::IouToXrp || (!isXRP (issue_in ()) && isXRP (issue_out ()))); // And make sure we're not crossing XRP for XRP assert (!isXRP (issue_in ()) || !isXRP (issue_out ())); // If this is a passive order, we adjust the quality so as to prevent offers // at the same quality level from being consumed. if (flags & tfPassive) ++threshold_; }
TER PathCursor::deliverNodeForward ( AccountID const& uInAccountID, // --> Input owner's account. STAmount const& saInReq, // --> Amount to deliver. STAmount& saInAct, // <-- Amount delivered, this invocation. STAmount& saInFees, // <-- Fees charged, this invocation. bool callerHasLiquidity) const { TER resultCode = tesSUCCESS; // Don't deliver more than wanted. // Zeroed in reverse pass. node().directory.restart(multiQuality_); saInAct.clear (saInReq); saInFees.clear (saInReq); int loopCount = 0; auto viewJ = rippleCalc_.logs_.journal ("View"); // XXX Perhaps make sure do not exceed node().saRevDeliver as another way to // stop? while (resultCode == tesSUCCESS && saInAct + saInFees < saInReq) { // Did not spend all inbound deliver funds. if (++loopCount > (multiQuality_ ? CALC_NODE_DELIVER_MAX_LOOPS_MQ : CALC_NODE_DELIVER_MAX_LOOPS)) { JLOG (j_.warn()) << "deliverNodeForward: max loops cndf"; return telFAILED_PROCESSING; } // Determine values for pass to adjust saInAct, saInFees, and // node().saFwdDeliver. advanceNode (saInAct, false, callerHasLiquidity); // If needed, advance to next funded offer. if (resultCode != tesSUCCESS) { } else if (!node().offerIndex_) { JLOG (j_.warn()) << "deliverNodeForward: INTERNAL ERROR: Ran out of offers."; return telFAILED_PROCESSING; } else if (resultCode == tesSUCCESS) { auto const xferRate = effectiveRate ( previousNode().issue_, uInAccountID, node().offerOwnerAccount_, previousNode().transferRate_); // First calculate assuming no output fees: saInPassAct, // saInPassFees, saOutPassAct. // Offer maximum out - limited by funds with out fees. auto saOutFunded = std::min ( node().saOfferFunds, node().saTakerGets); // Offer maximum out - limit by most to deliver. auto saOutPassFunded = std::min ( saOutFunded, node().saRevDeliver - node().saFwdDeliver); // Offer maximum in - Limited by by payout. auto saInFunded = mulRound ( saOutPassFunded, node().saOfrRate, node().saTakerPays.issue (), true); // Offer maximum in with fees. auto saInTotal = multiplyRound ( saInFunded, xferRate, true); auto saInRemaining = saInReq - saInAct - saInFees; if (saInRemaining < beast::zero) saInRemaining.clear(); // In limited by remaining. auto saInSum = std::min (saInTotal, saInRemaining); // In without fees. auto saInPassAct = std::min ( node().saTakerPays, divideRound (saInSum, xferRate, true)); // Out limited by in remaining. auto outPass = divRound ( saInPassAct, node().saOfrRate, node().saTakerGets.issue (), true); STAmount saOutPassMax = std::min (saOutPassFunded, outPass); STAmount saInPassFeesMax = saInSum - saInPassAct; // Will be determined by next node(). STAmount saOutPassAct; // Will be determined by adjusted saInPassAct. STAmount saInPassFees; JLOG (j_.trace()) << "deliverNodeForward:" << " nodeIndex_=" << nodeIndex_ << " saOutFunded=" << saOutFunded << " saOutPassFunded=" << saOutPassFunded << " node().saOfferFunds=" << node().saOfferFunds << " node().saTakerGets=" << node().saTakerGets << " saInReq=" << saInReq << " saInAct=" << saInAct << " saInFees=" << saInFees << " saInFunded=" << saInFunded << " saInTotal=" << saInTotal << " saInSum=" << saInSum << " saInPassAct=" << saInPassAct << " saOutPassMax=" << saOutPassMax; // FIXME: We remove an offer if WE didn't want anything out of it? if (!node().saTakerPays || saInSum <= beast::zero) { JLOG (j_.debug()) << "deliverNodeForward: Microscopic offer unfunded."; // After math offer is effectively unfunded. pathState_.unfundedOffers().push_back (node().offerIndex_); node().bEntryAdvance = true; continue; } if (!saInFunded) { // Previous check should catch this. JLOG (j_.warn()) << "deliverNodeForward: UNREACHABLE REACHED"; // After math offer is effectively unfunded. pathState_.unfundedOffers().push_back (node().offerIndex_); node().bEntryAdvance = true; continue; } if (!isXRP(nextNode().account_)) { // ? --> 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; JLOG (j_.trace()) << "deliverNodeForward: ? --> OFFER --> account:" << " offerOwnerAccount_=" << node().offerOwnerAccount_ << " nextNode().account_=" << nextNode().account_ << " saOutPassAct=" << saOutPassAct << " saOutFunded=" << saOutFunded; // Output: Debit offer owner, send XRP or non-XPR to next // account. resultCode = accountSend(view(), node().offerOwnerAccount_, nextNode().account_, saOutPassAct, viewJ); if (resultCode != tesSUCCESS) 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. resultCode = increment().deliverNodeForward ( node().offerOwnerAccount_, // --> Current holder. saOutPassMax, // --> Amount available. saOutPassAct, // <-- Amount delivered. saOutPassFees, // <-- Fees charged. saInAct > beast::zero); if (resultCode != tesSUCCESS) 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. assert (saOutPassAct < saOutPassMax); auto inPassAct = mulRound ( saOutPassAct, node().saOfrRate, saInReq.issue (), true); saInPassAct = std::min (node().saTakerPays, inPassAct); auto inPassFees = multiplyRound ( saInPassAct, xferRate, true); saInPassFees = std::min (saInPassFeesMax, inPassFees); } // Do outbound debiting. // Send to issuer/limbo total amount including fees (issuer gets // fees). auto const& id = isXRP(node().issue_) ? xrpAccount() : node().issue_.account; auto outPassTotal = saOutPassAct + saOutPassFees; accountSend(view(), node().offerOwnerAccount_, id, outPassTotal, viewJ); JLOG (j_.trace()) << "deliverNodeForward: ? --> OFFER --> offer:" << " saOutPassAct=" << saOutPassAct << " saOutPassFees=" << saOutPassFees; } JLOG (j_.trace()) << "deliverNodeForward: " << " nodeIndex_=" << nodeIndex_ << " node().saTakerGets=" << node().saTakerGets << " node().saTakerPays=" << node().saTakerPays << " saInPassAct=" << saInPassAct << " saInPassFees=" << saInPassFees << " saOutPassAct=" << saOutPassAct << " saOutFunded=" << saOutFunded; // Funds were spent. node().bFundsDirty = true; // Do inbound crediting. // // Credit offer owner from in issuer/limbo (input transfer fees left // with owner). Don't attempt to have someone credit themselves, it // is redundant. if (isXRP (previousNode().issue_.currency) || uInAccountID != node().offerOwnerAccount_) { auto id = !isXRP(previousNode().issue_.currency) ? uInAccountID : xrpAccount(); resultCode = accountSend(view(), id, node().offerOwnerAccount_, saInPassAct, viewJ); if (resultCode != tesSUCCESS) break; } // Adjust offer. // // Fees are considered paid from a seperate budget and are not named // in the offer. STAmount saTakerGetsNew = node().saTakerGets - saOutPassAct; STAmount saTakerPaysNew = node().saTakerPays - saInPassAct; if (saTakerPaysNew < beast::zero || saTakerGetsNew < beast::zero) { JLOG (j_.warn()) << "deliverNodeForward: NEGATIVE:" << " saTakerPaysNew=" << saTakerPaysNew << " saTakerGetsNew=" << saTakerGetsNew; resultCode = telFAILED_PROCESSING; break; } node().sleOffer->setFieldAmount (sfTakerGets, saTakerGetsNew); node().sleOffer->setFieldAmount (sfTakerPays, saTakerPaysNew); view().update (node().sleOffer); if (saOutPassAct == saOutFunded || saTakerGetsNew == beast::zero) { // Offer became unfunded. JLOG (j_.debug()) << "deliverNodeForward: unfunded:" << " saOutPassAct=" << saOutPassAct << " saOutFunded=" << saOutFunded; pathState_.unfundedOffers().push_back (node().offerIndex_); node().bEntryAdvance = true; } else { if (saOutPassAct >= saOutFunded) { JLOG (j_.warn()) << "deliverNodeForward: TOO MUCH:" << " saOutPassAct=" << saOutPassAct << " saOutFunded=" << saOutFunded; } assert (saOutPassAct < saOutFunded); } saInAct += saInPassAct; saInFees += saInPassFees; // Adjust amount available to next node(). node().saFwdDeliver = std::min (node().saRevDeliver, node().saFwdDeliver + saOutPassAct); } } JLOG (j_.trace()) << "deliverNodeForward<" << " nodeIndex_=" << nodeIndex_ << " saInAct=" << saInAct << " saInFees=" << saInFees; return resultCode; }
// To deliver from an order book, when computing TER PathCursor::deliverNodeReverseImpl ( AccountID const& uOutAccountID, // --> Output owner's account. STAmount const& saOutReq, // --> Funds requested to be // delivered for an increment. STAmount& saOutAct, // <-- Funds actually delivered for an // increment bool callerHasLiquidity ) const { TER resultCode = tesSUCCESS; // Accumulation of what the previous node must deliver. // Possible optimization: Note this gets zeroed on each increment, ideally // only on first increment, then it could be a limit on the forward pass. saOutAct.clear (saOutReq); JLOG (j_.trace()) << "deliverNodeReverse>" << " saOutAct=" << saOutAct << " saOutReq=" << saOutReq << " saPrvDlvReq=" << previousNode().saRevDeliver; assert (saOutReq != zero); int loopCount = 0; auto viewJ = rippleCalc_.logs_.journal ("View"); // While we did not deliver as much as requested: while (saOutAct < saOutReq) { if (++loopCount > (multiQuality_ ? CALC_NODE_DELIVER_MAX_LOOPS_MQ : CALC_NODE_DELIVER_MAX_LOOPS)) { JLOG (j_.warn()) << "loop count exceeded"; return telFAILED_PROCESSING; } resultCode = advanceNode (saOutAct, true, callerHasLiquidity); // If needed, advance to next funded offer. if (resultCode != tesSUCCESS || !node().offerIndex_) // Error or out of offers. break; auto const xferRate = effectiveRate ( node().issue_, uOutAccountID, node().offerOwnerAccount_, node().transferRate_); JLOG (j_.trace()) << "deliverNodeReverse:" << " offerOwnerAccount_=" << node().offerOwnerAccount_ << " uOutAccountID=" << uOutAccountID << " node().issue_.account=" << node().issue_.account << " xferRate=" << xferRate; // Only use rate when not in multi-quality mode if (!multiQuality_) { if (!node().rateMax) { // Set initial rate. JLOG (j_.trace()) << "Set initial rate"; node().rateMax = xferRate; } else if (xferRate > node().rateMax) { // Offer exceeds initial rate. JLOG (j_.trace()) << "Offer exceeds initial rate: " << *node().rateMax; break; // Done. Don't bother looking for smaller transferRates. } else if (xferRate < node().rateMax) { // Reducing rate. Additional offers will only // be considered for this increment if they // are at least this good. // // At this point, the overall rate is reducing, // while the overall rate is not xferRate, it // would be wrong to add anything with a rate // above xferRate. // // The rate would be reduced if the current // offer was from the issuer and the previous // offer wasn't. JLOG (j_.trace()) << "Reducing rate: " << *node().rateMax; node().rateMax = xferRate; } } // Amount that goes to the taker. STAmount saOutPassReq = std::min ( std::min (node().saOfferFunds, node().saTakerGets), saOutReq - saOutAct); // Maximum out - assuming no out fees. STAmount saOutPassAct = saOutPassReq; // 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. // // Round down: prefer liquidity rather than microscopic fees. STAmount saOutPlusFees = multiplyRound ( saOutPassAct, xferRate, false); // Offer out with fees. JLOG (j_.trace()) << "deliverNodeReverse:" << " saOutReq=" << saOutReq << " saOutAct=" << saOutAct << " node().saTakerGets=" << node().saTakerGets << " saOutPassAct=" << saOutPassAct << " saOutPlusFees=" << saOutPlusFees << " node().saOfferFunds=" << node().saOfferFunds; if (saOutPlusFees > node().saOfferFunds) { // Offer owner can not cover all fees, compute saOutPassAct based on // node().saOfferFunds. saOutPlusFees = node().saOfferFunds; // Round up: prefer liquidity rather than microscopic fees. But, // limit by requested. auto fee = divideRound (saOutPlusFees, xferRate, true); saOutPassAct = std::min (saOutPassReq, fee); JLOG (j_.trace()) << "deliverNodeReverse: Total exceeds fees:" << " saOutPassAct=" << saOutPassAct << " saOutPlusFees=" << saOutPlusFees << " node().saOfferFunds=" << node().saOfferFunds; } // Compute portion of input needed to cover actual output. auto outputFee = mulRound ( saOutPassAct, node().saOfrRate, node().saTakerPays.issue (), true); if (*stAmountCalcSwitchover == false && ! outputFee) { JLOG (j_.fatal()) << "underflow computing outputFee " << "saOutPassAct: " << saOutPassAct << " saOfrRate: " << node ().saOfrRate; return telFAILED_PROCESSING; } STAmount saInPassReq = std::min (node().saTakerPays, outputFee); STAmount saInPassAct; JLOG (j_.trace()) << "deliverNodeReverse:" << " outputFee=" << outputFee << " saInPassReq=" << saInPassReq << " node().saOfrRate=" << node().saOfrRate << " saOutPassAct=" << saOutPassAct << " saOutPlusFees=" << saOutPlusFees; if (!saInPassReq) // FIXME: This is bogus { // After rounding did not want anything. JLOG (j_.debug()) << "deliverNodeReverse: micro offer is unfunded."; node().bEntryAdvance = true; continue; } // Find out input amount actually available at current rate. else if (!isXRP(previousNode().account_)) { // 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; JLOG (j_.trace()) << "deliverNodeReverse: account --> OFFER --> ? :" << " saInPassAct=" << saInPassAct; } else { // offer --> OFFER --> ? // Compute in previous offer node how much could come in. // TODO(tom): Fix nasty recursion here! resultCode = increment(-1).deliverNodeReverseImpl( node().offerOwnerAccount_, saInPassReq, saInPassAct, saOutAct > zero); if (amendmentRIPD1141(view().info().parentCloseTime)) { // The recursive call is dry this time, but we have liquidity // from previous calls if (resultCode == tecPATH_DRY && saOutAct > zero) { resultCode = tesSUCCESS; break; } } JLOG (j_.trace()) << "deliverNodeReverse: offer --> OFFER --> ? :" << " saInPassAct=" << saInPassAct; } if (resultCode != tesSUCCESS) break; if (saInPassAct < saInPassReq) { // Adjust output to conform to limited input. auto outputRequirements = divRound (saInPassAct, node ().saOfrRate, node ().saTakerGets.issue (), true); saOutPassAct = std::min (saOutPassReq, outputRequirements); auto outputFees = multiplyRound (saOutPassAct, xferRate, true); saOutPlusFees = std::min (node().saOfferFunds, outputFees); JLOG (j_.trace()) << "deliverNodeReverse: adjusted:" << " saOutPassAct=" << saOutPassAct << " saOutPlusFees=" << saOutPlusFees; } else { // TODO(tom): more logging here. assert (saInPassAct == saInPassReq); } // Funds were spent. node().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. resultCode = accountSend(view(), node().offerOwnerAccount_, node().issue_.account, saOutPassAct, viewJ); if (resultCode != tesSUCCESS) break; // Adjust offer STAmount saTakerGetsNew = node().saTakerGets - saOutPassAct; STAmount saTakerPaysNew = node().saTakerPays - saInPassAct; if (saTakerPaysNew < zero || saTakerGetsNew < zero) { JLOG (j_.warn()) << "deliverNodeReverse: NEGATIVE:" << " node().saTakerPaysNew=" << saTakerPaysNew << " node().saTakerGetsNew=" << saTakerGetsNew; resultCode = telFAILED_PROCESSING; break; } node().sleOffer->setFieldAmount (sfTakerGets, saTakerGetsNew); node().sleOffer->setFieldAmount (sfTakerPays, saTakerPaysNew); view().update (node().sleOffer); if (saOutPassAct == node().saTakerGets) { // Offer became unfunded. JLOG (j_.debug()) << "deliverNodeReverse: offer became unfunded."; node().bEntryAdvance = true; // XXX When don't we want to set advance? } else { assert (saOutPassAct < node().saTakerGets); } saOutAct += saOutPassAct; // Accumulate what is to be delivered from previous node. previousNode().saRevDeliver += saInPassAct; } if (saOutAct > saOutReq) { JLOG (j_.warn()) << "deliverNodeReverse: TOO MUCH:" << " saOutAct=" << saOutAct << " saOutReq=" << saOutReq; } assert(saOutAct <= saOutReq); if (resultCode == tesSUCCESS && !saOutAct) resultCode = tecPATH_DRY; // Unable to meet request, consider path dry. // Design invariant: if nothing was actually delivered, return tecPATH_DRY. JLOG (j_.trace()) << "deliverNodeReverse<" << " saOutAct=" << saOutAct << " saOutReq=" << saOutReq << " saPrvDlvReq=" << previousNode().saRevDeliver; return resultCode; }
// Set this object to be an expanded path from spSourcePath - take the implied // nodes and makes them explicit. It also sanitizes the path. // // There are only two types of nodes: account nodes and order books nodes. // // You can infer some nodes automatically. If you're paying me bitstamp USD, // then there must be an intermediate bitstamp node. // // If you have accounts A and B, and they're delivery currency issued by C, then // there must be a node with account C in the middle. // // If you're paying USD and getting bitcoins, there has to be an order book in // between. // // terStatus = tesSUCCESS, temBAD_PATH, terNO_LINE, terNO_ACCOUNT, terNO_AUTH, // or temBAD_PATH_LOOP TER PathState::expandPath ( STPath const& spSourcePath, AccountID const& uReceiverID, AccountID const& uSenderID) { uQuality = 1; // Mark path as active. Currency const& uMaxCurrencyID = saInReq.getCurrency (); AccountID const& uMaxIssuerID = saInReq.getIssuer (); Currency const& currencyOutID = saOutReq.getCurrency (); AccountID const& issuerOutID = saOutReq.getIssuer (); AccountID const& uSenderIssuerID = isXRP(uMaxCurrencyID) ? xrpAccount() : uSenderID; // Sender is always issuer for non-XRP. JLOG (j_.trace) << "expandPath> " << spSourcePath.getJson (0); terStatus = tesSUCCESS; // XRP with issuer is malformed. if ((isXRP (uMaxCurrencyID) && !isXRP (uMaxIssuerID)) || (isXRP (currencyOutID) && !isXRP (issuerOutID))) { JLOG (j_.debug) << "expandPath> issuer with XRP"; 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 (terStatus == tesSUCCESS) { terStatus = pushNode ( !isXRP(uMaxCurrencyID) ? STPathElement::typeAccount | STPathElement::typeCurrency | STPathElement::typeIssuer : STPathElement::typeAccount | STPathElement::typeCurrency, uSenderID, uMaxCurrencyID, // Max specifies the currency. uSenderIssuerID); } JLOG (j_.debug) << "expandPath: pushed:" << " account=" << uSenderID << " currency=" << uMaxCurrencyID << " issuer=" << uSenderIssuerID; // Issuer was not same as sender. if (tesSUCCESS == terStatus && uMaxIssuerID != uSenderIssuerID) { // May have an implied account node. // - If it was XRP, then issuers would have matched. // Figure out next node properties for implied node. const auto uNxtCurrencyID = spSourcePath.size () ? Currency(spSourcePath.front ().getCurrency ()) // Use next node. : currencyOutID; // Use send. // TODO(tom): complexify this next logic further in case someone // understands it. const auto nextAccountID = spSourcePath.size () ? AccountID(spSourcePath. front ().getAccountID ()) : !isXRP(currencyOutID) ? (issuerOutID == uReceiverID) ? AccountID(uReceiverID) : AccountID(issuerOutID) // Use implied node. : xrpAccount(); JLOG (j_.debug) << "expandPath: implied check:" << " uMaxIssuerID=" << uMaxIssuerID << " uSenderIssuerID=" << uSenderIssuerID << " uNxtCurrencyID=" << uNxtCurrencyID << " nextAccountID=" << nextAccountID; // 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 != nextAccountID) // Next is not implied issuer { JLOG (j_.debug) << "expandPath: sender implied:" << " account=" << uMaxIssuerID << " currency=" << uMaxCurrencyID << " issuer=" << uMaxIssuerID; // Add account implied by SendMax. terStatus = pushNode ( !isXRP(uMaxCurrencyID) ? STPathElement::typeAccount | STPathElement::typeCurrency | STPathElement::typeIssuer : STPathElement::typeAccount | STPathElement::typeCurrency, uMaxIssuerID, uMaxCurrencyID, uMaxIssuerID); } } for (auto & speElement: spSourcePath) { if (terStatus == tesSUCCESS) { JLOG (j_.trace) << "expandPath: element in path"; terStatus = pushNode ( speElement.getNodeType (), speElement.getAccountID (), speElement.getCurrency (), speElement.getIssuerID ()); } } if (terStatus == tesSUCCESS && !isXRP(currencyOutID) // Next is not XRP && issuerOutID != uReceiverID) // Out issuer is not receiver { assert (!nodes_.empty ()); auto const& backNode = nodes_.back (); if (backNode.issue_.currency != currencyOutID // Previous will be offer || backNode.account_ != issuerOutID) // Need implied issuer { // Add implied account. JLOG (j_.debug) << "expandPath: receiver implied:" << " account=" << issuerOutID << " currency=" << currencyOutID << " issuer=" << issuerOutID; terStatus = pushNode ( !isXRP(currencyOutID) ? STPathElement::typeAccount | STPathElement::typeCurrency | STPathElement::typeIssuer : STPathElement::typeAccount | STPathElement::typeCurrency, issuerOutID, currencyOutID, issuerOutID); } } if (terStatus == tesSUCCESS) { // Create receiver node. // Last node is always an account. terStatus = pushNode ( !isXRP(currencyOutID) ? STPathElement::typeAccount | STPathElement::typeCurrency | STPathElement::typeIssuer : STPathElement::typeAccount | STPathElement::typeCurrency, uReceiverID, // Receive to output currencyOutID, // Desired currency uReceiverID); } if (terStatus == tesSUCCESS) { // Look for first mention of source in nodes and detect loops. // Note: The output is not allowed to be a source. unsigned int index = 0; for (auto& node: nodes_) { AccountIssue accountIssue (node.account_, node.issue_); if (!umForward.insert ({accountIssue, index++}).second) { // Failed to insert. Have a loop. JLOG (j_.debug) << "expandPath: loop detected: " << getJson (); terStatus = temBAD_PATH_LOOP; break; } } } JLOG (j_.debug) << "expandPath:" << " in=" << uMaxCurrencyID << "/" << uMaxIssuerID << " out=" << currencyOutID << "/" << issuerOutID << ": " << getJson (); return terStatus; }
// Append a node, then create and insert before it any implied nodes. Order // book nodes may go back to back. // // For each non-matching pair of IssuedCurrency, there's an order book. // // <-- resultCode: tesSUCCESS, temBAD_PATH, terNO_ACCOUNT, terNO_AUTH, // terNO_LINE, tecPATH_DRY TER PathState::pushNode ( const int iType, AccountID const& account, // If not specified, means an order book. Currency const& currency, // If not specified, default to previous. AccountID const& issuer) // If not specified, default to previous. { path::Node node; const bool pathIsEmpty = nodes_.empty (); // TODO(tom): if pathIsEmpty, we probably don't need to do ANYTHING below. // Indeed, we might just not even call pushNode in the first place! auto const& backNode = pathIsEmpty ? path::Node () : nodes_.back (); // true, iff node is a ripple account. false, iff node is an offer node. const bool hasAccount = (iType & STPathElement::typeAccount); // Is currency specified for the output of the current node? const bool hasCurrency = (iType & STPathElement::typeCurrency); // Issuer is specified for the output of the current node. const bool hasIssuer = (iType & STPathElement::typeIssuer); TER resultCode = tesSUCCESS; JLOG (j_.trace) << "pushNode> " << iType << ": " << (hasAccount ? to_string(account) : std::string("-")) << " " << (hasCurrency ? to_string(currency) : std::string("-")) << "/" << (hasIssuer ? to_string(issuer) : std::string("-")) << "/"; node.uFlags = iType; node.issue_.currency = hasCurrency ? currency : backNode.issue_.currency; // TODO(tom): we can probably just return immediately whenever we hit an // error in these next pages. if (iType & ~STPathElement::typeAll) { // Of course, this could never happen. JLOG (j_.debug) << "pushNode: bad bits."; resultCode = temBAD_PATH; } else if (hasIssuer && isXRP (node.issue_)) { JLOG (j_.debug) << "pushNode: issuer specified for XRP."; resultCode = temBAD_PATH; } else if (hasIssuer && !issuer) { JLOG (j_.debug) << "pushNode: specified bad issuer."; resultCode = temBAD_PATH; } else if (!hasAccount && !hasCurrency && !hasIssuer) { // You can't default everything to the previous node as you would make // no progress. JLOG (j_.debug) << "pushNode: offer must specify at least currency or issuer."; resultCode = temBAD_PATH; } else if (hasAccount) { // Account link node.account_ = account; node.issue_.account = hasIssuer ? issuer : (isXRP (node.issue_) ? xrpAccount() : account); // Zero value - for accounts. node.saRevRedeem = STAmount ({node.issue_.currency, account}); node.saRevIssue = node.saRevRedeem; // For order books only - zero currency with the issuer ID. node.saRevDeliver = STAmount (node.issue_); node.saFwdDeliver = node.saRevDeliver; if (pathIsEmpty) { // The first node is always correct as is. } else if (!account) { JLOG (j_.debug) << "pushNode: specified bad account."; resultCode = temBAD_PATH; } else { // Add required intermediate nodes to deliver to current account. JLOG (j_.trace) << "pushNode: imply for account."; resultCode = pushImpliedNodes ( node.account_, node.issue_.currency, isXRP(node.issue_.currency) ? xrpAccount() : account); // Note: backNode may no longer be the immediately previous node. } if (resultCode == tesSUCCESS && !nodes_.empty ()) { auto const& backNode = nodes_.back (); if (backNode.isAccount()) { auto sleRippleState = view().peek( keylet::line(backNode.account_, node.account_, backNode.issue_.currency)); // A "RippleState" means a balance betweeen two accounts for a // specific currency. if (!sleRippleState) { JLOG (j_.trace) << "pushNode: No credit line between " << backNode.account_ << " and " << node.account_ << " for " << node.issue_.currency << "." ; JLOG (j_.trace) << getJson (); resultCode = terNO_LINE; } else { JLOG (j_.trace) << "pushNode: Credit line found between " << backNode.account_ << " and " << node.account_ << " for " << node.issue_.currency << "." ; auto sleBck = view().peek ( keylet::account(backNode.account_)); // Is the source account the highest numbered account ID? bool bHigh = backNode.account_ > node.account_; if (!sleBck) { JLOG (j_.warning) << "pushNode: delay: can't receive IOUs from " << "non-existent issuer: " << backNode.account_; resultCode = terNO_ACCOUNT; } else if ((sleBck->getFieldU32 (sfFlags) & lsfRequireAuth) && !(sleRippleState->getFieldU32 (sfFlags) & (bHigh ? lsfHighAuth : lsfLowAuth)) && sleRippleState->getFieldAmount(sfBalance) == zero) { JLOG (j_.warning) << "pushNode: delay: can't receive IOUs from " << "issuer without auth."; resultCode = terNO_AUTH; } if (resultCode == tesSUCCESS) { STAmount saOwed = creditBalance (view(), node.account_, backNode.account_, node.issue_.currency); STAmount saLimit; if (saOwed <= zero) { saLimit = creditLimit (view(), node.account_, backNode.account_, node.issue_.currency); if (-saOwed >= saLimit) { JLOG (j_.debug) << "pushNode: dry:" << " saOwed=" << saOwed << " saLimit=" << saLimit; resultCode = tecPATH_DRY; } } } } } } if (resultCode == tesSUCCESS) nodes_.push_back (node); } else { // Offer link. // // Offers bridge a change in currency and issuer, or just a change in // issuer. if (hasIssuer) node.issue_.account = issuer; else if (isXRP (node.issue_.currency)) node.issue_.account = xrpAccount(); else if (isXRP (backNode.issue_.account)) node.issue_.account = backNode.account_; else node.issue_.account = backNode.issue_.account; node.saRateMax = STAmount::saZero; node.saRevDeliver = STAmount (node.issue_); node.saFwdDeliver = node.saRevDeliver; if (!isConsistent (node.issue_)) { JLOG (j_.debug) << "pushNode: currency is inconsistent with issuer."; resultCode = temBAD_PATH; } else if (backNode.issue_ == node.issue_) { JLOG (j_.debug) << "pushNode: bad path: offer to same currency and issuer"; resultCode = temBAD_PATH; } else { JLOG (j_.trace) << "pushNode: imply for offer."; // Insert intermediary issuer account if needed. resultCode = pushImpliedNodes ( xrpAccount(), // Rippling, but offers don't have an account. backNode.issue_.currency, backNode.issue_.account); } if (resultCode == tesSUCCESS) nodes_.push_back (node); } JLOG (j_.trace) << "pushNode< : " << transToken (resultCode); return resultCode; }
// This interface is deprecated. Json::Value doRipplePathFind (RPC::Context& context) { RPC::LegacyPathFind lpf (context.role == Role::ADMIN); if (!lpf.isOk ()) return rpcError (rpcTOO_BUSY); context.loadType = Resource::feeHighBurdenRPC; RippleAddress raSrc; RippleAddress raDst; STAmount saDstAmount; Ledger::pointer lpLedger; Json::Value jvResult; if (getConfig().RUN_STANDALONE || context.params.isMember(jss::ledger) || context.params.isMember(jss::ledger_index) || context.params.isMember(jss::ledger_hash)) { // The caller specified a ledger jvResult = RPC::lookupLedger ( context.params, lpLedger, context.netOps); if (!lpLedger) return jvResult; } if (!context.params.isMember ("source_account")) { jvResult = rpcError (rpcSRC_ACT_MISSING); } else if (!context.params["source_account"].isString () || !raSrc.setAccountID ( context.params["source_account"].asString ())) { jvResult = rpcError (rpcSRC_ACT_MALFORMED); } else if (!context.params.isMember ("destination_account")) { jvResult = rpcError (rpcDST_ACT_MISSING); } else if (!context.params["destination_account"].isString () || !raDst.setAccountID ( context.params["destination_account"].asString ())) { jvResult = rpcError (rpcDST_ACT_MALFORMED); } else if ( // Parse saDstAmount. !context.params.isMember ("destination_amount") || ! amountFromJsonNoThrow(saDstAmount, context.params["destination_amount"]) || saDstAmount <= zero || (!isXRP(saDstAmount.getCurrency ()) && (!saDstAmount.getIssuer () || noAccount() == saDstAmount.getIssuer ()))) { WriteLog (lsINFO, RPCHandler) << "Bad destination_amount."; jvResult = rpcError (rpcINVALID_PARAMS); } else if ( // Checks on source_currencies. context.params.isMember ("source_currencies") && (!context.params["source_currencies"].isArray () || !context.params["source_currencies"].size ()) // Don't allow empty currencies. ) { WriteLog (lsINFO, RPCHandler) << "Bad source_currencies."; jvResult = rpcError (rpcINVALID_PARAMS); } else { context.loadType = Resource::feeHighBurdenRPC; RippleLineCache::pointer cache; if (lpLedger) { // The caller specified a ledger lpLedger = std::make_shared<Ledger> (std::ref (*lpLedger), false); cache = std::make_shared<RippleLineCache>(lpLedger); } else { // The closed ledger is recent and any nodes made resident // have the best chance to persist lpLedger = context.netOps.getClosedLedger(); cache = getApp().getPathRequests().getLineCache(lpLedger, false); } Json::Value jvSrcCurrencies; if (context.params.isMember ("source_currencies")) { jvSrcCurrencies = context.params["source_currencies"]; } else { auto currencies = accountSourceCurrencies (raSrc, cache, true); jvSrcCurrencies = Json::Value (Json::arrayValue); for (auto const& uCurrency: currencies) { Json::Value jvCurrency (Json::objectValue); jvCurrency["currency"] = to_string(uCurrency); jvSrcCurrencies.append (jvCurrency); } } // Fill in currencies destination will accept Json::Value jvDestCur (Json::arrayValue); // TODO(tom): this could be optimized the same way that // PathRequest::doUpdate() is - if we don't obsolete this code first. auto usDestCurrID = accountDestCurrencies (raDst, cache, true); for (auto const& uCurrency: usDestCurrID) jvDestCur.append (to_string (uCurrency)); jvResult["destination_currencies"] = jvDestCur; jvResult["destination_account"] = raDst.humanAccountID (); Json::Value jvArray (Json::arrayValue); int level = getConfig().PATH_SEARCH_OLD; if ((getConfig().PATH_SEARCH_MAX > level) && !getApp().getFeeTrack().isLoadedLocal()) { ++level; } if (context.params.isMember("search_depth") && context.params["search_depth"].isIntegral()) { int rLev = context.params["search_depth"].asInt (); if ((rLev < level) || (context.role == Role::ADMIN)) level = rLev; } FindPaths fp ( cache, raSrc.getAccountID(), raDst.getAccountID(), saDstAmount, level, 4); // max paths for (unsigned int i = 0; i != jvSrcCurrencies.size (); ++i) { Json::Value jvSource = jvSrcCurrencies[i]; Currency uSrcCurrencyID; Account uSrcIssuerID; if (!jvSource.isObject ()) return rpcError (rpcINVALID_PARAMS); // Parse mandatory currency. if (!jvSource.isMember ("currency") || !to_currency ( uSrcCurrencyID, jvSource["currency"].asString ())) { WriteLog (lsINFO, RPCHandler) << "Bad currency."; return rpcError (rpcSRC_CUR_MALFORMED); } if (uSrcCurrencyID.isNonZero ()) uSrcIssuerID = raSrc.getAccountID (); // Parse optional issuer. if (jvSource.isMember ("issuer") && ((!jvSource["issuer"].isString () || !to_issuer (uSrcIssuerID, jvSource["issuer"].asString ())) || (uSrcIssuerID.isZero () != uSrcCurrencyID.isZero ()) || (noAccount() == uSrcIssuerID))) { WriteLog (lsINFO, RPCHandler) << "Bad issuer."; return rpcError (rpcSRC_ISR_MALFORMED); } STPathSet spsComputed; if (context.params.isMember("paths")) { Json::Value pathSet = Json::objectValue; pathSet["Paths"] = context.params["paths"]; STParsedJSONObject paths ("pathSet", pathSet); if (paths.object.get() == nullptr) return paths.error; else { spsComputed = paths.object.get()->getFieldPathSet (sfPaths); WriteLog (lsTRACE, RPCHandler) << "ripple_path_find: Paths: " << spsComputed.getJson (0); } } STPath fullLiquidityPath; auto valid = fp.findPathsForIssue ( {uSrcCurrencyID, uSrcIssuerID}, spsComputed, fullLiquidityPath); if (!valid) { WriteLog (lsWARNING, RPCHandler) << "ripple_path_find: No paths found."; } else { auto& issuer = isXRP (uSrcIssuerID) ? isXRP (uSrcCurrencyID) ? // Default to source account. xrpAccount() : Account (raSrc.getAccountID ()) : uSrcIssuerID; // Use specifed issuer. STAmount saMaxAmount ({uSrcCurrencyID, issuer}, 1); saMaxAmount.negate (); LedgerEntrySet lesSandbox (lpLedger, tapNONE); auto rc = path::RippleCalc::rippleCalculate ( lesSandbox, saMaxAmount, // --> Amount to send is unlimited // to get an estimate. saDstAmount, // --> Amount to deliver. raDst.getAccountID (), // --> Account to deliver to. raSrc.getAccountID (), // --> Account sending from. spsComputed); // --> Path set. WriteLog (lsWARNING, RPCHandler) << "ripple_path_find:" << " saMaxAmount=" << saMaxAmount << " saDstAmount=" << saDstAmount << " saMaxAmountAct=" << rc.actualAmountIn << " saDstAmountAct=" << rc.actualAmountOut; if (fullLiquidityPath.size() > 0 && (rc.result() == terNO_LINE || rc.result() == tecPATH_PARTIAL)) { WriteLog (lsDEBUG, PathRequest) << "Trying with an extra path element"; spsComputed.push_back (fullLiquidityPath); lesSandbox.clear (); rc = path::RippleCalc::rippleCalculate ( lesSandbox, saMaxAmount, // --> Amount to send is unlimited // to get an estimate. saDstAmount, // --> Amount to deliver. raDst.getAccountID (), // --> Account to deliver to. raSrc.getAccountID (), // --> Account sending from. spsComputed); // --> Path set. WriteLog (lsDEBUG, PathRequest) << "Extra path element gives " << transHuman (rc.result ()); } if (rc.result () == tesSUCCESS) { Json::Value jvEntry (Json::objectValue); STPathSet spsCanonical; // Reuse the expanded as it would need to be calcuated // anyway to produce the canonical. (At least unless we // make a direct canonical.) jvEntry["source_amount"] = rc.actualAmountIn.getJson (0); jvEntry["paths_canonical"] = Json::arrayValue; jvEntry["paths_computed"] = spsComputed.getJson (0); jvArray.append (jvEntry); } else { std::string strToken; std::string strHuman; transResultInfo (rc.result (), strToken, strHuman); WriteLog (lsDEBUG, RPCHandler) << "ripple_path_find: " << strToken << " " << strHuman << " " << spsComputed.getJson (0); } } } // Each alternative differs by source currency. jvResult["alternatives"] = jvArray; } WriteLog (lsDEBUG, RPCHandler) << boost::str (boost::format ("ripple_path_find< %s") % jvResult); return jvResult; }
Json::Value PathRequest::doUpdate (RippleLineCache::ref cache, bool fast) { m_journal.debug << iIdentifier << " update " << (fast ? "fast" : "normal"); ScopedLockType sl (mLock); if (!isValid (cache)) return jvStatus; jvStatus = Json::objectValue; auto sourceCurrencies = sciSourceCurrencies; if (sourceCurrencies.empty ()) { auto usCurrencies = usAccountSourceCurrencies (raSrcAccount, cache, true); bool sameAccount = raSrcAccount == raDstAccount; for (auto const& c: usCurrencies) { if (!sameAccount || (c != saDstAmount.getCurrency ())) { if (c.isZero ()) sourceCurrencies.insert (std::make_pair (c, xrpAccount())); else sourceCurrencies.insert (std::make_pair (c, raSrcAccount.getAccountID ())); } } } jvStatus["source_account"] = raSrcAccount.humanAccountID (); jvStatus["destination_account"] = raDstAccount.humanAccountID (); jvStatus["destination_amount"] = saDstAmount.getJson (0); if (!jvId.isNull ()) jvStatus["id"] = jvId; Json::Value jvArray = Json::arrayValue; int iLevel = iLastLevel; bool loaded = getApp().getFeeTrack().isLoadedLocal(); if (iLevel == 0) { // first pass if (loaded || fast) iLevel = getConfig().PATH_SEARCH_FAST; else iLevel = getConfig().PATH_SEARCH; } else if ((iLevel == getConfig().PATH_SEARCH_FAST) && !fast) { // leaving fast pathfinding iLevel = getConfig().PATH_SEARCH; if (loaded && (iLevel > getConfig().PATH_SEARCH_FAST)) --iLevel; } else if (bLastSuccess) { // decrement, if possible if (iLevel > getConfig().PATH_SEARCH || (loaded && (iLevel > getConfig().PATH_SEARCH_FAST))) --iLevel; } else { // adjust as needed if (!loaded && (iLevel < getConfig().PATH_SEARCH_MAX)) ++iLevel; if (loaded && (iLevel > getConfig().PATH_SEARCH_FAST)) --iLevel; } m_journal.debug << iIdentifier << " processing at level " << iLevel; bool found = false; for (auto const& currIssuer: sourceCurrencies) { { STAmount test ({currIssuer.first, currIssuer.second}, 1); if (m_journal.debug) { m_journal.debug << iIdentifier << " Trying to find paths: " << test.getFullText (); } } bool valid; STPathSet& spsPaths = mContext[currIssuer]; Pathfinder pf (cache, raSrcAccount, raDstAccount, currIssuer.first, currIssuer.second, saDstAmount, valid); CondLog (!valid, lsDEBUG, PathRequest) << iIdentifier << " PF request not valid"; STPath extraPath; if (valid && pf.findPaths (iLevel, 4, spsPaths, extraPath)) { LedgerEntrySet lesSandbox (cache->getLedger (), tapNONE); PathState::List pathStateList; STAmount saMaxAmountAct; STAmount saDstAmountAct; auto& account = currIssuer.second.isNonZero () ? Account(currIssuer.second) : isXRP (currIssuer.first) ? xrpAccount() : raSrcAccount.getAccountID (); STAmount saMaxAmount ({currIssuer.first, account}, 1); saMaxAmount.negate (); m_journal.debug << iIdentifier << " Paths found, calling rippleCalc"; TER resultCode = path::rippleCalculate ( lesSandbox, saMaxAmountAct, saDstAmountAct, pathStateList, saMaxAmount, saDstAmount, raDstAccount.getAccountID (), raSrcAccount.getAccountID (), spsPaths, false, false, false, true); if ((extraPath.size() > 0) && ((resultCode == terNO_LINE) || (resultCode == tecPATH_PARTIAL))) { m_journal.debug << iIdentifier << " Trying with an extra path element"; spsPaths.addPath(extraPath); pathStateList.clear (); resultCode = path::rippleCalculate ( lesSandbox, saMaxAmountAct, saDstAmountAct, pathStateList, saMaxAmount, saDstAmount, raDstAccount.getAccountID (), raSrcAccount.getAccountID (), spsPaths, false, false, false, true); m_journal.debug << iIdentifier << " Extra path element gives " << transHuman (resultCode); } if (resultCode == tesSUCCESS) { Json::Value jvEntry (Json::objectValue); jvEntry["source_amount"] = saMaxAmountAct.getJson (0); jvEntry["paths_computed"] = spsPaths.getJson (0); found = true; jvArray.append (jvEntry); } else { m_journal.debug << iIdentifier << " rippleCalc returns " << transHuman (resultCode); } } else { m_journal.debug << iIdentifier << " No paths found"; } } iLastLevel = iLevel; bLastSuccess = found; if (fast && ptQuickReply.is_not_a_date_time()) { ptQuickReply = boost::posix_time::microsec_clock::universal_time(); mOwner.reportFast ((ptQuickReply-ptCreated).total_milliseconds()); } else if (!fast && ptFullReply.is_not_a_date_time()) { ptFullReply = boost::posix_time::microsec_clock::universal_time(); mOwner.reportFull ((ptFullReply-ptCreated).total_milliseconds()); } jvStatus["alternatives"] = jvArray; return jvStatus; }
// 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 PathCursor::forwardLiquidityForAccount () const { TER resultCode = tesSUCCESS; auto const lastNodeIndex = pathState_.nodes().size () - 1; auto viewJ = rippleCalc_.logs_.journal ("View"); std::uint64_t uRateMax = 0; AccountID const& previousAccountID = previousNode().isAccount() ? previousNode().account_ : node().account_; // Offers are always issue. AccountID const& nextAccountID = nextNode().isAccount() ? nextNode().account_ : node().account_; auto const qualityIn = nodeIndex_ ? quality_in (view(), node().account_, previousAccountID, node().issue_.currency) : parityRate; auto const qualityOut = (nodeIndex_ == lastNodeIndex) ? quality_out (view(), node().account_, nextAccountID, node().issue_.currency) : parityRate; // 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 nextNode().isAccount() auto saPrvRedeemAct = previousNode().saFwdRedeem.zeroed(); auto saPrvIssueAct = previousNode().saFwdIssue.zeroed(); // For !previousNode().isAccount() auto saPrvDeliverAct = previousNode().saFwdDeliver.zeroed (); JLOG (j_.trace()) << "forwardLiquidityForAccount> " << "nodeIndex_=" << nodeIndex_ << "/" << lastNodeIndex << " previousNode.saFwdRedeem:" << previousNode().saFwdRedeem << " saPrvIssueReq:" << previousNode().saFwdIssue << " previousNode.saFwdDeliver:" << previousNode().saFwdDeliver << " node.saRevRedeem:" << node().saRevRedeem << " node.saRevIssue:" << node().saRevIssue << " node.saRevDeliver:" << node().saRevDeliver; // Ripple through account. if (previousNode().isAccount() && nextNode().isAccount()) { // Next is an account, must be rippling. if (!nodeIndex_) { // ^ --> ACCOUNT --> account // For the first node, calculate amount to ripple based on what is // available. node().saFwdRedeem = node().saRevRedeem; if (pathState_.inReq() >= beast::zero) { // Limit by send max. node().saFwdRedeem = std::min ( node().saFwdRedeem, pathState_.inReq() - pathState_.inAct()); } pathState_.setInPass (node().saFwdRedeem); node().saFwdIssue = node().saFwdRedeem == node().saRevRedeem // Fully redeemed. ? node().saRevIssue : STAmount (node().saRevIssue); if (node().saFwdIssue && pathState_.inReq() >= beast::zero) { // Limit by send max. node().saFwdIssue = std::min ( node().saFwdIssue, pathState_.inReq() - pathState_.inAct() - node().saFwdRedeem); } pathState_.setInPass (pathState_.inPass() + node().saFwdIssue); JLOG (j_.trace()) << "forwardLiquidityForAccount: ^ --> " << "ACCOUNT --> account :" << " saInReq=" << pathState_.inReq() << " saInAct=" << pathState_.inAct() << " node.saFwdRedeem:" << node().saFwdRedeem << " node.saRevIssue:" << node().saRevIssue << " node.saFwdIssue:" << node().saFwdIssue << " pathState_.saInPass:"******"forwardLiquidityForAccount: account --> " << "ACCOUNT --> $ :" << " previousAccountID=" << to_string (previousAccountID) << " node.account_=" << to_string (node().account_) << " previousNode.saFwdRedeem:" << previousNode().saFwdRedeem << " previousNode.saFwdIssue:" << previousNode().saFwdIssue; // Last node. Accept all funds. Calculate amount actually to credit. auto& saCurReceive = pathState_.outPass(); STAmount saIssueCrd = qualityIn >= parityRate ? previousNode().saFwdIssue // No fee. : multiplyRound ( previousNode().saFwdIssue, qualityIn, true); // Amount to credit. // Amount to credit. Credit for less than received as a surcharge. pathState_.setOutPass (previousNode().saFwdRedeem + saIssueCrd); if (saCurReceive) { // Actually receive. resultCode = rippleCredit(view(), previousAccountID, node().account_, previousNode().saFwdRedeem + previousNode().saFwdIssue, false, viewJ); } else { // After applying quality, total payment was microscopic. resultCode = tecPATH_DRY; } } else { // account --> ACCOUNT --> account JLOG (j_.trace()) << "forwardLiquidityForAccount: account --> " << "ACCOUNT --> account"; node().saFwdRedeem.clear (node().saRevRedeem); node().saFwdIssue.clear (node().saRevIssue); // Previous redeem part 1: redeem -> redeem if (previousNode().saFwdRedeem && node().saRevRedeem) // Previous wants to redeem. { // Rate : 1.0 : quality out rippleLiquidity ( rippleCalc_, parityRate, qualityOut, previousNode().saFwdRedeem, node().saRevRedeem, saPrvRedeemAct, node().saFwdRedeem, uRateMax); } // Previous issue part 1: issue -> redeem if (previousNode().saFwdIssue != saPrvIssueAct // Previous wants to issue. && node().saRevRedeem != node().saFwdRedeem) // Current has more to redeem to next. { // Rate: quality in : quality out rippleLiquidity ( rippleCalc_, qualityIn, qualityOut, previousNode().saFwdIssue, node().saRevRedeem, saPrvIssueAct, node().saFwdRedeem, uRateMax); } // Previous redeem part 2: redeem -> issue. if (previousNode().saFwdRedeem != saPrvRedeemAct // Previous still wants to redeem. && node().saRevRedeem == node().saFwdRedeem // Current redeeming is done can issue. && node().saRevIssue) // Current wants to issue. { // Rate : 1.0 : transfer_rate rippleLiquidity ( rippleCalc_, parityRate, transferRate (view(), node().account_), previousNode().saFwdRedeem, node().saRevIssue, saPrvRedeemAct, node().saFwdIssue, uRateMax); } // Previous issue part 2 : issue -> issue if (previousNode().saFwdIssue != saPrvIssueAct // Previous wants to issue. && node().saRevRedeem == node().saFwdRedeem // Current redeeming is done can issue. && node().saRevIssue) // Current wants to issue. { // Rate: quality in : 1.0 rippleLiquidity ( rippleCalc_, qualityIn, parityRate, previousNode().saFwdIssue, node().saRevIssue, saPrvIssueAct, node().saFwdIssue, uRateMax); } STAmount saProvide = node().saFwdRedeem + node().saFwdIssue; // Adjust prv --> cur balance : take all inbound resultCode = saProvide ? rippleCredit(view(), previousAccountID, node().account_, previousNode().saFwdRedeem + previousNode().saFwdIssue, false, viewJ) : tecPATH_DRY; } } else if (previousNode().isAccount() && !nextNode().isAccount()) { // 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 (nodeIndex_) { // Non-XRP, current node is the issuer. JLOG (j_.trace()) << "forwardLiquidityForAccount: account --> " << "ACCOUNT --> offer"; node().saFwdDeliver.clear (node().saRevDeliver); // redeem -> issue/deliver. // Previous wants to redeem. // Current is issuing to an offer so leave funds in account as // "limbo". if (previousNode().saFwdRedeem) // Previous wants to redeem. { // Rate : 1.0 : transfer_rate // XXX Is having the transfer rate here correct? rippleLiquidity ( rippleCalc_, parityRate, transferRate (view(), node().account_), previousNode().saFwdRedeem, node().saRevDeliver, saPrvRedeemAct, node().saFwdDeliver, uRateMax); } // issue -> issue/deliver if (previousNode().saFwdRedeem == saPrvRedeemAct // Previous done redeeming: Previous has no IOUs. && previousNode().saFwdIssue) // Previous wants to issue. To next must be ok. { // Rate: quality in : 1.0 rippleLiquidity ( rippleCalc_, qualityIn, parityRate, previousNode().saFwdIssue, node().saRevDeliver, saPrvIssueAct, node().saFwdDeliver, uRateMax); } // Adjust prv --> cur balance : take all inbound resultCode = node().saFwdDeliver ? rippleCredit(view(), previousAccountID, node().account_, previousNode().saFwdRedeem + previousNode().saFwdIssue, false, viewJ) : tecPATH_DRY; // Didn't actually deliver anything. } else { // Delivering amount requested from downstream. node().saFwdDeliver = node().saRevDeliver; // If limited, then limit by send max and available. if (pathState_.inReq() >= beast::zero) { // Limit by send max. node().saFwdDeliver = std::min ( node().saFwdDeliver, pathState_.inReq() - pathState_.inAct()); // Limit XRP by available. No limit for non-XRP as issuer. if (isXRP (node().issue_)) node().saFwdDeliver = std::min ( node().saFwdDeliver, accountHolds(view(), node().account_, xrpCurrency(), xrpAccount(), fhIGNORE_FREEZE, viewJ)); // XRP can't be frozen } // Record amount sent for pass. pathState_.setInPass (node().saFwdDeliver); if (!node().saFwdDeliver) { resultCode = tecPATH_DRY; } else if (!isXRP (node().issue_)) { // 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. JLOG (j_.trace()) << "forwardLiquidityForAccount: ^ --> " << "ACCOUNT -- !XRP --> offer"; // As the issuer, would only issue. // Don't need to actually deliver. As from delivering leave in // the issuer as limbo. } else { JLOG (j_.trace()) << "forwardLiquidityForAccount: ^ --> " << "ACCOUNT -- XRP --> offer"; // Deliver XRP to limbo. resultCode = accountSend(view(), node().account_, xrpAccount(), node().saFwdDeliver, viewJ); } } } else if (!previousNode().isAccount() && nextNode().isAccount()) { if (nodeIndex_ == lastNodeIndex) { // offer --> ACCOUNT --> $ JLOG (j_.trace()) << "forwardLiquidityForAccount: offer --> " << "ACCOUNT --> $ : " << previousNode().saFwdDeliver; // Amount to credit. pathState_.setOutPass (previousNode().saFwdDeliver); // No income balance adjustments necessary. The paying side inside // the offer paid to this account. } else { // offer --> ACCOUNT --> account JLOG (j_.trace()) << "forwardLiquidityForAccount: offer --> " << "ACCOUNT --> account"; node().saFwdRedeem.clear (node().saRevRedeem); node().saFwdIssue.clear (node().saRevIssue); // deliver -> redeem if (previousNode().saFwdDeliver && node().saRevRedeem) // Previous wants to deliver and can current redeem. { // Rate : 1.0 : quality out rippleLiquidity ( rippleCalc_, parityRate, qualityOut, previousNode().saFwdDeliver, node().saRevRedeem, saPrvDeliverAct, node().saFwdRedeem, uRateMax); } // deliver -> issue // Wants to redeem and current would and can issue. if (previousNode().saFwdDeliver != saPrvDeliverAct // Previous still wants to deliver. && node().saRevRedeem == node().saFwdRedeem // Current has more to redeem to next. && node().saRevIssue) // Current wants issue. { // Rate : 1.0 : transfer_rate rippleLiquidity ( rippleCalc_, parityRate, transferRate (view(), node().account_), previousNode().saFwdDeliver, node().saRevIssue, saPrvDeliverAct, node().saFwdIssue, uRateMax); } // No income balance adjustments necessary. The paying side inside // the offer paid and the next link will receive. STAmount saProvide = node().saFwdRedeem + node().saFwdIssue; if (!saProvide) resultCode = tecPATH_DRY; } } else { // offer --> ACCOUNT --> offer // deliver/redeem -> deliver/issue. JLOG (j_.trace()) << "forwardLiquidityForAccount: offer --> ACCOUNT --> offer"; node().saFwdDeliver.clear (node().saRevDeliver); if (previousNode().saFwdDeliver && node().saRevDeliver) { // Rate : 1.0 : transfer_rate rippleLiquidity ( rippleCalc_, parityRate, transferRate (view(), node().account_), previousNode().saFwdDeliver, node().saRevDeliver, saPrvDeliverAct, node().saFwdDeliver, uRateMax); } // No income balance adjustments necessary. The paying side inside the // offer paid and the next link will receive. if (!node().saFwdDeliver) resultCode = tecPATH_DRY; } return resultCode; }