mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
924 lines
33 KiB
C++
924 lines
33 KiB
C++
#include <xrpl/basics/base_uint.h>
|
|
#include <xrpl/ledger/OrderBookDB.h>
|
|
#include <xrpl/ledger/PaymentSandbox.h>
|
|
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
|
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
|
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
|
#include <xrpl/ledger/helpers/OfferHelpers.h>
|
|
#include <xrpl/ledger/helpers/PermissionedDEXHelpers.h>
|
|
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
|
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
|
#include <xrpl/protocol/Feature.h>
|
|
#include <xrpl/protocol/STAmount.h>
|
|
#include <xrpl/protocol/TER.h>
|
|
#include <xrpl/protocol/TxFlags.h>
|
|
#include <xrpl/tx/paths/Flow.h>
|
|
#include <xrpl/tx/transactors/dex/OfferCreate.h>
|
|
|
|
namespace xrpl {
|
|
TxConsequences
|
|
OfferCreate::makeTxConsequences(PreflightContext const& ctx)
|
|
{
|
|
auto calculateMaxXRPSpend = [](STTx const& tx) -> XRPAmount {
|
|
auto const& amount{tx[sfTakerGets]};
|
|
return amount.native() ? amount.xrp() : beast::zero;
|
|
};
|
|
|
|
return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)};
|
|
}
|
|
|
|
bool
|
|
OfferCreate::checkExtraFeatures(PreflightContext const& ctx)
|
|
{
|
|
if (ctx.tx.isFieldPresent(sfDomainID) && !ctx.rules.enabled(featurePermissionedDEX))
|
|
return false;
|
|
|
|
return ctx.rules.enabled(featureMPTokensV2) ||
|
|
(!ctx.tx[sfTakerPays].holds<MPTIssue>() && !ctx.tx[sfTakerGets].holds<MPTIssue>());
|
|
}
|
|
|
|
std::uint32_t
|
|
OfferCreate::getFlagsMask(PreflightContext const& ctx)
|
|
{
|
|
// The tfOfferCreateMask is built assuming that PermissionedDEX is
|
|
// enabled
|
|
if (ctx.rules.enabled(featurePermissionedDEX))
|
|
return tfOfferCreateMask;
|
|
// If PermissionedDEX is not enabled, add tfHybrid to the mask,
|
|
// indicating it is not allowed.
|
|
return tfOfferCreateMask | tfHybrid;
|
|
}
|
|
|
|
NotTEC
|
|
OfferCreate::preflight(PreflightContext const& ctx)
|
|
{
|
|
auto& tx = ctx.tx;
|
|
auto& j = ctx.j;
|
|
|
|
std::uint32_t const uTxFlags = tx.getFlags();
|
|
|
|
if (tx.isFlag(tfHybrid) && !tx.isFieldPresent(sfDomainID))
|
|
return temINVALID_FLAG;
|
|
|
|
bool const bImmediateOrCancel((uTxFlags & tfImmediateOrCancel) != 0u);
|
|
bool const bFillOrKill((uTxFlags & tfFillOrKill) != 0u);
|
|
|
|
if (bImmediateOrCancel && bFillOrKill)
|
|
{
|
|
JLOG(j.debug()) << "Malformed transaction: both IoC and FoK set.";
|
|
return temINVALID_FLAG;
|
|
}
|
|
|
|
bool const bHaveExpiration(tx.isFieldPresent(sfExpiration));
|
|
|
|
if (bHaveExpiration && (tx.getFieldU32(sfExpiration) == 0))
|
|
{
|
|
JLOG(j.debug()) << "Malformed offer: bad expiration";
|
|
return temBAD_EXPIRATION;
|
|
}
|
|
|
|
if (auto const cancelSequence = tx[~sfOfferSequence]; cancelSequence && *cancelSequence == 0)
|
|
{
|
|
JLOG(j.debug()) << "Malformed offer: bad cancel sequence";
|
|
return temBAD_SEQUENCE;
|
|
}
|
|
|
|
STAmount const saTakerPays = tx[sfTakerPays];
|
|
STAmount const saTakerGets = tx[sfTakerGets];
|
|
|
|
if (!isLegalNet(saTakerPays) || !isLegalNet(saTakerGets))
|
|
return temBAD_AMOUNT;
|
|
|
|
if (saTakerPays.native() && saTakerGets.native())
|
|
{
|
|
JLOG(j.debug()) << "Malformed offer: redundant (XRP for XRP)";
|
|
return temBAD_OFFER;
|
|
}
|
|
if (saTakerPays <= beast::zero || saTakerGets <= beast::zero)
|
|
{
|
|
JLOG(j.debug()) << "Malformed offer: bad amount";
|
|
return temBAD_OFFER;
|
|
}
|
|
|
|
auto const& uPaysIssuerID = saTakerPays.getIssuer();
|
|
auto const& uPaysAsset = saTakerPays.asset();
|
|
|
|
auto const& uGetsIssuerID = saTakerGets.getIssuer();
|
|
auto const& uGetsAsset = saTakerGets.asset();
|
|
|
|
if (uPaysAsset == uGetsAsset)
|
|
{
|
|
JLOG(j.debug()) << "Malformed offer: redundant (IOU for IOU)";
|
|
return temREDUNDANT;
|
|
}
|
|
// We don't allow a non-native currency to use the currency code XRP.
|
|
if (badAsset() == uPaysAsset || badAsset() == uGetsAsset)
|
|
{
|
|
JLOG(j.debug()) << "Malformed offer: bad currency";
|
|
return temBAD_CURRENCY;
|
|
}
|
|
|
|
if (saTakerPays.native() != !uPaysIssuerID || saTakerGets.native() != !uGetsIssuerID)
|
|
{
|
|
JLOG(j.debug()) << "Malformed offer: bad issuer";
|
|
return temBAD_ISSUER;
|
|
}
|
|
|
|
return tesSUCCESS;
|
|
}
|
|
|
|
TER
|
|
OfferCreate::preclaim(PreclaimContext const& ctx)
|
|
{
|
|
auto const id = ctx.tx[sfAccount];
|
|
|
|
auto saTakerPays = ctx.tx[sfTakerPays];
|
|
auto saTakerGets = ctx.tx[sfTakerGets];
|
|
|
|
auto const& uPaysAsset = saTakerPays.asset();
|
|
|
|
auto const cancelSequence = ctx.tx[~sfOfferSequence];
|
|
|
|
AccountRoot const acctCreator(id, ctx.view);
|
|
if (!acctCreator)
|
|
return terNO_ACCOUNT;
|
|
|
|
std::uint32_t const uAccountSequence = acctCreator->getFieldU32(sfSequence);
|
|
|
|
auto viewJ = ctx.registry.get().getJournal("View");
|
|
|
|
if (isGlobalFrozen(ctx.view, saTakerPays.asset()) ||
|
|
isGlobalFrozen(ctx.view, saTakerGets.asset()))
|
|
{
|
|
JLOG(ctx.j.debug()) << "Offer involves frozen asset";
|
|
return tecFROZEN;
|
|
}
|
|
|
|
// Allow unfunded MPT for issuer (OutstandingAmount >= MaximumAmount)
|
|
if ((!saTakerGets.holds<MPTIssue>() || saTakerGets.getIssuer() != id) &&
|
|
accountFunds(ctx.view, id, saTakerGets, fhZERO_IF_FROZEN, ahZERO_IF_UNAUTHORIZED, viewJ) <=
|
|
beast::zero)
|
|
{
|
|
JLOG(ctx.j.debug()) << "delay: Offers must be at least partially funded.";
|
|
return tecUNFUNDED_OFFER;
|
|
}
|
|
|
|
// This can probably be simplified to make sure that you cancel sequences
|
|
// before the transaction sequence number.
|
|
if (cancelSequence && (uAccountSequence <= *cancelSequence))
|
|
{
|
|
JLOG(ctx.j.debug()) << "uAccountSequenceNext=" << uAccountSequence
|
|
<< " uOfferSequence=" << *cancelSequence;
|
|
return temBAD_SEQUENCE;
|
|
}
|
|
|
|
if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
|
|
{
|
|
// Note that this will get checked again in applyGuts, but it saves
|
|
// us a call to checkAcceptAsset and possible false negative.
|
|
return tecEXPIRED;
|
|
}
|
|
|
|
// Make sure that we are authorized to hold what the taker will pay us.
|
|
if (!saTakerPays.native())
|
|
{
|
|
auto result = checkAcceptAsset(ctx.view, ctx.flags, id, ctx.j, uPaysAsset);
|
|
if (!isTesSuccess(result))
|
|
return result;
|
|
}
|
|
|
|
// if domain is specified, make sure that domain exists and the offer create
|
|
// is part of the domain
|
|
if (ctx.tx.isFieldPresent(sfDomainID))
|
|
{
|
|
if (!permissioned_dex::accountInDomain(ctx.view, id, ctx.tx[sfDomainID]))
|
|
return tecNO_PERMISSION;
|
|
}
|
|
|
|
if (auto const ter = canTrade(ctx.view, saTakerPays.asset()); !isTesSuccess(ter))
|
|
return ter;
|
|
if (auto const ter = canTrade(ctx.view, saTakerGets.asset()); !isTesSuccess(ter))
|
|
return ter;
|
|
|
|
return tesSUCCESS;
|
|
}
|
|
|
|
TER
|
|
OfferCreate::checkAcceptAsset(
|
|
ReadView const& view,
|
|
ApplyFlags const flags,
|
|
AccountID const id,
|
|
beast::Journal const j,
|
|
Asset const& asset)
|
|
{
|
|
// Only valid for custom currencies
|
|
XRPL_ASSERT(!isXRP(asset), "xrpl::OfferCreate::checkAcceptAsset : input is not XRP");
|
|
|
|
AccountRoot const issuerAccount(asset.getIssuer(), view);
|
|
|
|
if (!issuerAccount)
|
|
{
|
|
JLOG(j.debug()) << "delay: can't receive IOUs from non-existent issuer: "
|
|
<< to_string(asset.getIssuer());
|
|
|
|
return ((flags & tapRETRY) != 0u) ? TER{terNO_ACCOUNT} : TER{tecNO_ISSUER};
|
|
}
|
|
|
|
// An account cannot create a trustline to itself, so no line can exist
|
|
// to be frozen. Additionally, an issuer can always accept its own
|
|
// issuance.
|
|
if (asset.getIssuer() == id)
|
|
return tesSUCCESS;
|
|
|
|
return asset.visit(
|
|
[&](Issue const& issue) -> TER {
|
|
auto const& issuer = issue.getIssuer();
|
|
if (issuerAccount->isFlag(lsfRequireAuth))
|
|
{
|
|
auto const trustLine = view.read(keylet::line(id, issuer, issue.currency));
|
|
|
|
if (!trustLine)
|
|
{
|
|
return ((flags & tapRETRY) != 0u) ? TER{terNO_LINE} : TER{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(id > issuer);
|
|
|
|
bool const is_authorized(
|
|
((*trustLine)[sfFlags] & (canonical_gt ? lsfLowAuth : lsfHighAuth)) != 0u);
|
|
|
|
if (!is_authorized)
|
|
{
|
|
JLOG(j.debug()) << "delay: can't receive IOUs from "
|
|
"issuer without auth.";
|
|
|
|
return ((flags & tapRETRY) != 0u) ? TER{terNO_AUTH} : TER{tecNO_AUTH};
|
|
}
|
|
}
|
|
|
|
auto const trustLine = view.read(keylet::line(id, issue.account, issue.currency));
|
|
|
|
if (!trustLine)
|
|
{
|
|
return tesSUCCESS;
|
|
}
|
|
|
|
// There's no difference which side enacted deep freeze, accepting
|
|
// tokens shouldn't be possible.
|
|
bool const deepFrozen =
|
|
((*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze)) != 0u;
|
|
|
|
if (deepFrozen)
|
|
{
|
|
return tecFROZEN;
|
|
}
|
|
|
|
return tesSUCCESS;
|
|
},
|
|
[&](MPTIssue const& issue) -> TER {
|
|
// WeakAuth - don't check if MPToken exists since it's created
|
|
// if needed.
|
|
return requireAuth(view, issue, id, AuthType::WeakAuth);
|
|
});
|
|
}
|
|
|
|
std::pair<TER, Amounts>
|
|
OfferCreate::flowCross(
|
|
PaymentSandbox& psb,
|
|
PaymentSandbox& psbCancel,
|
|
Amounts const& takerAmount,
|
|
std::optional<uint256> const& domainID)
|
|
{
|
|
try
|
|
{
|
|
// If the taker is unfunded before we begin crossing there's nothing
|
|
// to do - just return an error.
|
|
//
|
|
// We check this in preclaim, but when selling XRP charged fees can
|
|
// cause a user's available balance to go to 0 (by causing it to dip
|
|
// below the reserve) so we check this case again.
|
|
STAmount const inStartBalance = accountFunds(
|
|
psb, accountID_, takerAmount.in, fhZERO_IF_FROZEN, ahZERO_IF_UNAUTHORIZED, j_);
|
|
// Allow unfunded MPT issuer
|
|
auto const disallowUnfunded =
|
|
!inStartBalance.holds<MPTIssue>() || inStartBalance.getIssuer() != accountID_;
|
|
if (disallowUnfunded && inStartBalance <= beast::zero)
|
|
{
|
|
// The account balance can't cover even part of the offer.
|
|
JLOG(j_.debug()) << "Not crossing: taker is unfunded.";
|
|
return {tecUNFUNDED_OFFER, takerAmount};
|
|
}
|
|
|
|
// If the gateway has a transfer rate, accommodate that. The
|
|
// gateway takes its cut without any special consent from the
|
|
// offer taker. Set sendMax to allow for the gateway's cut.
|
|
Rate gatewayXferRate{QUALITY_ONE};
|
|
STAmount sendMax = takerAmount.in;
|
|
if (!sendMax.native() && (accountID_ != sendMax.getIssuer()))
|
|
{
|
|
gatewayXferRate = transferRate(psb, sendMax);
|
|
if (gatewayXferRate.value != QUALITY_ONE)
|
|
{
|
|
sendMax =
|
|
multiplyRound(takerAmount.in, gatewayXferRate, takerAmount.in.asset(), true);
|
|
}
|
|
}
|
|
|
|
// Payment flow code compares quality after the transfer rate is
|
|
// included. Since transfer rate is incorporated compute threshold.
|
|
Quality threshold{takerAmount.out, sendMax};
|
|
|
|
// If we're creating a passive offer adjust the threshold so we only
|
|
// cross offers that have a better quality than this one.
|
|
std::uint32_t const txFlags = ctx_.tx.getFlags();
|
|
if ((txFlags & tfPassive) != 0u)
|
|
++threshold;
|
|
|
|
// Don't send more than our balance.
|
|
if (sendMax > inStartBalance)
|
|
sendMax = inStartBalance;
|
|
|
|
// Always invoke flow() with the default path. However if neither
|
|
// of the takerAmount currencies are XRP then we cross through an
|
|
// additional path with XRP as the intermediate between two books.
|
|
// This second path we have to build ourselves.
|
|
STPathSet paths;
|
|
if (!takerAmount.in.native() && !takerAmount.out.native())
|
|
{
|
|
STPath path;
|
|
path.emplace_back(std::nullopt, xrpCurrency(), std::nullopt);
|
|
paths.emplace_back(std::move(path));
|
|
}
|
|
// Special handling for the tfSell flag.
|
|
STAmount deliver = takerAmount.out;
|
|
auto const& deliverAsset = deliver.asset();
|
|
OfferCrossing offerCrossing = OfferCrossing::yes;
|
|
if ((txFlags & tfSell) != 0u)
|
|
{
|
|
offerCrossing = OfferCrossing::sell;
|
|
// We are selling, so we will accept *more* than the offer
|
|
// specified. Since we don't know how much they might offer,
|
|
// we allow delivery of the largest possible amount.
|
|
deliver.asset().visit(
|
|
[&](Issue const& issue) {
|
|
if (issue.native())
|
|
{
|
|
deliver = STAmount{STAmount::cMaxNative};
|
|
}
|
|
// We can't use the maximum possible currency here because
|
|
// there might be a gateway transfer rate to account for.
|
|
// Since the transfer rate cannot exceed 200%, we use 1/2
|
|
// maxValue for our limit.
|
|
else
|
|
{
|
|
deliver =
|
|
STAmount{deliverAsset, STAmount::cMaxValue / 2, STAmount::cMaxOffset};
|
|
}
|
|
},
|
|
[&](MPTIssue const&) { deliver = STAmount{deliverAsset, maxMPTokenAmount / 2}; });
|
|
}
|
|
|
|
// Call the payment engine's flow() to do the actual work.
|
|
auto const result = flow(
|
|
psb,
|
|
deliver,
|
|
accountID_,
|
|
accountID_,
|
|
paths,
|
|
true, // default path
|
|
(txFlags & tfFillOrKill) == 0u, // partial payment
|
|
true, // owner pays transfer fee
|
|
offerCrossing,
|
|
threshold,
|
|
sendMax,
|
|
domainID,
|
|
j_);
|
|
|
|
// If stale offers were found remove them.
|
|
for (auto const& toRemove : result.removableOffers)
|
|
{
|
|
if (auto otr = psb.peek(keylet::offer(toRemove)))
|
|
offerDelete(psb, otr, j_);
|
|
if (auto otr = psbCancel.peek(keylet::offer(toRemove)))
|
|
offerDelete(psbCancel, otr, j_);
|
|
}
|
|
|
|
// Determine the size of the final offer after crossing.
|
|
auto afterCross = takerAmount; // If !tesSUCCESS offer unchanged
|
|
if (isTesSuccess(result.result()))
|
|
{
|
|
STAmount const takerInBalance = accountFunds(
|
|
psb, accountID_, takerAmount.in, fhZERO_IF_FROZEN, ahZERO_IF_UNAUTHORIZED, j_);
|
|
|
|
if (disallowUnfunded && takerInBalance <= beast::zero)
|
|
{
|
|
// If offer crossing exhausted the account's funds don't
|
|
// create the offer.
|
|
afterCross.in.clear();
|
|
afterCross.out.clear();
|
|
}
|
|
else
|
|
{
|
|
STAmount const rate{Quality{takerAmount.out, takerAmount.in}.rate()};
|
|
|
|
if ((txFlags & tfSell) != 0u)
|
|
{
|
|
// If selling then scale the new out amount based on how
|
|
// much we sold during crossing. This preserves the offer
|
|
// Quality,
|
|
|
|
// Reduce the offer that is placed by the crossed amount.
|
|
// Note that we must ignore the portion of the
|
|
// actualAmountIn that may have been consumed by a
|
|
// gateway's transfer rate.
|
|
STAmount nonGatewayAmountIn = result.actualAmountIn;
|
|
if (gatewayXferRate.value != QUALITY_ONE)
|
|
{
|
|
nonGatewayAmountIn = divideRound(
|
|
result.actualAmountIn, gatewayXferRate, takerAmount.in.asset(), true);
|
|
}
|
|
|
|
afterCross.in -= nonGatewayAmountIn;
|
|
|
|
// It's possible that the divRound will cause our subtract
|
|
// to go slightly negative. So limit afterCross.in to zero.
|
|
if (afterCross.in < beast::zero)
|
|
{
|
|
// We should verify that the difference *is* small, but
|
|
// what is a good threshold to check?
|
|
afterCross.in.clear();
|
|
}
|
|
|
|
afterCross.out =
|
|
divRoundStrict(afterCross.in, rate, takerAmount.out.asset(), false);
|
|
}
|
|
else
|
|
{
|
|
// If not selling, we scale the input based on the
|
|
// remaining output. This too preserves the offer
|
|
// Quality.
|
|
afterCross.out -= result.actualAmountOut;
|
|
XRPL_ASSERT(
|
|
afterCross.out >= beast::zero,
|
|
"xrpl::OfferCreate::flowCross : minimum offer");
|
|
if (afterCross.out < beast::zero)
|
|
afterCross.out.clear();
|
|
afterCross.in = mulRound(afterCross.out, rate, takerAmount.in.asset(), true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return how much of the offer is left.
|
|
return {tesSUCCESS, afterCross};
|
|
}
|
|
catch (std::exception const& e)
|
|
{
|
|
JLOG(j_.error()) << "Exception during offer crossing: " << e.what();
|
|
}
|
|
return {tecINTERNAL, takerAmount};
|
|
}
|
|
|
|
std::string
|
|
OfferCreate::format_amount(STAmount const& amount)
|
|
{
|
|
std::string txt = amount.getText();
|
|
txt += "/";
|
|
amount.asset().visit(
|
|
[&](Issue const& issue) { txt += to_string(issue.currency); },
|
|
[&](MPTIssue const& issue) { txt += to_string(issue); });
|
|
return txt;
|
|
}
|
|
|
|
TER
|
|
OfferCreate::applyHybrid(
|
|
Sandbox& sb,
|
|
std::shared_ptr<STLedgerEntry> sleOffer,
|
|
Keylet const& offerKey,
|
|
STAmount const& saTakerPays,
|
|
STAmount const& saTakerGets,
|
|
std::function<void(SLE::ref, std::optional<uint256>)> const& setDir)
|
|
{
|
|
if (!sleOffer->isFieldPresent(sfDomainID))
|
|
return tecINTERNAL; // LCOV_EXCL_LINE
|
|
|
|
// set hybrid flag
|
|
sleOffer->setFlag(lsfHybrid);
|
|
|
|
// if offer is hybrid, need to also place into open offer dir
|
|
Book const book{saTakerPays.asset(), saTakerGets.asset(), std::nullopt};
|
|
|
|
auto dir = keylet::quality(keylet::book(book), getRate(saTakerGets, saTakerPays));
|
|
bool const bookExists = sb.exists(dir);
|
|
|
|
auto const bookNode = sb.dirAppend(dir, offerKey, [&](SLE::ref sle) {
|
|
// don't set domainID on the directory object since this directory is
|
|
// for open book
|
|
setDir(sle, std::nullopt);
|
|
});
|
|
|
|
if (!bookNode)
|
|
{
|
|
JLOG(j_.debug()) << "final result: failed to add hybrid offer to open book";
|
|
return tecDIR_FULL; // LCOV_EXCL_LINE
|
|
}
|
|
|
|
STArray bookArr(sfAdditionalBooks, 1);
|
|
auto bookInfo = STObject::makeInnerObject(sfBook);
|
|
bookInfo.setFieldH256(sfBookDirectory, dir.key);
|
|
bookInfo.setFieldU64(sfBookNode, *bookNode);
|
|
bookArr.push_back(std::move(bookInfo));
|
|
|
|
if (!bookExists)
|
|
ctx_.registry.get().getOrderBookDB().addOrderBook(book);
|
|
|
|
sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
|
|
return tesSUCCESS;
|
|
}
|
|
|
|
std::pair<TER, bool>
|
|
OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
|
|
{
|
|
using beast::zero;
|
|
|
|
std::uint32_t const uTxFlags = ctx_.tx.getFlags();
|
|
|
|
bool const bPassive((uTxFlags & tfPassive) != 0u);
|
|
bool const bImmediateOrCancel((uTxFlags & tfImmediateOrCancel) != 0u);
|
|
bool const bFillOrKill((uTxFlags & tfFillOrKill) != 0u);
|
|
bool const bSell((uTxFlags & tfSell) != 0u);
|
|
bool const bHybrid((uTxFlags & tfHybrid) != 0u);
|
|
|
|
auto saTakerPays = ctx_.tx[sfTakerPays];
|
|
auto saTakerGets = ctx_.tx[sfTakerGets];
|
|
auto const domainID = ctx_.tx[~sfDomainID];
|
|
|
|
auto const cancelSequence = ctx_.tx[~sfOfferSequence];
|
|
|
|
// Note that we we use the value from the sequence or ticket as the
|
|
// offer sequence. For more explanation see comments in SeqProxy.h.
|
|
auto const offerSequence = ctx_.tx.getSeqValue();
|
|
|
|
// 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.
|
|
auto uRate = getRate(saTakerGets, saTakerPays);
|
|
|
|
auto viewJ = ctx_.registry.get().getJournal("View");
|
|
|
|
TER result = tesSUCCESS;
|
|
|
|
// Process a cancellation request that's passed along with an offer.
|
|
if (cancelSequence)
|
|
{
|
|
auto const sleCancel = sb.peek(keylet::offer(accountID_, *cancelSequence));
|
|
|
|
// 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)
|
|
{
|
|
JLOG(j_.debug()) << "Create cancels order " << *cancelSequence;
|
|
result = offerDelete(sb, sleCancel, viewJ);
|
|
}
|
|
}
|
|
|
|
auto const expiration = ctx_.tx[~sfExpiration];
|
|
|
|
if (hasExpired(sb, expiration))
|
|
{
|
|
// If the offer has expired, the transaction has successfully
|
|
// done nothing, so short circuit from here.
|
|
return {tecEXPIRED, true};
|
|
}
|
|
|
|
bool const bOpenLedger = sb.open();
|
|
bool crossed = false;
|
|
|
|
if (isTesSuccess(result))
|
|
{
|
|
// If a tick size applies, round the offer to the tick size
|
|
auto const& uPaysIssuerID = saTakerPays.getIssuer();
|
|
auto const& uGetsIssuerID = saTakerGets.getIssuer();
|
|
|
|
std::uint8_t uTickSize = Quality::maxTickSize;
|
|
// Not XRP or MPT
|
|
if (!saTakerPays.integral())
|
|
{
|
|
AccountRoot const acctPays(uPaysIssuerID, sb);
|
|
if (acctPays && acctPays->isFieldPresent(sfTickSize))
|
|
uTickSize = std::min(uTickSize, (*acctPays)[sfTickSize]);
|
|
}
|
|
// Not XRP or MPT
|
|
if (!saTakerGets.integral())
|
|
{
|
|
AccountRoot const acctGets(uGetsIssuerID, sb);
|
|
if (acctGets && acctGets->isFieldPresent(sfTickSize))
|
|
uTickSize = std::min(uTickSize, (*acctGets)[sfTickSize]);
|
|
}
|
|
if (uTickSize < Quality::maxTickSize)
|
|
{
|
|
auto const rate = Quality{saTakerGets, saTakerPays}.round(uTickSize).rate();
|
|
|
|
// We round the side that's not exact,
|
|
// just as if the offer happened to execute
|
|
// at a slightly better (for the placer) rate
|
|
if (bSell)
|
|
{
|
|
// this is a sell, round taker pays
|
|
if (!saTakerPays.holds<MPTIssue>())
|
|
saTakerPays = multiply(saTakerGets, rate, saTakerPays.asset());
|
|
}
|
|
else if (!saTakerGets.holds<MPTIssue>())
|
|
{
|
|
// this is a buy, round taker gets
|
|
saTakerGets = divide(saTakerPays, rate, saTakerGets.asset());
|
|
}
|
|
if (!saTakerGets || !saTakerPays)
|
|
{
|
|
JLOG(j_.debug()) << "Offer rounded to zero";
|
|
return {result, true};
|
|
}
|
|
|
|
uRate = getRate(saTakerGets, saTakerPays);
|
|
}
|
|
|
|
// We reverse pays and gets because during crossing we are taking.
|
|
Amounts const takerAmount(saTakerGets, saTakerPays);
|
|
|
|
JLOG(j_.debug()) << "Attempting cross: " << to_string(takerAmount.in.asset()) << " -> "
|
|
<< to_string(takerAmount.out.asset());
|
|
|
|
if (auto stream = j_.trace())
|
|
{
|
|
stream << " mode: " << (bPassive ? "passive " : "") << (bSell ? "sell" : "buy");
|
|
stream << " in: " << format_amount(takerAmount.in);
|
|
stream << " out: " << format_amount(takerAmount.out);
|
|
}
|
|
|
|
// 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;
|
|
PaymentSandbox psbFlow{&sb};
|
|
PaymentSandbox psbCancelFlow{&sbCancel};
|
|
|
|
std::tie(result, place_offer) = flowCross(psbFlow, psbCancelFlow, takerAmount, domainID);
|
|
psbFlow.apply(sb);
|
|
psbCancelFlow.apply(sbCancel);
|
|
|
|
// We expect the implementation of cross to succeed
|
|
// or give a tec.
|
|
XRPL_ASSERT(
|
|
isTesSuccess(result) || isTecClaim(result),
|
|
"xrpl::OfferCreate::applyGuts : result is tesSUCCESS or "
|
|
"tecCLAIM");
|
|
|
|
if (auto stream = j_.trace())
|
|
{
|
|
stream << "Cross result: " << transToken(result);
|
|
stream << " in: " << format_amount(place_offer.in);
|
|
stream << " out: " << format_amount(place_offer.out);
|
|
}
|
|
|
|
if (result == tecFAILED_PROCESSING && bOpenLedger)
|
|
result = telFAILED_PROCESSING;
|
|
|
|
if (!isTesSuccess(result))
|
|
{
|
|
JLOG(j_.debug()) << "final result: " << transToken(result);
|
|
return {result, true};
|
|
}
|
|
|
|
XRPL_ASSERT(
|
|
saTakerGets.asset() == place_offer.in.asset(),
|
|
"xrpl::OfferCreate::applyGuts : taker gets issue match");
|
|
XRPL_ASSERT(
|
|
saTakerPays.asset() == place_offer.out.asset(),
|
|
"xrpl::OfferCreate::applyGuts : taker pays issue match");
|
|
|
|
if (takerAmount != 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)
|
|
{
|
|
JLOG(j_.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)
|
|
{
|
|
JLOG(j_.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;
|
|
}
|
|
|
|
XRPL_ASSERT(
|
|
saTakerPays > zero && saTakerGets > zero,
|
|
"xrpl::OfferCreate::applyGuts : taker pays and gets positive");
|
|
|
|
if (!isTesSuccess(result))
|
|
{
|
|
JLOG(j_.debug()) << "final result: " << transToken(result);
|
|
return {result, true};
|
|
}
|
|
|
|
if (auto stream = j_.trace())
|
|
{
|
|
stream << "Place" << (crossed ? " remaining " : " ") << "offer:";
|
|
stream << " Pays: " << saTakerPays.getFullText();
|
|
stream << " 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)
|
|
{
|
|
JLOG(j_.trace()) << "Fill or Kill: offer killed";
|
|
return {tecKILLED, false};
|
|
}
|
|
|
|
// For 'immediate or cancel' offers, the amount remaining doesn't get
|
|
// placed - it gets canceled and the operation succeeds.
|
|
if (bImmediateOrCancel)
|
|
{
|
|
JLOG(j_.trace()) << "Immediate or cancel: offer canceled";
|
|
if (!crossed)
|
|
{
|
|
// Any ImmediateOrCancel offer that transfers absolutely no funds
|
|
// returns tecKILLED rather than tesSUCCESS. Motivation for the
|
|
// change is here: https://github.com/XRPLF/rippled/issues/4115
|
|
return {tecKILLED, false};
|
|
}
|
|
return {tesSUCCESS, true};
|
|
}
|
|
|
|
WAccountRoot wrappedCreator(accountID_, sb, j_);
|
|
if (!wrappedCreator)
|
|
return {tefINTERNAL, false};
|
|
|
|
{
|
|
XRPAmount const reserve =
|
|
sb.fees().accountReserve(wrappedCreator->getFieldU32(sfOwnerCount) + 1);
|
|
|
|
if (preFeeBalance_ < reserve)
|
|
{
|
|
// 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 (!isTesSuccess(result))
|
|
{
|
|
JLOG(j_.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 = keylet::offer(accountID_, offerSequence);
|
|
|
|
// Add offer to owner's directory.
|
|
auto const ownerNode =
|
|
sb.dirInsert(keylet::ownerDir(accountID_), offer_index, describeOwnerDir(accountID_));
|
|
|
|
if (!ownerNode)
|
|
{
|
|
// LCOV_EXCL_START
|
|
JLOG(j_.debug()) << "final result: failed to add offer to owner's directory";
|
|
return {tecDIR_FULL, true};
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
// Update owner count.
|
|
wrappedCreator.adjustOwnerCount(1);
|
|
|
|
JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.asset()) << " : "
|
|
<< to_string(saTakerGets.asset())
|
|
<< (domainID ? (" : " + to_string(*domainID)) : "");
|
|
|
|
Book const book{saTakerPays.asset(), saTakerGets.asset(), domainID};
|
|
|
|
// Add offer to order book, using the original rate
|
|
// before any crossing occurred.
|
|
//
|
|
// Regular offer - BookDirectory points to open directory
|
|
//
|
|
// Domain offer (w/o hybrid) - BookDirectory points to domain
|
|
// directory
|
|
//
|
|
// Hybrid domain offer - BookDirectory points to domain directory,
|
|
// and AdditionalBooks field stores one entry that points to the open
|
|
// directory
|
|
auto dir = keylet::quality(keylet::book(book), uRate);
|
|
bool const bookExisted = static_cast<bool>(sb.peek(dir));
|
|
|
|
auto setBookDir = [&](SLE::ref sle, std::optional<uint256> const& maybeDomain) {
|
|
saTakerPays.asset().visit(
|
|
[&](Issue const& issue) {
|
|
sle->setFieldH160(sfTakerPaysCurrency, issue.currency);
|
|
sle->setFieldH160(sfTakerPaysIssuer, issue.account);
|
|
},
|
|
[&](MPTIssue const& issue) { sle->setFieldH192(sfTakerPaysMPT, issue.getMptID()); });
|
|
saTakerGets.asset().visit(
|
|
[&](Issue const& issue) {
|
|
sle->setFieldH160(sfTakerGetsCurrency, issue.currency);
|
|
sle->setFieldH160(sfTakerGetsIssuer, issue.account);
|
|
},
|
|
[&](MPTIssue const& issue) { sle->setFieldH192(sfTakerGetsMPT, issue.getMptID()); });
|
|
sle->setFieldU64(sfExchangeRate, uRate);
|
|
if (maybeDomain)
|
|
sle->setFieldH256(sfDomainID, *maybeDomain);
|
|
};
|
|
|
|
auto const bookNode = sb.dirAppend(dir, offer_index, [&](SLE::ref sle) {
|
|
// sets domainID on book directory if it's a domain offer
|
|
setBookDir(sle, domainID);
|
|
});
|
|
|
|
if (!bookNode)
|
|
{
|
|
// LCOV_EXCL_START
|
|
JLOG(j_.debug()) << "final result: failed to add offer to book";
|
|
return {tecDIR_FULL, true};
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
auto sleOffer = std::make_shared<SLE>(offer_index);
|
|
sleOffer->setAccountID(sfAccount, accountID_);
|
|
sleOffer->setFieldU32(sfSequence, offerSequence);
|
|
sleOffer->setFieldH256(sfBookDirectory, dir.key);
|
|
sleOffer->setFieldAmount(sfTakerPays, saTakerPays);
|
|
sleOffer->setFieldAmount(sfTakerGets, saTakerGets);
|
|
sleOffer->setFieldU64(sfOwnerNode, *ownerNode);
|
|
sleOffer->setFieldU64(sfBookNode, *bookNode);
|
|
if (expiration)
|
|
sleOffer->setFieldU32(sfExpiration, *expiration);
|
|
if (bPassive)
|
|
sleOffer->setFlag(lsfPassive);
|
|
if (bSell)
|
|
sleOffer->setFlag(lsfSell);
|
|
if (domainID)
|
|
sleOffer->setFieldH256(sfDomainID, *domainID);
|
|
|
|
// if it's a hybrid offer, set hybrid flag, and create an open dir
|
|
if (bHybrid)
|
|
{
|
|
auto const res =
|
|
applyHybrid(sb, sleOffer, offer_index, saTakerPays, saTakerGets, setBookDir);
|
|
if (!isTesSuccess(res))
|
|
return {res, true}; // LCOV_EXCL_LINE
|
|
}
|
|
|
|
sb.insert(sleOffer);
|
|
|
|
if (!bookExisted)
|
|
ctx_.registry.get().getOrderBookDB().addOrderBook(book);
|
|
|
|
JLOG(j_.debug()) << "final result: success";
|
|
|
|
return {tesSUCCESS, true};
|
|
}
|
|
|
|
TER
|
|
OfferCreate::doApply()
|
|
{
|
|
// This is the ledger view that we work against. Transactions are applied
|
|
// as we go on processing transactions.
|
|
Sandbox sb(&ctx_.view());
|
|
|
|
// This is a ledger with just the fees paid and any unfunded or expired
|
|
// offers we encounter removed. It's used when handling Fill-or-Kill offers,
|
|
// if the order isn't going to be placed, to avoid wasting the work we did.
|
|
Sandbox sbCancel(&ctx_.view());
|
|
|
|
auto const result = applyGuts(sb, sbCancel);
|
|
if (result.second)
|
|
{
|
|
sb.apply(ctx_.rawView());
|
|
}
|
|
else
|
|
{
|
|
sbCancel.apply(ctx_.rawView());
|
|
}
|
|
return result.first;
|
|
}
|
|
|
|
} // namespace xrpl
|