rippled
Loading...
Searching...
No Matches
CreateOffer.cpp
1#include <xrpld/app/ledger/OrderBookDB.h>
2#include <xrpld/app/misc/PermissionedDEXHelpers.h>
3#include <xrpld/app/paths/Flow.h>
4#include <xrpld/app/tx/detail/CreateOffer.h>
5
6#include <xrpl/basics/base_uint.h>
7#include <xrpl/beast/utility/WrappedSink.h>
8#include <xrpl/ledger/PaymentSandbox.h>
9#include <xrpl/protocol/Feature.h>
10#include <xrpl/protocol/STAmount.h>
11#include <xrpl/protocol/TER.h>
12#include <xrpl/protocol/TxFlags.h>
13#include <xrpl/protocol/st.h>
14
15namespace xrpl {
16TxConsequences
18{
19 auto calculateMaxXRPSpend = [](STTx const& tx) -> XRPAmount {
20 auto const& amount{tx[sfTakerGets]};
21 return amount.native() ? amount.xrp() : beast::zero;
22 };
23
24 return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)};
25}
26
27bool
29{
30 if (ctx.tx.isFieldPresent(sfDomainID) && !ctx.rules.enabled(featurePermissionedDEX))
31 return false;
32
33 return true;
34}
35
38{
39 // The tfOfferCreateMask is built assuming that PermissionedDEX is
40 // enabled
41 if (ctx.rules.enabled(featurePermissionedDEX))
42 return tfOfferCreateMask;
43 // If PermissionedDEX is not enabled, add tfHybrid to the mask,
44 // indicating it is not allowed.
46}
47
50{
51 auto& tx = ctx.tx;
52 auto& j = ctx.j;
53
54 std::uint32_t const uTxFlags = tx.getFlags();
55
56 if (tx.isFlag(tfHybrid) && !tx.isFieldPresent(sfDomainID))
57 return temINVALID_FLAG;
58
59 bool const bImmediateOrCancel(uTxFlags & tfImmediateOrCancel);
60 bool const bFillOrKill(uTxFlags & tfFillOrKill);
61
62 if (bImmediateOrCancel && bFillOrKill)
63 {
64 JLOG(j.debug()) << "Malformed transaction: both IoC and FoK set.";
65 return temINVALID_FLAG;
66 }
67
68 bool const bHaveExpiration(tx.isFieldPresent(sfExpiration));
69
70 if (bHaveExpiration && (tx.getFieldU32(sfExpiration) == 0))
71 {
72 JLOG(j.debug()) << "Malformed offer: bad expiration";
73 return temBAD_EXPIRATION;
74 }
75
76 if (auto const cancelSequence = tx[~sfOfferSequence]; cancelSequence && *cancelSequence == 0)
77 {
78 JLOG(j.debug()) << "Malformed offer: bad cancel sequence";
79 return temBAD_SEQUENCE;
80 }
81
82 STAmount saTakerPays = tx[sfTakerPays];
83 STAmount saTakerGets = tx[sfTakerGets];
84
85 if (!isLegalNet(saTakerPays) || !isLegalNet(saTakerGets))
86 return temBAD_AMOUNT;
87
88 if (saTakerPays.native() && saTakerGets.native())
89 {
90 JLOG(j.debug()) << "Malformed offer: redundant (XRP for XRP)";
91 return temBAD_OFFER;
92 }
93 if (saTakerPays <= beast::zero || saTakerGets <= beast::zero)
94 {
95 JLOG(j.debug()) << "Malformed offer: bad amount";
96 return temBAD_OFFER;
97 }
98
99 auto const& uPaysIssuerID = saTakerPays.getIssuer();
100 auto const& uPaysCurrency = saTakerPays.getCurrency();
101
102 auto const& uGetsIssuerID = saTakerGets.getIssuer();
103 auto const& uGetsCurrency = saTakerGets.getCurrency();
104
105 if (uPaysCurrency == uGetsCurrency && uPaysIssuerID == uGetsIssuerID)
106 {
107 JLOG(j.debug()) << "Malformed offer: redundant (IOU for IOU)";
108 return temREDUNDANT;
109 }
110 // We don't allow a non-native currency to use the currency code XRP.
111 if (badCurrency() == uPaysCurrency || badCurrency() == uGetsCurrency)
112 {
113 JLOG(j.debug()) << "Malformed offer: bad currency";
114 return temBAD_CURRENCY;
115 }
116
117 if (saTakerPays.native() != !uPaysIssuerID || saTakerGets.native() != !uGetsIssuerID)
118 {
119 JLOG(j.debug()) << "Malformed offer: bad issuer";
120 return temBAD_ISSUER;
121 }
122
123 return tesSUCCESS;
124}
125
126TER
128{
129 auto const id = ctx.tx[sfAccount];
130
131 auto saTakerPays = ctx.tx[sfTakerPays];
132 auto saTakerGets = ctx.tx[sfTakerGets];
133
134 auto const& uPaysIssuerID = saTakerPays.getIssuer();
135 auto const& uPaysCurrency = saTakerPays.getCurrency();
136
137 auto const& uGetsIssuerID = saTakerGets.getIssuer();
138
139 auto const cancelSequence = ctx.tx[~sfOfferSequence];
140
141 auto const sleCreator = ctx.view.read(keylet::account(id));
142 if (!sleCreator)
143 return terNO_ACCOUNT;
144
145 std::uint32_t const uAccountSequence = sleCreator->getFieldU32(sfSequence);
146
147 auto viewJ = ctx.app.journal("View");
148
149 if (isGlobalFrozen(ctx.view, uPaysIssuerID) || isGlobalFrozen(ctx.view, uGetsIssuerID))
150 {
151 JLOG(ctx.j.debug()) << "Offer involves frozen asset";
152 return tecFROZEN;
153 }
154
155 if (accountFunds(ctx.view, id, saTakerGets, fhZERO_IF_FROZEN, viewJ) <= beast::zero)
156 {
157 JLOG(ctx.j.debug()) << "delay: Offers must be at least partially funded.";
158 return tecUNFUNDED_OFFER;
159 }
160
161 // This can probably be simplified to make sure that you cancel sequences
162 // before the transaction sequence number.
163 if (cancelSequence && (uAccountSequence <= *cancelSequence))
164 {
165 JLOG(ctx.j.debug()) << "uAccountSequenceNext=" << uAccountSequence << " uOfferSequence=" << *cancelSequence;
166 return temBAD_SEQUENCE;
167 }
168
169 if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
170 {
171 // Note that this will get checked again in applyGuts, but it saves
172 // us a call to checkAcceptAsset and possible false negative.
173 return tecEXPIRED;
174 }
175
176 // Make sure that we are authorized to hold what the taker will pay us.
177 if (!saTakerPays.native())
178 {
179 auto result = checkAcceptAsset(ctx.view, ctx.flags, id, ctx.j, Issue(uPaysCurrency, uPaysIssuerID));
180 if (result != tesSUCCESS)
181 return result;
182 }
183
184 // if domain is specified, make sure that domain exists and the offer create
185 // is part of the domain
186 if (ctx.tx.isFieldPresent(sfDomainID))
187 {
188 if (!permissioned_dex::accountInDomain(ctx.view, id, ctx.tx[sfDomainID]))
189 return tecNO_PERMISSION;
190 }
191
192 return tesSUCCESS;
193}
194
195TER
197 ReadView const& view,
198 ApplyFlags const flags,
199 AccountID const id,
200 beast::Journal const j,
201 Issue const& issue)
202{
203 // Only valid for custom currencies
204 XRPL_ASSERT(!isXRP(issue.currency), "xrpl::CreateOffer::checkAcceptAsset : input is not XRP");
205
206 auto const issuerAccount = view.read(keylet::account(issue.account));
207
208 if (!issuerAccount)
209 {
210 JLOG(j.debug()) << "delay: can't receive IOUs from non-existent issuer: " << to_string(issue.account);
211
212 return (flags & tapRETRY) ? TER{terNO_ACCOUNT} : TER{tecNO_ISSUER};
213 }
214
215 if (issue.account == id)
216 // An account can always accept its own issuance.
217 return tesSUCCESS;
218
219 if ((*issuerAccount)[sfFlags] & lsfRequireAuth)
220 {
221 auto const trustLine = view.read(keylet::line(id, issue.account, issue.currency));
222
223 if (!trustLine)
224 {
225 return (flags & tapRETRY) ? TER{terNO_LINE} : TER{tecNO_LINE};
226 }
227
228 // Entries have a canonical representation, determined by a
229 // lexicographical "greater than" comparison employing strict weak
230 // ordering. Determine which entry we need to access.
231 bool const canonical_gt(id > issue.account);
232
233 bool const is_authorized((*trustLine)[sfFlags] & (canonical_gt ? lsfLowAuth : lsfHighAuth));
234
235 if (!is_authorized)
236 {
237 JLOG(j.debug()) << "delay: can't receive IOUs from issuer without auth.";
238
239 return (flags & tapRETRY) ? TER{terNO_AUTH} : TER{tecNO_AUTH};
240 }
241 }
242
243 // An account can not create a trustline to itself, so no line can exist
244 // to be frozen. Additionally, an issuer can always accept its own
245 // issuance.
246 if (issue.account == id)
247 {
248 return tesSUCCESS;
249 }
250
251 auto const trustLine = view.read(keylet::line(id, issue.account, issue.currency));
252
253 if (!trustLine)
254 {
255 return tesSUCCESS;
256 }
257
258 // There's no difference which side enacted deep freeze, accepting
259 // tokens shouldn't be possible.
260 bool const deepFrozen = (*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze);
261
262 if (deepFrozen)
263 {
264 return tecFROZEN;
265 }
266
267 return tesSUCCESS;
268}
269
272 PaymentSandbox& psb,
273 PaymentSandbox& psbCancel,
274 Amounts const& takerAmount,
275 std::optional<uint256> const& domainID)
276{
277 try
278 {
279 // If the taker is unfunded before we begin crossing there's nothing
280 // to do - just return an error.
281 //
282 // We check this in preclaim, but when selling XRP charged fees can
283 // cause a user's available balance to go to 0 (by causing it to dip
284 // below the reserve) so we check this case again.
285 STAmount const inStartBalance = accountFunds(psb, account_, takerAmount.in, fhZERO_IF_FROZEN, j_);
286 if (inStartBalance <= beast::zero)
287 {
288 // The account balance can't cover even part of the offer.
289 JLOG(j_.debug()) << "Not crossing: taker is unfunded.";
290 return {tecUNFUNDED_OFFER, takerAmount};
291 }
292
293 // If the gateway has a transfer rate, accommodate that. The
294 // gateway takes its cut without any special consent from the
295 // offer taker. Set sendMax to allow for the gateway's cut.
296 Rate gatewayXferRate{QUALITY_ONE};
297 STAmount sendMax = takerAmount.in;
298 if (!sendMax.native() && (account_ != sendMax.getIssuer()))
299 {
300 gatewayXferRate = transferRate(psb, sendMax.getIssuer());
301 if (gatewayXferRate.value != QUALITY_ONE)
302 {
303 sendMax = multiplyRound(takerAmount.in, gatewayXferRate, takerAmount.in.issue(), true);
304 }
305 }
306
307 // Payment flow code compares quality after the transfer rate is
308 // included. Since transfer rate is incorporated compute threshold.
309 Quality threshold{takerAmount.out, sendMax};
310
311 // If we're creating a passive offer adjust the threshold so we only
312 // cross offers that have a better quality than this one.
313 std::uint32_t const txFlags = ctx_.tx.getFlags();
314 if (txFlags & tfPassive)
315 ++threshold;
316
317 // Don't send more than our balance.
318 if (sendMax > inStartBalance)
319 sendMax = inStartBalance;
320
321 // Always invoke flow() with the default path. However if neither
322 // of the takerAmount currencies are XRP then we cross through an
323 // additional path with XRP as the intermediate between two books.
324 // This second path we have to build ourselves.
325 STPathSet paths;
326 if (!takerAmount.in.native() && !takerAmount.out.native())
327 {
328 STPath path;
330 paths.emplace_back(std::move(path));
331 }
332 // Special handling for the tfSell flag.
333 STAmount deliver = takerAmount.out;
334 OfferCrossing offerCrossing = OfferCrossing::yes;
335 if (txFlags & tfSell)
336 {
337 offerCrossing = OfferCrossing::sell;
338 // We are selling, so we will accept *more* than the offer
339 // specified. Since we don't know how much they might offer,
340 // we allow delivery of the largest possible amount.
341 if (deliver.native())
343 else
344 // We can't use the maximum possible currency here because
345 // there might be a gateway transfer rate to account for.
346 // Since the transfer rate cannot exceed 200%, we use 1/2
347 // maxValue for our limit.
348 deliver = STAmount{takerAmount.out.issue(), STAmount::cMaxValue / 2, STAmount::cMaxOffset};
349 }
350
351 // Call the payment engine's flow() to do the actual work.
352 auto const result = flow(
353 psb,
354 deliver,
355 account_,
356 account_,
357 paths,
358 true, // default path
359 !(txFlags & tfFillOrKill), // partial payment
360 true, // owner pays transfer fee
361 offerCrossing,
362 threshold,
363 sendMax,
364 domainID,
365 j_);
366
367 // If stale offers were found remove them.
368 for (auto const& toRemove : result.removableOffers)
369 {
370 if (auto otr = psb.peek(keylet::offer(toRemove)))
371 offerDelete(psb, otr, j_);
372 if (auto otr = psbCancel.peek(keylet::offer(toRemove)))
373 offerDelete(psbCancel, otr, j_);
374 }
375
376 // Determine the size of the final offer after crossing.
377 auto afterCross = takerAmount; // If !tesSUCCESS offer unchanged
378 if (isTesSuccess(result.result()))
379 {
380 STAmount const takerInBalance = accountFunds(psb, account_, takerAmount.in, fhZERO_IF_FROZEN, j_);
381
382 if (takerInBalance <= beast::zero)
383 {
384 // If offer crossing exhausted the account's funds don't
385 // create the offer.
386 afterCross.in.clear();
387 afterCross.out.clear();
388 }
389 else
390 {
391 STAmount const rate{Quality{takerAmount.out, takerAmount.in}.rate()};
392
393 if (txFlags & tfSell)
394 {
395 // If selling then scale the new out amount based on how
396 // much we sold during crossing. This preserves the offer
397 // Quality,
398
399 // Reduce the offer that is placed by the crossed amount.
400 // Note that we must ignore the portion of the
401 // actualAmountIn that may have been consumed by a
402 // gateway's transfer rate.
403 STAmount nonGatewayAmountIn = result.actualAmountIn;
404 if (gatewayXferRate.value != QUALITY_ONE)
405 nonGatewayAmountIn =
406 divideRound(result.actualAmountIn, gatewayXferRate, takerAmount.in.issue(), true);
407
408 afterCross.in -= nonGatewayAmountIn;
409
410 // It's possible that the divRound will cause our subtract
411 // to go slightly negative. So limit afterCross.in to zero.
412 if (afterCross.in < beast::zero)
413 // We should verify that the difference *is* small, but
414 // what is a good threshold to check?
415 afterCross.in.clear();
416
417 afterCross.out = divRoundStrict(afterCross.in, rate, takerAmount.out.issue(), false);
418 }
419 else
420 {
421 // If not selling, we scale the input based on the
422 // remaining output. This too preserves the offer
423 // Quality.
424 afterCross.out -= result.actualAmountOut;
425 XRPL_ASSERT(afterCross.out >= beast::zero, "xrpl::CreateOffer::flowCross : minimum offer");
426 if (afterCross.out < beast::zero)
427 afterCross.out.clear();
428 afterCross.in = mulRound(afterCross.out, rate, takerAmount.in.issue(), true);
429 }
430 }
431 }
432
433 // Return how much of the offer is left.
434 return {tesSUCCESS, afterCross};
435 }
436 catch (std::exception const& e)
437 {
438 JLOG(j_.error()) << "Exception during offer crossing: " << e.what();
439 }
440 return {tecINTERNAL, takerAmount};
441}
442
445{
446 std::string txt = amount.getText();
447 txt += "/";
448 txt += to_string(amount.issue().currency);
449 return txt;
450}
451
452TER
454 Sandbox& sb,
456 Keylet const& offerKey,
457 STAmount const& saTakerPays,
458 STAmount const& saTakerGets,
459 std::function<void(SLE::ref, std::optional<uint256>)> const& setDir)
460{
461 if (!sleOffer->isFieldPresent(sfDomainID))
462 return tecINTERNAL; // LCOV_EXCL_LINE
463
464 // set hybrid flag
465 sleOffer->setFlag(lsfHybrid);
466
467 // if offer is hybrid, need to also place into open offer dir
468 Book const book{saTakerPays.issue(), saTakerGets.issue(), std::nullopt};
469
470 auto dir = keylet::quality(keylet::book(book), getRate(saTakerGets, saTakerPays));
471 bool const bookExists = sb.exists(dir);
472
473 auto const bookNode = sb.dirAppend(dir, offerKey, [&](SLE::ref sle) {
474 // don't set domainID on the directory object since this directory is
475 // for open book
476 setDir(sle, std::nullopt);
477 });
478
479 if (!bookNode)
480 {
481 JLOG(j_.debug()) << "final result: failed to add hybrid offer to open book";
482 return tecDIR_FULL; // LCOV_EXCL_LINE
483 }
484
485 STArray bookArr(sfAdditionalBooks, 1);
486 auto bookInfo = STObject::makeInnerObject(sfBook);
487 bookInfo.setFieldH256(sfBookDirectory, dir.key);
488 bookInfo.setFieldU64(sfBookNode, *bookNode);
489 bookArr.push_back(std::move(bookInfo));
490
491 if (!bookExists)
493
494 sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
495 return tesSUCCESS;
496}
497
500{
501 using beast::zero;
502
503 std::uint32_t const uTxFlags = ctx_.tx.getFlags();
504
505 bool const bPassive(uTxFlags & tfPassive);
506 bool const bImmediateOrCancel(uTxFlags & tfImmediateOrCancel);
507 bool const bFillOrKill(uTxFlags & tfFillOrKill);
508 bool const bSell(uTxFlags & tfSell);
509 bool const bHybrid(uTxFlags & tfHybrid);
510
511 auto saTakerPays = ctx_.tx[sfTakerPays];
512 auto saTakerGets = ctx_.tx[sfTakerGets];
513 auto const domainID = ctx_.tx[~sfDomainID];
514
515 auto const cancelSequence = ctx_.tx[~sfOfferSequence];
516
517 // Note that we we use the value from the sequence or ticket as the
518 // offer sequence. For more explanation see comments in SeqProxy.h.
519 auto const offerSequence = ctx_.tx.getSeqValue();
520
521 // This is the original rate of the offer, and is the rate at which
522 // it will be placed, even if crossing offers change the amounts that
523 // end up on the books.
524 auto uRate = getRate(saTakerGets, saTakerPays);
525
526 auto viewJ = ctx_.app.journal("View");
527
528 TER result = tesSUCCESS;
529
530 // Process a cancellation request that's passed along with an offer.
531 if (cancelSequence)
532 {
533 auto const sleCancel = sb.peek(keylet::offer(account_, *cancelSequence));
534
535 // It's not an error to not find the offer to cancel: it might have
536 // been consumed or removed. If it is found, however, it's an error
537 // to fail to delete it.
538 if (sleCancel)
539 {
540 JLOG(j_.debug()) << "Create cancels order " << *cancelSequence;
541 result = offerDelete(sb, sleCancel, viewJ);
542 }
543 }
544
545 auto const expiration = ctx_.tx[~sfExpiration];
546
547 if (hasExpired(sb, expiration))
548 {
549 // If the offer has expired, the transaction has successfully
550 // done nothing, so short circuit from here.
551 return {tecEXPIRED, true};
552 }
553
554 bool const bOpenLedger = sb.open();
555 bool crossed = false;
556
557 if (result == tesSUCCESS)
558 {
559 // If a tick size applies, round the offer to the tick size
560 auto const& uPaysIssuerID = saTakerPays.getIssuer();
561 auto const& uGetsIssuerID = saTakerGets.getIssuer();
562
563 std::uint8_t uTickSize = Quality::maxTickSize;
564 if (!isXRP(uPaysIssuerID))
565 {
566 auto const sle = sb.read(keylet::account(uPaysIssuerID));
567 if (sle && sle->isFieldPresent(sfTickSize))
568 uTickSize = std::min(uTickSize, (*sle)[sfTickSize]);
569 }
570 if (!isXRP(uGetsIssuerID))
571 {
572 auto const sle = sb.read(keylet::account(uGetsIssuerID));
573 if (sle && sle->isFieldPresent(sfTickSize))
574 uTickSize = std::min(uTickSize, (*sle)[sfTickSize]);
575 }
576 if (uTickSize < Quality::maxTickSize)
577 {
578 auto const rate = Quality{saTakerGets, saTakerPays}.round(uTickSize).rate();
579
580 // We round the side that's not exact,
581 // just as if the offer happened to execute
582 // at a slightly better (for the placer) rate
583 if (bSell)
584 {
585 // this is a sell, round taker pays
586 saTakerPays = multiply(saTakerGets, rate, saTakerPays.issue());
587 }
588 else
589 {
590 // this is a buy, round taker gets
591 saTakerGets = divide(saTakerPays, rate, saTakerGets.issue());
592 }
593 if (!saTakerGets || !saTakerPays)
594 {
595 JLOG(j_.debug()) << "Offer rounded to zero";
596 return {result, true};
597 }
598
599 uRate = getRate(saTakerGets, saTakerPays);
600 }
601
602 // We reverse pays and gets because during crossing we are taking.
603 Amounts const takerAmount(saTakerGets, saTakerPays);
604
605 JLOG(j_.debug()) << "Attempting cross: " << to_string(takerAmount.in.issue()) << " -> "
606 << to_string(takerAmount.out.issue());
607
608 if (auto stream = j_.trace())
609 {
610 stream << " mode: " << (bPassive ? "passive " : "") << (bSell ? "sell" : "buy");
611 stream << " in: " << format_amount(takerAmount.in);
612 stream << " out: " << format_amount(takerAmount.out);
613 }
614
615 // The amount of the offer that is unfilled after crossing has been
616 // performed. It may be equal to the original amount (didn't cross),
617 // empty (fully crossed), or something in-between.
618 Amounts place_offer;
619 PaymentSandbox psbFlow{&sb};
620 PaymentSandbox psbCancelFlow{&sbCancel};
621
622 std::tie(result, place_offer) = flowCross(psbFlow, psbCancelFlow, takerAmount, domainID);
623 psbFlow.apply(sb);
624 psbCancelFlow.apply(sbCancel);
625
626 // We expect the implementation of cross to succeed
627 // or give a tec.
628 XRPL_ASSERT(
629 result == tesSUCCESS || isTecClaim(result),
630 "xrpl::CreateOffer::applyGuts : result is tesSUCCESS or "
631 "tecCLAIM");
632
633 if (auto stream = j_.trace())
634 {
635 stream << "Cross result: " << transToken(result);
636 stream << " in: " << format_amount(place_offer.in);
637 stream << " out: " << format_amount(place_offer.out);
638 }
639
640 if (result == tecFAILED_PROCESSING && bOpenLedger)
641 result = telFAILED_PROCESSING;
642
643 if (result != tesSUCCESS)
644 {
645 JLOG(j_.debug()) << "final result: " << transToken(result);
646 return {result, true};
647 }
648
649 XRPL_ASSERT(
650 saTakerGets.issue() == place_offer.in.issue(), "xrpl::CreateOffer::applyGuts : taker gets issue match");
651 XRPL_ASSERT(
652 saTakerPays.issue() == place_offer.out.issue(), "xrpl::CreateOffer::applyGuts : taker pays issue match");
653
654 if (takerAmount != place_offer)
655 crossed = true;
656
657 // The offer that we need to place after offer crossing should
658 // never be negative. If it is, something went very very wrong.
659 if (place_offer.in < zero || place_offer.out < zero)
660 {
661 JLOG(j_.fatal()) << "Cross left offer negative!"
662 << " in: " << format_amount(place_offer.in)
663 << " out: " << format_amount(place_offer.out);
664 return {tefINTERNAL, true};
665 }
666
667 if (place_offer.in == zero || place_offer.out == zero)
668 {
669 JLOG(j_.debug()) << "Offer fully crossed!";
670 return {result, true};
671 }
672
673 // We now need to adjust the offer to reflect the amount left after
674 // crossing. We reverse in and out here, since during crossing we
675 // were the taker.
676 saTakerPays = place_offer.out;
677 saTakerGets = place_offer.in;
678 }
679
680 XRPL_ASSERT(
681 saTakerPays > zero && saTakerGets > zero, "xrpl::CreateOffer::applyGuts : taker pays and gets positive");
682
683 if (result != tesSUCCESS)
684 {
685 JLOG(j_.debug()) << "final result: " << transToken(result);
686 return {result, true};
687 }
688
689 if (auto stream = j_.trace())
690 {
691 stream << "Place" << (crossed ? " remaining " : " ") << "offer:";
692 stream << " Pays: " << saTakerPays.getFullText();
693 stream << " Gets: " << saTakerGets.getFullText();
694 }
695
696 // For 'fill or kill' offers, failure to fully cross means that the
697 // entire operation should be aborted, with only fees paid.
698 if (bFillOrKill)
699 {
700 JLOG(j_.trace()) << "Fill or Kill: offer killed";
701 return {tecKILLED, false};
702 }
703
704 // For 'immediate or cancel' offers, the amount remaining doesn't get
705 // placed - it gets canceled and the operation succeeds.
706 if (bImmediateOrCancel)
707 {
708 JLOG(j_.trace()) << "Immediate or cancel: offer canceled";
709 if (!crossed)
710 // Any ImmediateOrCancel offer that transfers absolutely no funds
711 // returns tecKILLED rather than tesSUCCESS. Motivation for the
712 // change is here: https://github.com/ripple/rippled/issues/4115
713 return {tecKILLED, false};
714 return {tesSUCCESS, true};
715 }
716
717 auto const sleCreator = sb.peek(keylet::account(account_));
718 if (!sleCreator)
719 return {tefINTERNAL, false};
720
721 {
722 XRPAmount reserve = sb.fees().accountReserve(sleCreator->getFieldU32(sfOwnerCount) + 1);
723
724 if (mPriorBalance < reserve)
725 {
726 // If we are here, the signing account had an insufficient reserve
727 // *prior* to our processing. If something actually crossed, then
728 // we allow this; otherwise, we just claim a fee.
729 if (!crossed)
730 result = tecINSUF_RESERVE_OFFER;
731
732 if (result != tesSUCCESS)
733 {
734 JLOG(j_.debug()) << "final result: " << transToken(result);
735 }
736
737 return {result, true};
738 }
739 }
740
741 // We need to place the remainder of the offer into its order book.
742 auto const offer_index = keylet::offer(account_, offerSequence);
743
744 // Add offer to owner's directory.
745 auto const ownerNode = sb.dirInsert(keylet::ownerDir(account_), offer_index, describeOwnerDir(account_));
746
747 if (!ownerNode)
748 {
749 // LCOV_EXCL_START
750 JLOG(j_.debug()) << "final result: failed to add offer to owner's directory";
751 return {tecDIR_FULL, true};
752 // LCOV_EXCL_STOP
753 }
754
755 // Update owner count.
756 adjustOwnerCount(sb, sleCreator, 1, viewJ);
757
758 JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.issue()) << " : " << to_string(saTakerGets.issue())
759 << (domainID ? (" : " + to_string(*domainID)) : "");
760
761 Book const book{saTakerPays.issue(), saTakerGets.issue(), domainID};
762
763 // Add offer to order book, using the original rate
764 // before any crossing occurred.
765 //
766 // Regular offer - BookDirectory points to open directory
767 //
768 // Domain offer (w/o hybrid) - BookDirectory points to domain
769 // directory
770 //
771 // Hybrid domain offer - BookDirectory points to domain directory,
772 // and AdditionalBooks field stores one entry that points to the open
773 // directory
774 auto dir = keylet::quality(keylet::book(book), uRate);
775 bool const bookExisted = static_cast<bool>(sb.peek(dir));
776
777 auto setBookDir = [&](SLE::ref sle, std::optional<uint256> const& maybeDomain) {
778 sle->setFieldH160(sfTakerPaysCurrency, saTakerPays.issue().currency);
779 sle->setFieldH160(sfTakerPaysIssuer, saTakerPays.issue().account);
780 sle->setFieldH160(sfTakerGetsCurrency, saTakerGets.issue().currency);
781 sle->setFieldH160(sfTakerGetsIssuer, saTakerGets.issue().account);
782 sle->setFieldU64(sfExchangeRate, uRate);
783 if (maybeDomain)
784 sle->setFieldH256(sfDomainID, *maybeDomain);
785 };
786
787 auto const bookNode = sb.dirAppend(dir, offer_index, [&](SLE::ref sle) {
788 // sets domainID on book directory if it's a domain offer
789 setBookDir(sle, domainID);
790 });
791
792 if (!bookNode)
793 {
794 // LCOV_EXCL_START
795 JLOG(j_.debug()) << "final result: failed to add offer to book";
796 return {tecDIR_FULL, true};
797 // LCOV_EXCL_STOP
798 }
799
800 auto sleOffer = std::make_shared<SLE>(offer_index);
801 sleOffer->setAccountID(sfAccount, account_);
802 sleOffer->setFieldU32(sfSequence, offerSequence);
803 sleOffer->setFieldH256(sfBookDirectory, dir.key);
804 sleOffer->setFieldAmount(sfTakerPays, saTakerPays);
805 sleOffer->setFieldAmount(sfTakerGets, saTakerGets);
806 sleOffer->setFieldU64(sfOwnerNode, *ownerNode);
807 sleOffer->setFieldU64(sfBookNode, *bookNode);
808 if (expiration)
809 sleOffer->setFieldU32(sfExpiration, *expiration);
810 if (bPassive)
811 sleOffer->setFlag(lsfPassive);
812 if (bSell)
813 sleOffer->setFlag(lsfSell);
814 if (domainID)
815 sleOffer->setFieldH256(sfDomainID, *domainID);
816
817 // if it's a hybrid offer, set hybrid flag, and create an open dir
818 if (bHybrid)
819 {
820 auto const res = applyHybrid(sb, sleOffer, offer_index, saTakerPays, saTakerGets, setBookDir);
821 if (res != tesSUCCESS)
822 return {res, true}; // LCOV_EXCL_LINE
823 }
824
825 sb.insert(sleOffer);
826
827 if (!bookExisted)
829
830 JLOG(j_.debug()) << "final result: success";
831
832 return {tesSUCCESS, true};
833}
834
835TER
837{
838 // This is the ledger view that we work against. Transactions are applied
839 // as we go on processing transactions.
840 Sandbox sb(&ctx_.view());
841
842 // This is a ledger with just the fees paid and any unfunded or expired
843 // offers we encounter removed. It's used when handling Fill-or-Kill offers,
844 // if the order isn't going to be placed, to avoid wasting the work we did.
845 Sandbox sbCancel(&ctx_.view());
846
847 auto const result = applyGuts(sb, sbCancel);
848 if (result.second)
849 sb.apply(ctx_.rawView());
850 else
851 sbCancel.apply(ctx_.rawView());
852 return result.first;
853}
854
855} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:41
Stream fatal() const
Definition Journal.h:325
Stream error() const
Definition Journal.h:319
Stream debug() const
Definition Journal.h:301
Stream trace() const
Severity stream access functions.
Definition Journal.h:295
virtual beast::Journal journal(std::string const &name)=0
virtual OrderBookDB & getOrderBookDB()=0
STTx const & tx
RawView & rawView()
ApplyView & view()
Application & app
std::optional< std::uint64_t > dirInsert(Keylet const &directory, uint256 const &key, std::function< void(std::shared_ptr< SLE > const &)> const &describe)
Insert an entry to a directory.
Definition ApplyView.h:284
std::optional< std::uint64_t > dirAppend(Keylet const &directory, Keylet const &key, std::function< void(std::shared_ptr< SLE > const &)> const &describe)
Append an entry to a directory.
Definition ApplyView.h:245
Specifies an order book.
Definition Book.h:17
static TER preclaim(PreclaimContext const &ctx)
Enforce constraints beyond those of the Transactor base class.
TER doApply() override
Precondition: fee collection is likely.
static std::uint32_t getFlagsMask(PreflightContext const &ctx)
static TxConsequences makeTxConsequences(PreflightContext const &ctx)
static bool checkExtraFeatures(PreflightContext const &ctx)
static std::string format_amount(STAmount const &amount)
TER applyHybrid(Sandbox &sb, std::shared_ptr< STLedgerEntry > sleOffer, Keylet const &offer_index, STAmount const &saTakerPays, STAmount const &saTakerGets, std::function< void(SLE::ref, std::optional< uint256 >)> const &setDir)
std::pair< TER, Amounts > flowCross(PaymentSandbox &psb, PaymentSandbox &psbCancel, Amounts const &takerAmount, std::optional< uint256 > const &domainID)
std::pair< TER, bool > applyGuts(Sandbox &view, Sandbox &view_cancel)
static NotTEC preflight(PreflightContext const &ctx)
Enforce constraints beyond those of the Transactor base class.
static TER checkAcceptAsset(ReadView const &view, ApplyFlags const flags, AccountID const id, beast::Journal const j, Issue const &issue)
A currency issued by an account.
Definition Issue.h:14
Currency currency
Definition Issue.h:16
AccountID account
Definition Issue.h:17
void addOrderBook(Book const &)
A wrapper which makes credits unavailable to balances.
A view into a ledger.
Definition ReadView.h:32
virtual std::shared_ptr< SLE const > read(Keylet const &k) const =0
Return the state item associated with a key.
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:118
std::string getFullText() const override
Definition STAmount.cpp:629
static constexpr std::uint64_t cMaxValue
Definition STAmount.h:52
Issue const & issue() const
Definition STAmount.h:455
static int const cMaxOffset
Definition STAmount.h:47
Currency const & getCurrency() const
Definition STAmount.h:461
bool native() const noexcept
Definition STAmount.h:417
static constexpr std::uint64_t cMaxNative
Definition STAmount.h:54
AccountID const & getIssuer() const
Definition STAmount.h:467
void push_back(STObject const &object)
Definition STArray.h:188
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:439
static STObject makeInnerObject(SField const &name)
Definition STObject.cpp:72
std::uint32_t getFlags() const
Definition STObject.cpp:492
void emplace_back(Args &&... args)
Definition STPathSet.h:481
void emplace_back(Args &&... args)
Definition STPathSet.h:377
std::uint32_t getSeqValue() const
Returns the first non-zero value of (Sequence, TicketSequence).
Definition STTx.cpp:208
Discardable, editable view to a ledger.
Definition Sandbox.h:16
void apply(RawView &to)
Definition Sandbox.h:36
AccountID const account_
Definition Transactor.h:113
beast::Journal const j_
Definition Transactor.h:111
ApplyView & view()
Definition Transactor.h:129
XRPAmount mPriorBalance
Definition Transactor.h:114
ApplyContext & ctx_
Definition Transactor.h:109
Class describing the consequences to the account of applying a transaction if the transaction consume...
Definition applySteps.h:38
bool open() const override
Returns true if this reflects an open ledger.
std::shared_ptr< SLE > peek(Keylet const &k) override
Prepare to modify the SLE associated with key.
std::shared_ptr< SLE const > read(Keylet const &k) const override
Return the state item associated with a key.
Fees const & fees() const override
Returns the fees for the base ledger.
void insert(std::shared_ptr< SLE > const &sle) override
Insert a new state SLE.
bool exists(Keylet const &k) const override
Determine if a state item exists.
T is_same_v
T min(T... args)
Keylet quality(Keylet const &k, std::uint64_t q) noexcept
The initial directory page for a specific quality.
Definition Indexes.cpp:241
static book_t const book
Definition Indexes.h:86
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:325
Keylet offer(AccountID const &id, std::uint32_t seq) noexcept
An offer from an account.
Definition Indexes.cpp:235
Keylet line(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:214
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:160
bool accountInDomain(ReadView const &view, AccountID const &account, Domain const &domainID)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
constexpr std::uint32_t tfSell
Definition TxFlags.h:82
@ telFAILED_PROCESSING
Definition TER.h:37
STAmount divide(STAmount const &amount, Rate const &rate)
Definition Rate2.cpp:69
@ terNO_LINE
Definition TER.h:200
@ terNO_AUTH
Definition TER.h:199
@ terNO_ACCOUNT
Definition TER.h:198
constexpr std::uint32_t tfPassive
Definition TxFlags.h:79
constexpr std::uint32_t tfImmediateOrCancel
Definition TxFlags.h:80
@ fhZERO_IF_FROZEN
Definition View.h:59
constexpr std::uint32_t tfFillOrKill
Definition TxFlags.h:81
bool isXRP(AccountID const &c)
Definition AccountID.h:71
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:598
constexpr std::uint32_t tfHybrid
Definition TxFlags.h:83
bool hasExpired(ReadView const &view, std::optional< std::uint32_t > const &exp)
Determines whether the given expiration time has passed.
Definition View.cpp:129
@ tefINTERNAL
Definition TER.h:154
bool isLegalNet(STAmount const &value)
Definition STAmount.h:567
constexpr std::uint32_t tfOfferCreateMask
Definition TxFlags.h:84
STAmount multiply(STAmount const &amount, Rate const &rate)
Definition Rate2.cpp:34
std::string transToken(TER code)
Definition TER.cpp:243
Currency const & xrpCurrency()
XRP currency.
Definition UintTypes.cpp:96
STAmount accountFunds(ReadView const &view, AccountID const &id, STAmount const &saDefault, FreezeHandling freezeHandling, beast::Journal j)
Definition View.cpp:515
bool isGlobalFrozen(ReadView const &view, AccountID const &issuer)
Definition View.cpp:138
void adjustOwnerCount(ApplyView &view, std::shared_ptr< SLE > const &sle, std::int32_t amount, beast::Journal j)
Adjust the owner count up or down.
Definition View.cpp:941
StrandResult< TInAmt, TOutAmt > flow(PaymentSandbox const &baseView, Strand const &strand, std::optional< TInAmt > const &maxIn, TOutAmt const &out, beast::Journal j)
Request out amount from a strand.
Definition StrandFlow.h:82
Rate transferRate(ReadView const &view, AccountID const &issuer)
Returns IOU issuer transfer fee as Rate.
Definition View.cpp:699
STAmount multiplyRound(STAmount const &amount, Rate const &rate, bool roundUp)
Definition Rate2.cpp:45
std::uint64_t getRate(STAmount const &offerOut, STAmount const &offerIn)
Definition STAmount.cpp:434
std::function< void(SLE::ref)> describeOwnerDir(AccountID const &account)
Definition View.cpp:955
TER offerDelete(ApplyView &view, std::shared_ptr< SLE > const &sle, beast::Journal j)
Delete an offer.
Definition View.cpp:1672
ApplyFlags
Definition ApplyView.h:11
@ tapRETRY
Definition ApplyView.h:20
STAmount mulRound(STAmount const &v1, STAmount const &v2, Asset const &asset, bool roundUp)
@ temBAD_ISSUER
Definition TER.h:74
@ temBAD_CURRENCY
Definition TER.h:71
@ temBAD_EXPIRATION
Definition TER.h:72
@ temBAD_SEQUENCE
Definition TER.h:85
@ temINVALID_FLAG
Definition TER.h:92
@ temBAD_AMOUNT
Definition TER.h:70
@ temBAD_OFFER
Definition TER.h:76
@ temREDUNDANT
Definition TER.h:93
bool isTesSuccess(TER x) noexcept
Definition TER.h:650
STAmount divRoundStrict(STAmount const &v1, STAmount const &v2, Asset const &asset, bool roundUp)
@ tecINSUF_RESERVE_OFFER
Definition TER.h:271
@ tecDIR_FULL
Definition TER.h:269
@ tecNO_AUTH
Definition TER.h:282
@ tecINTERNAL
Definition TER.h:292
@ tecFAILED_PROCESSING
Definition TER.h:268
@ tecFROZEN
Definition TER.h:285
@ tecUNFUNDED_OFFER
Definition TER.h:266
@ tecEXPIRED
Definition TER.h:296
@ tecNO_LINE
Definition TER.h:283
@ tecKILLED
Definition TER.h:298
@ tecNO_PERMISSION
Definition TER.h:287
@ tecNO_ISSUER
Definition TER.h:281
bool isTecClaim(TER x) noexcept
Definition TER.h:657
@ lsfRequireAuth
@ lsfLowDeepFreeze
@ lsfHighDeepFreeze
@ lsfHighAuth
OfferCrossing
Definition Steps.h:26
@ sell
Definition Steps.h:26
@ yes
Definition Steps.h:26
Currency const & badCurrency()
We deliberately disallow the currency that looks like "XRP" because too many people were using it ins...
STAmount divideRound(STAmount const &amount, Rate const &rate, bool roundUp)
Definition Rate2.cpp:80
@ tesSUCCESS
Definition TER.h:226
XRPAmount accountReserve(std::size_t ownerCount) const
Returns the account reserve given the owner count, in drops.
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:54
ReadView const & view
Definition Transactor.h:57
Application & app
Definition Transactor.h:56
beast::Journal const j
Definition Transactor.h:62
State information when preflighting a tx.
Definition Transactor.h:16
beast::Journal const j
Definition Transactor.h:23
Represents a transfer rate.
Definition Rate.h:21
T tie(T... args)
T what(T... args)