Files
rippled/src/libxrpl/tx/transactors/dex/OfferCreate.cpp
Mayukha Vadari d616aff4e2 fix tests (typos)
2026-04-09 17:14:46 -04:00

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