mirror of
https://github.com/Xahau/xahaud.git
synced 2025-12-06 17:27:52 +00:00
Member functions and free functions on Ledger and LedgerEntrySet are rewritten in terms of new abstract interfaces `BasicView` and `View`, representing the set of non-decomposable primitives necessary to read and write state map items in a ledger, and to overlay a discardable view onto a Ledger that can calculate metadata during transaction processing. const-correctness is enforced through the parameter and return types. The MetaView now supports multi-level stacking: A MetaView can be stacked on top of either a Ledger or another MetaView, up to any number of levels. The getSLEi member function is removed. The CachedView wrapper replaces it, wrapping a View such that any function called with a CachedView will go through the SLECache. * Add BasicView, View, CachedView * Rename LedgerEntrySet to MetaView * Factor out free functions * Consolidate free functions in ViewAPI * Remove unused class members and free functions
875 lines
31 KiB
C++
875 lines
31 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2012, 2013 Ripple Labs Inc.
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
copyright notice and this permission notice appear in all copies.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#include <BeastConfig.h>
|
|
#include <ripple/app/tx/impl/OfferStream.h>
|
|
#include <ripple/app/tx/impl/Taker.h>
|
|
#include <ripple/app/tx/impl/Transactor.h>
|
|
#include <ripple/ledger/ViewAPI.h>
|
|
#include <ripple/protocol/Quality.h>
|
|
#include <ripple/basics/Log.h>
|
|
#include <ripple/json/to_string.h>
|
|
#include <beast/cxx14/memory.h>
|
|
#include <beast/utility/Journal.h>
|
|
#include <beast/utility/WrappedSink.h>
|
|
#include <stdexcept>
|
|
|
|
namespace ripple {
|
|
|
|
class CreateOffer
|
|
: public Transactor
|
|
{
|
|
private:
|
|
// What kind of offer we are placing
|
|
CrossType cross_type_;
|
|
|
|
/** Determine if we are authorized to hold the asset we want to get */
|
|
TER
|
|
checkAcceptAsset(IssueRef issue) const
|
|
{
|
|
// Only valid for custom currencies
|
|
assert (!isXRP (issue.currency));
|
|
|
|
auto const issuerAccount = mEngine->view().read(
|
|
keylet::account(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)
|
|
{
|
|
auto const trustLine = mEngine->view().read(
|
|
keylet::line(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;
|
|
}
|
|
|
|
static
|
|
bool
|
|
dry_offer (View& view, Offer const& offer)
|
|
{
|
|
if (offer.fully_consumed ())
|
|
return true;
|
|
auto const amount = accountFunds(view, offer.owner(),
|
|
offer.amount().out, fhZERO_IF_FROZEN, getConfig());
|
|
return (amount <= zero);
|
|
}
|
|
|
|
static
|
|
std::pair<bool, Quality>
|
|
select_path (
|
|
bool have_direct, OfferStream const& direct,
|
|
bool have_bridge, OfferStream const& leg1, OfferStream const& leg2)
|
|
{
|
|
// If we don't have any viable path, why are we here?!
|
|
assert (have_direct || have_bridge);
|
|
|
|
// If there's no bridged path, the direct is the best by default.
|
|
if (!have_bridge)
|
|
return std::make_pair (true, direct.tip ().quality ());
|
|
|
|
Quality const bridged_quality (composed_quality (
|
|
leg1.tip ().quality (), leg2.tip ().quality ()));
|
|
|
|
if (have_direct)
|
|
{
|
|
// We compare the quality of the composed quality of the bridged
|
|
// offers and compare it against the direct offer to pick the best.
|
|
Quality const direct_quality (direct.tip ().quality ());
|
|
|
|
if (bridged_quality < direct_quality)
|
|
return std::make_pair (true, direct_quality);
|
|
}
|
|
|
|
// Either there was no direct offer, or it didn't have a better quality
|
|
// than the bridge.
|
|
return std::make_pair (false, bridged_quality);
|
|
}
|
|
|
|
std::pair<TER, Amounts>
|
|
bridged_cross (
|
|
Taker& taker,
|
|
View& view,
|
|
View& view_cancel,
|
|
Clock::time_point const when)
|
|
{
|
|
auto const& taker_amount = taker.original_offer ();
|
|
|
|
assert (!isXRP (taker_amount.in) && !isXRP (taker_amount.out));
|
|
|
|
if (isXRP (taker_amount.in) || isXRP (taker_amount.out))
|
|
throw std::logic_error ("Bridging with XRP and an endpoint.");
|
|
|
|
OfferStream offers_direct (view, view_cancel,
|
|
Book (taker.issue_in (), taker.issue_out ()), when, m_journal);
|
|
|
|
OfferStream offers_leg1 (view, view_cancel,
|
|
Book (taker.issue_in (), xrpIssue ()), when, m_journal);
|
|
|
|
OfferStream offers_leg2 (view, view_cancel,
|
|
Book (xrpIssue (), taker.issue_out ()), when, m_journal);
|
|
|
|
TER cross_result = tesSUCCESS;
|
|
|
|
// Note the subtle distinction here: self-offers encountered in the
|
|
// bridge are taken, but self-offers encountered in the direct book
|
|
// are not.
|
|
bool have_bridge = offers_leg1.step () && offers_leg2.step ();
|
|
bool have_direct = step_account (offers_direct, taker);
|
|
int count = 0;
|
|
|
|
// Modifying the order or logic of the operations in the loop will cause
|
|
// a protocol breaking change.
|
|
while (have_direct || have_bridge)
|
|
{
|
|
bool leg1_consumed = false;
|
|
bool leg2_consumed = false;
|
|
bool direct_consumed = false;
|
|
|
|
Quality quality;
|
|
bool use_direct;
|
|
|
|
std::tie (use_direct, quality) = select_path (
|
|
have_direct, offers_direct,
|
|
have_bridge, offers_leg1, offers_leg2);
|
|
|
|
// We are always looking at the best quality; we are done with
|
|
// crossing as soon as we cross the quality boundary.
|
|
if (taker.reject(quality))
|
|
break;
|
|
|
|
count++;
|
|
|
|
if (use_direct)
|
|
{
|
|
if (m_journal.debug)
|
|
{
|
|
m_journal.debug << count << " Direct:";
|
|
m_journal.debug << " offer: " << offers_direct.tip ();
|
|
m_journal.debug << " in: " << offers_direct.tip ().amount().in;
|
|
m_journal.debug << " out: " << offers_direct.tip ().amount ().out;
|
|
m_journal.debug << " owner: " << offers_direct.tip ().owner ();
|
|
m_journal.debug << " funds: " << accountFunds(view,
|
|
offers_direct.tip ().owner (),
|
|
offers_direct.tip ().amount ().out,
|
|
fhIGNORE_FREEZE,
|
|
getConfig());
|
|
}
|
|
|
|
cross_result = taker.cross(offers_direct.tip ());
|
|
|
|
m_journal.debug << "Direct Result: " << transToken (cross_result);
|
|
|
|
if (dry_offer (view, offers_direct.tip ()))
|
|
{
|
|
direct_consumed = true;
|
|
have_direct = step_account (offers_direct, taker);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (m_journal.debug)
|
|
{
|
|
auto const owner1_funds_before = accountFunds(view,
|
|
offers_leg1.tip ().owner (),
|
|
offers_leg1.tip ().amount ().out,
|
|
fhIGNORE_FREEZE,
|
|
getConfig());
|
|
|
|
auto const owner2_funds_before = accountFunds(view,
|
|
offers_leg2.tip ().owner (),
|
|
offers_leg2.tip ().amount ().out,
|
|
fhIGNORE_FREEZE,
|
|
getConfig());
|
|
|
|
m_journal.debug << count << " Bridge:";
|
|
m_journal.debug << " offer1: " << offers_leg1.tip ();
|
|
m_journal.debug << " in: " << offers_leg1.tip ().amount().in;
|
|
m_journal.debug << " out: " << offers_leg1.tip ().amount ().out;
|
|
m_journal.debug << " owner: " << offers_leg1.tip ().owner ();
|
|
m_journal.debug << " funds: " << owner1_funds_before;
|
|
m_journal.debug << " offer2: " << offers_leg2.tip ();
|
|
m_journal.debug << " in: " << offers_leg2.tip ().amount ().in;
|
|
m_journal.debug << " out: " << offers_leg2.tip ().amount ().out;
|
|
m_journal.debug << " owner: " << offers_leg2.tip ().owner ();
|
|
m_journal.debug << " funds: " << owner2_funds_before;
|
|
}
|
|
|
|
cross_result = taker.cross(offers_leg1.tip (), offers_leg2.tip ());
|
|
|
|
m_journal.debug << "Bridge Result: " << transToken (cross_result);
|
|
|
|
if (dry_offer (view, offers_leg1.tip ()))
|
|
{
|
|
leg1_consumed = true;
|
|
have_bridge = (have_bridge && offers_leg1.step ());
|
|
}
|
|
if (dry_offer (view, offers_leg2.tip ()))
|
|
{
|
|
leg2_consumed = true;
|
|
have_bridge = (have_bridge && offers_leg2.step ());
|
|
}
|
|
}
|
|
|
|
if (cross_result != tesSUCCESS)
|
|
{
|
|
cross_result = tecFAILED_PROCESSING;
|
|
break;
|
|
}
|
|
|
|
if (taker.done())
|
|
{
|
|
m_journal.debug << "The taker reports he's done during crossing!";
|
|
break;
|
|
}
|
|
|
|
// Postcondition: If we aren't done, then we *must* have consumed at
|
|
// least one offer fully.
|
|
assert (direct_consumed || leg1_consumed || leg2_consumed);
|
|
|
|
if (!direct_consumed && !leg1_consumed && !leg2_consumed)
|
|
throw std::logic_error ("bridged crossing: nothing was fully consumed.");
|
|
}
|
|
|
|
return std::make_pair(cross_result, taker.remaining_offer ());
|
|
}
|
|
|
|
std::pair<TER, Amounts>
|
|
direct_cross (
|
|
Taker& taker,
|
|
View& view,
|
|
View& view_cancel,
|
|
Clock::time_point const when)
|
|
{
|
|
OfferStream offers (
|
|
view, view_cancel,
|
|
Book (taker.issue_in (), taker.issue_out ()),
|
|
when, m_journal);
|
|
|
|
TER cross_result (tesSUCCESS);
|
|
int count = 0;
|
|
|
|
bool have_offer = step_account (offers, taker);
|
|
|
|
// Modifying the order or logic of the operations in the loop will cause
|
|
// a protocol breaking change.
|
|
while (have_offer)
|
|
{
|
|
bool direct_consumed = false;
|
|
auto const& offer (offers.tip());
|
|
|
|
// We are done with crossing as soon as we cross the quality boundary
|
|
if (taker.reject (offer.quality()))
|
|
break;
|
|
|
|
count++;
|
|
|
|
if (m_journal.debug)
|
|
{
|
|
m_journal.debug << count << " Direct:";
|
|
m_journal.debug << " offer: " << offer;
|
|
m_journal.debug << " in: " << offer.amount ().in;
|
|
m_journal.debug << " out: " << offer.amount ().out;
|
|
m_journal.debug << " owner: " << offer.owner ();
|
|
m_journal.debug << " funds: " << accountFunds(view,
|
|
offer.owner (), offer.amount ().out, fhIGNORE_FREEZE, getConfig());
|
|
}
|
|
|
|
cross_result = taker.cross (offer);
|
|
|
|
m_journal.debug << "Direct Result: " << transToken (cross_result);
|
|
|
|
if (dry_offer (view, offer))
|
|
{
|
|
direct_consumed = true;
|
|
have_offer = step_account (offers, taker);
|
|
}
|
|
|
|
if (cross_result != tesSUCCESS)
|
|
{
|
|
cross_result = tecFAILED_PROCESSING;
|
|
break;
|
|
}
|
|
|
|
if (taker.done())
|
|
{
|
|
m_journal.debug << "The taker reports he's done during crossing!";
|
|
break;
|
|
}
|
|
|
|
// Postcondition: If we aren't done, then we *must* have consumed the
|
|
// offer on the books fully!
|
|
assert (direct_consumed);
|
|
|
|
if (!direct_consumed)
|
|
throw std::logic_error ("direct crossing: nothing was fully consumed.");
|
|
}
|
|
|
|
return std::make_pair(cross_result, taker.remaining_offer ());
|
|
}
|
|
|
|
// Step through the stream for as long as possible, skipping any offers
|
|
// that are from the taker or which cross the taker's threshold.
|
|
// Return false if the is no offer in the book, true otherwise.
|
|
static
|
|
bool
|
|
step_account (OfferStream& stream, Taker const& taker)
|
|
{
|
|
while (stream.step ())
|
|
{
|
|
auto const& offer = stream.tip ();
|
|
|
|
// This offer at the tip crosses the taker's threshold. We're done.
|
|
if (taker.reject (offer.quality ()))
|
|
return true;
|
|
|
|
// This offer at the tip is not from the taker. We're done.
|
|
if (offer.owner () != taker.account ())
|
|
return true;
|
|
}
|
|
|
|
// We ran out of offers. Can't advance.
|
|
return false;
|
|
}
|
|
|
|
// Fill offer as much as possible by consuming offers already on the books,
|
|
// and adjusting account balances accordingly.
|
|
//
|
|
// Charges fees on top to taker.
|
|
std::pair<TER, Amounts>
|
|
cross (
|
|
View& view,
|
|
View& cancel_view,
|
|
Amounts const& taker_amount)
|
|
{
|
|
Clock::time_point const when (
|
|
mEngine->getLedger ()->getParentCloseTimeNC ());
|
|
|
|
beast::WrappedSink takerSink (m_journal, "Taker ");
|
|
|
|
Taker taker (cross_type_, view, mTxnAccountID, taker_amount,
|
|
mTxn.getFlags(), beast::Journal (takerSink));
|
|
|
|
try
|
|
{
|
|
if (cross_type_ == CrossType::IouToIou)
|
|
return bridged_cross (taker, view, cancel_view, when);
|
|
|
|
return direct_cross (taker, view, cancel_view, when);
|
|
}
|
|
catch (std::exception const& e)
|
|
{
|
|
m_journal.error << "Exception during offer crossing: " << e.what ();
|
|
return std::make_pair (tecINTERNAL, taker.remaining_offer ());
|
|
}
|
|
catch (...)
|
|
{
|
|
m_journal.error << "Exception during offer crossing.";
|
|
return std::make_pair (tecINTERNAL, taker.remaining_offer ());
|
|
}
|
|
}
|
|
|
|
static
|
|
std::string
|
|
format_amount (STAmount const& amount)
|
|
{
|
|
std::string txt = amount.getText ();
|
|
txt += "/";
|
|
txt += to_string (amount.issue().currency);
|
|
return txt;
|
|
}
|
|
|
|
public:
|
|
CreateOffer (
|
|
CrossType cross_type,
|
|
STTx const& txn,
|
|
TransactionEngineParams params,
|
|
TransactionEngine* engine)
|
|
: Transactor (
|
|
txn,
|
|
params,
|
|
engine,
|
|
deprecatedLogs().journal("CreateOffer"))
|
|
, cross_type_ (cross_type)
|
|
{
|
|
|
|
}
|
|
|
|
/** Returns the reserve the account would have if an offer was added. */
|
|
STAmount
|
|
getAccountReserve (SLE::pointer account)
|
|
{
|
|
return STAmount (mEngine->getLedger ()->getReserve (
|
|
account->getFieldU32 (sfOwnerCount) + 1));
|
|
}
|
|
|
|
TER
|
|
preCheck () override
|
|
{
|
|
std::uint32_t const uTxFlags = mTxn.getFlags ();
|
|
|
|
if (uTxFlags & tfOfferCreateMask)
|
|
{
|
|
if (m_journal.debug) m_journal.debug <<
|
|
"Malformed transaction: Invalid flags set.";
|
|
return temINVALID_FLAG;
|
|
}
|
|
|
|
bool const bImmediateOrCancel (uTxFlags & tfImmediateOrCancel);
|
|
bool const bFillOrKill (uTxFlags & tfFillOrKill);
|
|
|
|
if (bImmediateOrCancel && bFillOrKill)
|
|
{
|
|
if (m_journal.debug) m_journal.debug <<
|
|
"Malformed transaction: both IoC and FoK set.";
|
|
return temINVALID_FLAG;
|
|
}
|
|
|
|
bool const bHaveExpiration (mTxn.isFieldPresent (sfExpiration));
|
|
|
|
if (bHaveExpiration && (mTxn.getFieldU32 (sfExpiration) == 0))
|
|
{
|
|
if (m_journal.debug) m_journal.warning <<
|
|
"Malformed offer: bad expiration";
|
|
return temBAD_EXPIRATION;
|
|
}
|
|
|
|
bool const bHaveCancel (mTxn.isFieldPresent (sfOfferSequence));
|
|
|
|
if (bHaveCancel && (mTxn.getFieldU32 (sfOfferSequence) == 0))
|
|
{
|
|
if (m_journal.debug) m_journal.debug <<
|
|
"Malformed offer: bad cancel sequence";
|
|
return temBAD_SEQUENCE;
|
|
}
|
|
|
|
STAmount saTakerPays = mTxn.getFieldAmount (sfTakerPays);
|
|
STAmount saTakerGets = mTxn.getFieldAmount (sfTakerGets);
|
|
|
|
if (!isLegalNet (saTakerPays) || !isLegalNet (saTakerGets))
|
|
return temBAD_AMOUNT;
|
|
|
|
if (saTakerPays.native () && saTakerGets.native ())
|
|
{
|
|
if (m_journal.debug) m_journal.warning <<
|
|
"Malformed offer: XRP for XRP";
|
|
return temBAD_OFFER;
|
|
}
|
|
if (saTakerPays <= zero || saTakerGets <= zero)
|
|
{
|
|
if (m_journal.debug) m_journal.warning <<
|
|
"Malformed offer: bad amount";
|
|
return temBAD_OFFER;
|
|
}
|
|
|
|
auto const& uPaysIssuerID = saTakerPays.getIssuer ();
|
|
auto const& uPaysCurrency = saTakerPays.getCurrency ();
|
|
|
|
auto const& uGetsIssuerID = saTakerGets.getIssuer ();
|
|
auto const& uGetsCurrency = saTakerGets.getCurrency ();
|
|
|
|
if (uPaysCurrency == uGetsCurrency && uPaysIssuerID == uGetsIssuerID)
|
|
{
|
|
if (m_journal.debug) m_journal.debug <<
|
|
"Malformed offer: redundant offer";
|
|
return temREDUNDANT;
|
|
}
|
|
// We don't allow a non-native currency to use the currency code XRP.
|
|
if (badCurrency() == uPaysCurrency || badCurrency() == uGetsCurrency)
|
|
{
|
|
if (m_journal.debug) m_journal.warning <<
|
|
"Malformed offer: Bad currency.";
|
|
return temBAD_CURRENCY;
|
|
}
|
|
|
|
if (saTakerPays.native () != !uPaysIssuerID ||
|
|
saTakerGets.native () != !uGetsIssuerID)
|
|
{
|
|
if (m_journal.warning) m_journal.warning <<
|
|
"Malformed offer: bad issuer";
|
|
return temBAD_ISSUER;
|
|
}
|
|
|
|
return Transactor::preCheck ();
|
|
}
|
|
|
|
std::pair<TER, bool>
|
|
applyGuts (View& view, View& view_cancel)
|
|
{
|
|
std::uint32_t const uTxFlags = mTxn.getFlags ();
|
|
|
|
bool const bPassive (uTxFlags & tfPassive);
|
|
bool const bImmediateOrCancel (uTxFlags & tfImmediateOrCancel);
|
|
bool const bFillOrKill (uTxFlags & tfFillOrKill);
|
|
bool const bSell (uTxFlags & tfSell);
|
|
|
|
STAmount saTakerPays = mTxn.getFieldAmount (sfTakerPays);
|
|
STAmount saTakerGets = mTxn.getFieldAmount (sfTakerGets);
|
|
|
|
if (!isLegalNet (saTakerPays) || !isLegalNet (saTakerGets))
|
|
return { temBAD_AMOUNT, true };
|
|
|
|
auto const& uPaysIssuerID = saTakerPays.getIssuer ();
|
|
auto const& uPaysCurrency = saTakerPays.getCurrency ();
|
|
|
|
auto const& uGetsIssuerID = saTakerGets.getIssuer ();
|
|
|
|
bool const bHaveExpiration (mTxn.isFieldPresent (sfExpiration));
|
|
bool const bHaveCancel (mTxn.isFieldPresent (sfOfferSequence));
|
|
|
|
std::uint32_t const uExpiration = mTxn.getFieldU32 (sfExpiration);
|
|
std::uint32_t const uCancelSequence = mTxn.getFieldU32 (sfOfferSequence);
|
|
|
|
// FIXME understand why we use SequenceNext instead of current transaction
|
|
// sequence to determine the transaction. Why is the offer sequence
|
|
// number insufficient?
|
|
|
|
std::uint32_t const uAccountSequenceNext = mTxnAccount->getFieldU32 (sfSequence);
|
|
std::uint32_t const uSequence = mTxn.getSequence ();
|
|
|
|
// This is the original rate of the offer, and is the rate at which
|
|
// it will be placed, even if crossing offers change the amounts that
|
|
// end up on the books.
|
|
std::uint64_t const uRate = getRate (saTakerGets, saTakerPays);
|
|
|
|
TER result = tesSUCCESS;
|
|
|
|
// This is the ledger view that we work against. Transactions are applied
|
|
// as we go on processing transactions.
|
|
|
|
auto const sleCreator = view.peek (
|
|
keylet::account(mTxnAccountID));
|
|
|
|
if (isGlobalFrozen (view, uPaysIssuerID) || isGlobalFrozen (view, uGetsIssuerID))
|
|
{
|
|
if (m_journal.warning) m_journal.warning <<
|
|
"Offer involves frozen asset";
|
|
|
|
result = tecFROZEN;
|
|
}
|
|
else if (accountFunds(view, mTxnAccountID, saTakerGets,
|
|
fhZERO_IF_FROZEN, getConfig()) <= zero)
|
|
{
|
|
if (m_journal.debug) m_journal.debug <<
|
|
"delay: Offers must be at least partially funded.";
|
|
|
|
result = tecUNFUNDED_OFFER;
|
|
}
|
|
// This can probably be simplified to make sure that you cancel sequences
|
|
// before the transaction sequence number.
|
|
else if (bHaveCancel && (uAccountSequenceNext - 1 <= uCancelSequence))
|
|
{
|
|
if (m_journal.debug) m_journal.debug <<
|
|
"uAccountSequenceNext=" << uAccountSequenceNext <<
|
|
" uOfferSequence=" << uCancelSequence;
|
|
|
|
result = temBAD_SEQUENCE;
|
|
}
|
|
|
|
if (result != tesSUCCESS)
|
|
{
|
|
m_journal.debug << "final result: " << transToken (result);
|
|
return { result, true };
|
|
}
|
|
|
|
// Process a cancellation request that's passed along with an offer.
|
|
if (bHaveCancel)
|
|
{
|
|
auto const sleCancel = view.peek(
|
|
keylet::offer(mTxnAccountID, uCancelSequence));
|
|
|
|
// It's not an error to not find the offer to cancel: it might have
|
|
// been consumed or removed. If it is found, however, it's an error
|
|
// to fail to delete it.
|
|
if (sleCancel)
|
|
{
|
|
m_journal.debug << "Create cancels order " << uCancelSequence;
|
|
result = offerDelete (view, sleCancel);
|
|
}
|
|
}
|
|
|
|
// Expiration is defined in terms of the close time of the parent ledger,
|
|
// because we definitively know the time that it closed but we do not
|
|
// know the closing time of the ledger that is under construction.
|
|
if (bHaveExpiration &&
|
|
(mEngine->getLedger ()->getParentCloseTimeNC () >= uExpiration))
|
|
{
|
|
return { tesSUCCESS, true };
|
|
}
|
|
|
|
// Make sure that we are authorized to hold what the taker will pay us.
|
|
if (result == tesSUCCESS && !saTakerPays.native ())
|
|
result = checkAcceptAsset (Issue (uPaysCurrency, uPaysIssuerID));
|
|
|
|
bool const bOpenLedger (mParams & tapOPEN_LEDGER);
|
|
bool crossed = false;
|
|
|
|
if (result == tesSUCCESS)
|
|
{
|
|
// We reverse pays and gets because during crossing we are taking.
|
|
Amounts const taker_amount (saTakerGets, saTakerPays);
|
|
|
|
// The amount of the offer that is unfilled after crossing has been
|
|
// performed. It may be equal to the original amount (didn't cross),
|
|
// empty (fully crossed), or something in-between.
|
|
Amounts place_offer;
|
|
|
|
m_journal.debug << "Attempting cross: " <<
|
|
to_string (taker_amount.in.issue ()) << " -> " <<
|
|
to_string (taker_amount.out.issue ());
|
|
|
|
if (m_journal.trace)
|
|
{
|
|
m_journal.debug << " mode: " <<
|
|
(bPassive ? "passive " : "") <<
|
|
(bSell ? "sell" : "buy");
|
|
m_journal.trace <<" in: " << format_amount (taker_amount.in);
|
|
m_journal.trace << " out: " << format_amount (taker_amount.out);
|
|
}
|
|
|
|
std::tie(result, place_offer) = cross (view, view_cancel, taker_amount);
|
|
assert (result != tefINTERNAL);
|
|
|
|
if (m_journal.trace)
|
|
{
|
|
m_journal.trace << "Cross result: " << transToken (result);
|
|
m_journal.trace << " in: " << format_amount (place_offer.in);
|
|
m_journal.trace << " out: " << format_amount (place_offer.out);
|
|
}
|
|
|
|
if (result == tecFAILED_PROCESSING && bOpenLedger)
|
|
result = telFAILED_PROCESSING;
|
|
|
|
if (result != tesSUCCESS)
|
|
{
|
|
m_journal.debug << "final result: " << transToken (result);
|
|
return { result, true };
|
|
}
|
|
|
|
assert (saTakerGets.issue () == place_offer.in.issue ());
|
|
assert (saTakerPays.issue () == place_offer.out.issue ());
|
|
|
|
if (taker_amount != place_offer)
|
|
crossed = true;
|
|
|
|
// The offer that we need to place after offer crossing should
|
|
// never be negative. If it is, something went very very wrong.
|
|
if (place_offer.in < zero || place_offer.out < zero)
|
|
{
|
|
m_journal.fatal << "Cross left offer negative!" <<
|
|
" in: " << format_amount (place_offer.in) <<
|
|
" out: " << format_amount (place_offer.out);
|
|
return { tefINTERNAL, true };
|
|
}
|
|
|
|
if (place_offer.in == zero || place_offer.out == zero)
|
|
{
|
|
m_journal.debug << "Offer fully crossed!";
|
|
return { result, true };
|
|
}
|
|
|
|
// We now need to adjust the offer to reflect the amount left after
|
|
// crossing. We reverse in and out here, since during crossing we
|
|
// were the taker.
|
|
saTakerPays = place_offer.out;
|
|
saTakerGets = place_offer.in;
|
|
}
|
|
|
|
assert (saTakerPays > zero && saTakerGets > zero);
|
|
|
|
if (result != tesSUCCESS)
|
|
{
|
|
m_journal.debug << "final result: " << transToken (result);
|
|
return { result, true };
|
|
}
|
|
|
|
if (m_journal.trace)
|
|
{
|
|
m_journal.trace << "Place" << (crossed ? " remaining " : " ") << "offer:";
|
|
m_journal.trace << " Pays: " << saTakerPays.getFullText ();
|
|
m_journal.trace << " Gets: " << saTakerGets.getFullText ();
|
|
}
|
|
|
|
// For 'fill or kill' offers, failure to fully cross means that the
|
|
// entire operation should be aborted, with only fees paid.
|
|
if (bFillOrKill)
|
|
{
|
|
m_journal.trace << "Fill or Kill: offer killed";
|
|
return { tesSUCCESS, false };
|
|
}
|
|
|
|
// For 'immediate or cancel' offers, the amount remaining doesn't get
|
|
// placed - it gets cancelled and the operation succeeds.
|
|
if (bImmediateOrCancel)
|
|
{
|
|
m_journal.trace << "Immediate or cancel: offer cancelled";
|
|
return { tesSUCCESS, true };
|
|
}
|
|
|
|
if (mPriorBalance < getAccountReserve (sleCreator))
|
|
{
|
|
// If we are here, the signing account had an insufficient reserve
|
|
// *prior* to our processing. If something actually crossed, then
|
|
// we allow this; otherwise, we just claim a fee.
|
|
if (!crossed)
|
|
result = tecINSUF_RESERVE_OFFER;
|
|
|
|
if (result != tesSUCCESS)
|
|
m_journal.debug << "final result: " << transToken (result);
|
|
|
|
return { result, true };
|
|
}
|
|
|
|
// We need to place the remainder of the offer into its order book.
|
|
auto const offer_index = getOfferIndex (mTxnAccountID, uSequence);
|
|
|
|
std::uint64_t uOwnerNode;
|
|
std::uint64_t uBookNode;
|
|
uint256 uDirectory;
|
|
|
|
// Add offer to owner's directory.
|
|
result = dirAdd(view, uOwnerNode,
|
|
getOwnerDirIndex (mTxnAccountID), offer_index,
|
|
std::bind (
|
|
&ownerDirDescriber, std::placeholders::_1,
|
|
std::placeholders::_2, mTxnAccountID));
|
|
|
|
if (result == tesSUCCESS)
|
|
{
|
|
// Update owner count.
|
|
adjustOwnerCount(view, sleCreator, 1);
|
|
|
|
if (m_journal.trace) m_journal.trace <<
|
|
"adding to book: " << to_string (saTakerPays.issue ()) <<
|
|
" : " << to_string (saTakerGets.issue ());
|
|
|
|
uint256 const book_base (getBookBase (
|
|
{ saTakerPays.issue (), saTakerGets.issue () }));
|
|
|
|
// We use the original rate to place the offer.
|
|
uDirectory = getQualityIndex (book_base, uRate);
|
|
|
|
// Add offer to order book.
|
|
result = dirAdd (view, uBookNode, uDirectory, offer_index,
|
|
std::bind (
|
|
&qualityDirDescriber, std::placeholders::_1,
|
|
std::placeholders::_2, saTakerPays.getCurrency (),
|
|
uPaysIssuerID, saTakerGets.getCurrency (),
|
|
uGetsIssuerID, uRate));
|
|
}
|
|
|
|
if (result == tesSUCCESS)
|
|
{
|
|
auto sleOffer = std::make_shared<SLE>(ltOFFER, offer_index);
|
|
sleOffer->setFieldAccount (sfAccount, mTxnAccountID);
|
|
sleOffer->setFieldU32 (sfSequence, uSequence);
|
|
sleOffer->setFieldH256 (sfBookDirectory, uDirectory);
|
|
sleOffer->setFieldAmount (sfTakerPays, saTakerPays);
|
|
sleOffer->setFieldAmount (sfTakerGets, saTakerGets);
|
|
sleOffer->setFieldU64 (sfOwnerNode, uOwnerNode);
|
|
sleOffer->setFieldU64 (sfBookNode, uBookNode);
|
|
if (uExpiration)
|
|
sleOffer->setFieldU32 (sfExpiration, uExpiration);
|
|
if (bPassive)
|
|
sleOffer->setFlag (lsfPassive);
|
|
if (bSell)
|
|
sleOffer->setFlag (lsfSell);
|
|
view.insert(sleOffer);
|
|
}
|
|
|
|
if (result != tesSUCCESS)
|
|
m_journal.debug << "final result: " << transToken (result);
|
|
|
|
return { result, true };
|
|
}
|
|
|
|
TER
|
|
doApply() override
|
|
{
|
|
bool const openLedger = mParams & tapOPEN_LEDGER;
|
|
// This is the ledger view that we work against. Transactions are applied
|
|
// as we go on processing transactions.
|
|
MetaView view (mEngine->view(), openLedger);
|
|
// This is a checkpoint with just the fees paid. If something goes wrong
|
|
// with this transaction, we roll back to this ledger.
|
|
MetaView viewCancel (mEngine->view(), openLedger);
|
|
auto const result = applyGuts(view, viewCancel);
|
|
if (result.second)
|
|
view.apply();
|
|
else
|
|
viewCancel.apply();
|
|
return result.first;
|
|
}
|
|
};
|
|
|
|
TER
|
|
transact_CreateOffer (
|
|
STTx const& txn,
|
|
TransactionEngineParams params,
|
|
TransactionEngine* engine)
|
|
{
|
|
CrossType cross_type = CrossType::IouToIou;
|
|
|
|
bool const pays_xrp = txn.getFieldAmount (sfTakerPays).native ();
|
|
bool const gets_xrp = txn.getFieldAmount (sfTakerGets).native ();
|
|
|
|
if (pays_xrp && !gets_xrp)
|
|
cross_type = CrossType::IouToXrp;
|
|
else if (gets_xrp && !pays_xrp)
|
|
cross_type = CrossType::XrpToIou;
|
|
|
|
return CreateOffer (cross_type, txn, params, engine).apply ();
|
|
}
|
|
|
|
}
|