rippled
Loading...
Searching...
No Matches
PaySteps.cpp
1#include <xrpld/app/paths/detail/Steps.h>
2
3#include <xrpl/basics/contract.h>
4#include <xrpl/json/json_writer.h>
5#include <xrpl/ledger/ReadView.h>
6#include <xrpl/protocol/IOUAmount.h>
7#include <xrpl/protocol/XRPAmount.h>
8
9#include <algorithm>
10
11namespace ripple {
12
13// Check equal with tolerance
14bool
15checkNear(IOUAmount const& expected, IOUAmount const& actual)
16{
17 double const ratTol = 0.001;
18 if (abs(expected.exponent() - actual.exponent()) > 1)
19 return false;
20
21 if (actual.exponent() < -20)
22 return true;
23
24 auto const a = (expected.exponent() < actual.exponent())
25 ? expected.mantissa() / 10
26 : expected.mantissa();
27 auto const b = (actual.exponent() < expected.exponent())
28 ? actual.mantissa() / 10
29 : actual.mantissa();
30 if (a == b)
31 return true;
32
33 double const diff = std::abs(a - b);
34 auto const r = diff / std::max(std::abs(a), std::abs(b));
35 return r <= ratTol;
36};
37
38bool
39checkNear(XRPAmount const& expected, XRPAmount const& actual)
40{
41 return expected == actual;
42};
43
44static bool
46{
48 return false;
49 return isXRP(pe.getAccountID());
50};
51
54 StrandContext const& ctx,
55 STPathElement const* e1,
56 STPathElement const* e2,
57 Issue const& curIssue)
58{
59 auto& j = ctx.j;
60
61 if (ctx.isFirst && e1->isAccount() &&
63 isXRP(e1->getCurrency()))
64 {
65 return make_XRPEndpointStep(ctx, e1->getAccountID());
66 }
67
68 if (ctx.isLast && isXRPAccount(*e1) && e2->isAccount())
69 return make_XRPEndpointStep(ctx, e2->getAccountID());
70
71 if (e1->isAccount() && e2->isAccount())
72 {
73 return make_DirectStepI(
74 ctx, e1->getAccountID(), e2->getAccountID(), curIssue.currency);
75 }
76
77 if (e1->isOffer() && e2->isAccount())
78 {
79 // LCOV_EXCL_START
80 // should already be taken care of
81 JLOG(j.error())
82 << "Found offer/account payment step. Aborting payment strand.";
83 UNREACHABLE("ripple::toStep : offer/account payment payment strand");
85 // LCOV_EXCL_STOP
86 }
87
88 XRPL_ASSERT(
91 "ripple::toStep : currency or issuer");
92 auto const outCurrency = e2->getNodeType() & STPathElement::typeCurrency
93 ? e2->getCurrency()
94 : curIssue.currency;
95 auto const outIssuer = e2->getNodeType() & STPathElement::typeIssuer
96 ? e2->getIssuerID()
97 : curIssue.account;
98
99 if (isXRP(curIssue.currency) && isXRP(outCurrency))
100 {
101 JLOG(j.info()) << "Found xrp/xrp offer payment step";
103 }
104
105 XRPL_ASSERT(e2->isOffer(), "ripple::toStep : is offer");
106
107 if (isXRP(outCurrency))
108 return make_BookStepIX(ctx, curIssue);
109
110 if (isXRP(curIssue.currency))
111 return make_BookStepXI(ctx, {outCurrency, outIssuer});
112
113 return make_BookStepII(ctx, curIssue, {outCurrency, outIssuer});
114}
115
118 ReadView const& view,
119 AccountID const& src,
120 AccountID const& dst,
121 Issue const& deliver,
122 std::optional<Quality> const& limitQuality,
123 std::optional<Issue> const& sendMaxIssue,
124 STPath const& path,
125 bool ownerPaysTransferFee,
126 OfferCrossing offerCrossing,
127 AMMContext& ammContext,
128 std::optional<uint256> const& domainID,
130{
131 if (isXRP(src) || isXRP(dst) || !isConsistent(deliver) ||
132 (sendMaxIssue && !isConsistent(*sendMaxIssue)))
133 return {temBAD_PATH, Strand{}};
134
135 if ((sendMaxIssue && sendMaxIssue->account == noAccount()) ||
136 (src == noAccount()) || (dst == noAccount()) ||
137 (deliver.account == noAccount()))
138 return {temBAD_PATH, Strand{}};
139
140 for (auto const& pe : path)
141 {
142 auto const t = pe.getNodeType();
143
144 if ((t & ~STPathElement::typeAll) || !t)
145 return {temBAD_PATH, Strand{}};
146
147 bool const hasAccount = t & STPathElement::typeAccount;
148 bool const hasIssuer = t & STPathElement::typeIssuer;
149 bool const hasCurrency = t & STPathElement::typeCurrency;
150
151 if (hasAccount && (hasIssuer || hasCurrency))
152 return {temBAD_PATH, Strand{}};
153
154 if (hasIssuer && isXRP(pe.getIssuerID()))
155 return {temBAD_PATH, Strand{}};
156
157 if (hasAccount && isXRP(pe.getAccountID()))
158 return {temBAD_PATH, Strand{}};
159
160 if (hasCurrency && hasIssuer &&
161 isXRP(pe.getCurrency()) != isXRP(pe.getIssuerID()))
162 return {temBAD_PATH, Strand{}};
163
164 if (hasIssuer && (pe.getIssuerID() == noAccount()))
165 return {temBAD_PATH, Strand{}};
166
167 if (hasAccount && (pe.getAccountID() == noAccount()))
168 return {temBAD_PATH, Strand{}};
169 }
170
171 Issue curIssue = [&] {
172 auto const& currency =
173 sendMaxIssue ? sendMaxIssue->currency : deliver.currency;
174 if (isXRP(currency))
175 return xrpIssue();
176 return Issue{currency, src};
177 }();
178
179 auto hasCurrency = [](STPathElement const pe) {
180 return pe.getNodeType() & STPathElement::typeCurrency;
181 };
182
184 // reserve enough for the path, the implied source, destination,
185 // sendmax and deliver.
186 normPath.reserve(4 + path.size());
187 {
188 normPath.emplace_back(
189 STPathElement::typeAll, src, curIssue.currency, curIssue.account);
190
191 if (sendMaxIssue && sendMaxIssue->account != src &&
192 (path.empty() || !path[0].isAccount() ||
193 path[0].getAccountID() != sendMaxIssue->account))
194 {
195 normPath.emplace_back(
196 sendMaxIssue->account, std::nullopt, std::nullopt);
197 }
198
199 for (auto const& i : path)
200 normPath.push_back(i);
201
202 {
203 // Note that for offer crossing (only) we do use an offer book
204 // even if all that is changing is the Issue.account.
205 STPathElement const& lastCurrency =
206 *std::find_if(normPath.rbegin(), normPath.rend(), hasCurrency);
207 if ((lastCurrency.getCurrency() != deliver.currency) ||
208 (offerCrossing &&
209 lastCurrency.getIssuerID() != deliver.account))
210 {
211 normPath.emplace_back(
212 std::nullopt, deliver.currency, deliver.account);
213 }
214 }
215
216 if (!((normPath.back().isAccount() &&
217 normPath.back().getAccountID() == deliver.account) ||
218 (dst == deliver.account)))
219 {
220 normPath.emplace_back(deliver.account, std::nullopt, std::nullopt);
221 }
222
223 if (!normPath.back().isAccount() ||
224 normPath.back().getAccountID() != dst)
225 {
226 normPath.emplace_back(dst, std::nullopt, std::nullopt);
227 }
228 }
229
230 if (normPath.size() < 2)
231 return {temBAD_PATH, Strand{}};
232
233 auto const strandSrc = normPath.front().getAccountID();
234 auto const strandDst = normPath.back().getAccountID();
235 bool const isDefaultPath = path.empty();
236
237 Strand result;
238 result.reserve(2 * normPath.size());
239
240 /* A strand may not include the same account node more than once
241 in the same currency. In a direct step, an account will show up
242 at most twice: once as a src and once as a dst (hence the two element
243 array). The strandSrc and strandDst will only show up once each.
244 */
246 // A strand may not include the same offer book more than once
247 boost::container::flat_set<Issue> seenBookOuts;
248 seenDirectIssues[0].reserve(normPath.size());
249 seenDirectIssues[1].reserve(normPath.size());
250 seenBookOuts.reserve(normPath.size());
251 auto ctx = [&](bool isLast = false) {
252 return StrandContext{
253 view,
254 result,
255 strandSrc,
256 strandDst,
257 deliver,
258 limitQuality,
259 isLast,
260 ownerPaysTransferFee,
261 offerCrossing,
263 seenDirectIssues,
264 seenBookOuts,
265 ammContext,
266 domainID,
267 j};
268 };
269
270 for (std::size_t i = 0; i < normPath.size() - 1; ++i)
271 {
272 /* Iterate through the path elements considering them in pairs.
273 The first element of the pair is `cur` and the second element is
274 `next`. When an offer is one of the pairs, the step created will be
275 for `next`. This means when `cur` is an offer and `next` is an
276 account then no step is created, as a step has already been created
277 for that offer.
278 */
280 auto cur = &normPath[i];
281 auto const next = &normPath[i + 1];
282
283 if (cur->isAccount())
284 curIssue.account = cur->getAccountID();
285 else if (cur->hasIssuer())
286 curIssue.account = cur->getIssuerID();
287
288 if (cur->hasCurrency())
289 {
290 curIssue.currency = cur->getCurrency();
291 if (isXRP(curIssue.currency))
292 curIssue.account = xrpAccount();
293 }
294
295 if (cur->isAccount() && next->isAccount())
296 {
297 if (!isXRP(curIssue.currency) &&
298 curIssue.account != cur->getAccountID() &&
299 curIssue.account != next->getAccountID())
300 {
301 JLOG(j.trace()) << "Inserting implied account";
302 auto msr = make_DirectStepI(
303 ctx(),
304 cur->getAccountID(),
305 curIssue.account,
306 curIssue.currency);
307 if (msr.first != tesSUCCESS)
308 return {msr.first, Strand{}};
309 result.push_back(std::move(msr.second));
310 impliedPE.emplace(
312 curIssue.account,
313 xrpCurrency(),
314 xrpAccount());
315 cur = &*impliedPE;
316 }
317 }
318 else if (cur->isAccount() && next->isOffer())
319 {
320 if (curIssue.account != cur->getAccountID())
321 {
322 JLOG(j.trace()) << "Inserting implied account before offer";
323 auto msr = make_DirectStepI(
324 ctx(),
325 cur->getAccountID(),
326 curIssue.account,
327 curIssue.currency);
328 if (msr.first != tesSUCCESS)
329 return {msr.first, Strand{}};
330 result.push_back(std::move(msr.second));
331 impliedPE.emplace(
333 curIssue.account,
334 xrpCurrency(),
335 xrpAccount());
336 cur = &*impliedPE;
337 }
338 }
339 else if (cur->isOffer() && next->isAccount())
340 {
341 if (curIssue.account != next->getAccountID() &&
342 !isXRP(next->getAccountID()))
343 {
344 if (isXRP(curIssue))
345 {
346 if (i != normPath.size() - 2)
347 return {temBAD_PATH, Strand{}};
348 else
349 {
350 // Last step. insert xrp endpoint step
351 auto msr =
352 make_XRPEndpointStep(ctx(), next->getAccountID());
353 if (msr.first != tesSUCCESS)
354 return {msr.first, Strand{}};
355 result.push_back(std::move(msr.second));
356 }
357 }
358 else
359 {
360 JLOG(j.trace()) << "Inserting implied account after offer";
361 auto msr = make_DirectStepI(
362 ctx(),
363 curIssue.account,
364 next->getAccountID(),
365 curIssue.currency);
366 if (msr.first != tesSUCCESS)
367 return {msr.first, Strand{}};
368 result.push_back(std::move(msr.second));
369 }
370 }
371 continue;
372 }
373
374 if (!next->isOffer() && next->hasCurrency() &&
375 next->getCurrency() != curIssue.currency)
376 {
377 // Should never happen
378 // LCOV_EXCL_START
379 UNREACHABLE("ripple::toStrand : offer currency mismatch");
380 return {temBAD_PATH, Strand{}};
381 // LCOV_EXCL_STOP
382 }
383
384 auto s = toStep(
385 ctx(/*isLast*/ i == normPath.size() - 2), cur, next, curIssue);
386 if (s.first == tesSUCCESS)
387 result.emplace_back(std::move(s.second));
388 else
389 {
390 JLOG(j.debug()) << "toStep failed: " << s.first;
391 return {s.first, Strand{}};
392 }
393 }
394
395 auto checkStrand = [&]() -> bool {
396 auto stepAccts = [](Step const& s) -> std::pair<AccountID, AccountID> {
397 if (auto r = s.directStepAccts())
398 return *r;
399 if (auto const r = s.bookStepBook())
400 return std::make_pair(r->in.account, r->out.account);
401 Throw<FlowException>(
402 tefEXCEPTION, "Step should be either a direct or book step");
404 };
405
406 auto curAcc = src;
407 auto curIss = [&] {
408 auto& currency =
409 sendMaxIssue ? sendMaxIssue->currency : deliver.currency;
410 if (isXRP(currency))
411 return xrpIssue();
412 return Issue{currency, src};
413 }();
414
415 for (auto const& s : result)
416 {
417 auto const accts = stepAccts(*s);
418 if (accts.first != curAcc)
419 return false;
420
421 if (auto const b = s->bookStepBook())
422 {
423 if (curIss != b->in)
424 return false;
425 curIss = b->out;
426 }
427 else
428 {
429 curIss.account = accts.second;
430 }
431
432 curAcc = accts.second;
433 }
434 if (curAcc != dst)
435 return false;
436 if (curIss.currency != deliver.currency)
437 return false;
438 if (curIss.account != deliver.account && curIss.account != dst)
439 return false;
440 return true;
441 };
442
443 if (!checkStrand())
444 {
445 // LCOV_EXCL_START
446 JLOG(j.warn()) << "Flow check strand failed";
447 UNREACHABLE("ripple::toStrand : invalid strand");
448 return {temBAD_PATH, Strand{}};
449 // LCOV_EXCL_STOP
450 }
451
452 return {tesSUCCESS, std::move(result)};
453}
454
457 ReadView const& view,
458 AccountID const& src,
459 AccountID const& dst,
460 Issue const& deliver,
461 std::optional<Quality> const& limitQuality,
462 std::optional<Issue> const& sendMax,
463 STPathSet const& paths,
464 bool addDefaultPath,
465 bool ownerPaysTransferFee,
466 OfferCrossing offerCrossing,
467 AMMContext& ammContext,
468 std::optional<uint256> const& domainID,
470{
471 std::vector<Strand> result;
472 result.reserve(1 + paths.size());
473 // Insert the strand into result if it is not already part of the vector
474 auto insert = [&](Strand s) {
475 bool const hasStrand =
476 std::find(result.begin(), result.end(), s) != result.end();
477
478 if (!hasStrand)
479 result.emplace_back(std::move(s));
480 };
481
482 if (addDefaultPath)
483 {
484 auto sp = toStrand(
485 view,
486 src,
487 dst,
488 deliver,
489 limitQuality,
490 sendMax,
491 STPath(),
492 ownerPaysTransferFee,
493 offerCrossing,
494 ammContext,
495 domainID,
496 j);
497 auto const ter = sp.first;
498 auto& strand = sp.second;
499
500 if (ter != tesSUCCESS)
501 {
502 JLOG(j.trace()) << "failed to add default path";
503 if (isTemMalformed(ter) || paths.empty())
504 {
505 return {ter, std::vector<Strand>{}};
506 }
507 }
508 else if (strand.empty())
509 {
510 JLOG(j.trace()) << "toStrand failed";
511 Throw<FlowException>(
512 tefEXCEPTION, "toStrand returned tes & empty strand");
513 }
514 else
515 {
516 insert(std::move(strand));
517 }
518 }
519 else if (paths.empty())
520 {
521 JLOG(j.debug()) << "Flow: Invalid transaction: No paths and direct "
522 "ripple not allowed.";
524 }
525
526 TER lastFailTer = tesSUCCESS;
527 for (auto const& p : paths)
528 {
529 auto sp = toStrand(
530 view,
531 src,
532 dst,
533 deliver,
534 limitQuality,
535 sendMax,
536 p,
537 ownerPaysTransferFee,
538 offerCrossing,
539 ammContext,
540 domainID,
541 j);
542 auto ter = sp.first;
543 auto& strand = sp.second;
544
545 if (ter != tesSUCCESS)
546 {
547 lastFailTer = ter;
548 JLOG(j.trace()) << "failed to add path: ter: " << ter
549 << "path: " << p.getJson(JsonOptions::none);
550 if (isTemMalformed(ter))
551 return {ter, std::vector<Strand>{}};
552 }
553 else if (strand.empty())
554 {
555 JLOG(j.trace()) << "toStrand failed";
556 Throw<FlowException>(
557 tefEXCEPTION, "toStrand returned tes & empty strand");
558 }
559 else
560 {
561 insert(std::move(strand));
562 }
563 }
564
565 if (result.empty())
566 return {lastFailTer, std::move(result)};
567
568 return {tesSUCCESS, std::move(result)};
569}
570
572 ReadView const& view_,
573 std::vector<std::unique_ptr<Step>> const& strand_,
574 // A strand may not include an inner node that
575 // replicates the source or destination.
576 AccountID const& strandSrc_,
577 AccountID const& strandDst_,
578 Issue const& strandDeliver_,
579 std::optional<Quality> const& limitQuality_,
580 bool isLast_,
581 bool ownerPaysTransferFee_,
582 OfferCrossing offerCrossing_,
583 bool isDefaultPath_,
584 std::array<boost::container::flat_set<Issue>, 2>& seenDirectIssues_,
585 boost::container::flat_set<Issue>& seenBookOuts_,
586 AMMContext& ammContext_,
587 std::optional<uint256> const& domainID_,
589 : view(view_)
590 , strandSrc(strandSrc_)
591 , strandDst(strandDst_)
592 , strandDeliver(strandDeliver_)
593 , limitQuality(limitQuality_)
594 , isFirst(strand_.empty())
595 , isLast(isLast_)
596 , ownerPaysTransferFee(ownerPaysTransferFee_)
597 , offerCrossing(offerCrossing_)
598 , isDefaultPath(isDefaultPath_)
599 , strandSize(strand_.size())
600 , prevStep(!strand_.empty() ? strand_.back().get() : nullptr)
601 , seenDirectIssues(seenDirectIssues_)
602 , seenBookOuts(seenBookOuts_)
603 , ammContext(ammContext_)
604 , domainID(domainID_)
605 , j(j_)
606{
607}
608
609template <class InAmt, class OutAmt>
610bool
611isDirectXrpToXrp(Strand const& strand)
612{
613 return false;
614}
615
616template <>
617bool
618isDirectXrpToXrp<XRPAmount, XRPAmount>(Strand const& strand)
619{
620 return (strand.size() == 2);
621}
622
623template bool
624isDirectXrpToXrp<XRPAmount, IOUAmount>(Strand const& strand);
625template bool
626isDirectXrpToXrp<IOUAmount, XRPAmount>(Strand const& strand);
627template bool
628isDirectXrpToXrp<IOUAmount, IOUAmount>(Strand const& strand);
629
630} // namespace ripple
T back(T... args)
T begin(T... args)
A generic endpoint for log messages.
Definition Journal.h:41
Stream debug() const
Definition Journal.h:309
Stream trace() const
Severity stream access functions.
Definition Journal.h:303
Stream warn() const
Definition Journal.h:321
Maintains AMM info per overall payment engine execution and individual iteration.
Definition AMMContext.h:17
Floating point representation of amounts with high dynamic range.
Definition IOUAmount.h:27
int exponent() const noexcept
Definition IOUAmount.h:153
std::int64_t mantissa() const noexcept
Definition IOUAmount.h:159
A currency issued by an account.
Definition Issue.h:14
AccountID account
Definition Issue.h:17
Currency currency
Definition Issue.h:16
A view into a ledger.
Definition ReadView.h:32
Currency const & getCurrency() const
Definition STPathSet.h:347
AccountID const & getAccountID() const
Definition STPathSet.h:341
bool isOffer() const
Definition STPathSet.h:309
AccountID const & getIssuerID() const
Definition STPathSet.h:353
auto getNodeType() const
Definition STPathSet.h:303
bool isAccount() const
Definition STPathSet.h:315
bool empty() const
Definition STPathSet.h:489
std::vector< STPath >::size_type size() const
Definition STPathSet.h:483
A step in a payment path.
Definition Steps.h:67
T emplace_back(T... args)
T emplace(T... args)
T empty(T... args)
T end(T... args)
T find_if(T... args)
T front(T... args)
T is_same_v
T make_pair(T... args)
T max(T... args)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:96
AccountID const & noAccount()
A placeholder for empty accounts.
static std::pair< TER, std::unique_ptr< Step > > toStep(StrandContext const &ctx, STPathElement const *e1, STPathElement const *e2, Issue const &curIssue)
Definition PaySteps.cpp:53
bool isConsistent(Book const &book)
Definition Book.cpp:10
bool isXRP(AccountID const &c)
Definition AccountID.h:71
AccountID const & xrpAccount()
Compute AccountID from public key.
std::pair< TER, std::unique_ptr< Step > > make_XRPEndpointStep(StrandContext const &ctx, AccountID const &acc)
template bool isDirectXrpToXrp< IOUAmount, IOUAmount >(Strand const &strand)
static bool isDefaultPath(STPath const &path)
@ tefEXCEPTION
Definition TER.h:153
template bool isDirectXrpToXrp< IOUAmount, XRPAmount >(Strand const &strand)
OfferCrossing
Definition Steps.h:26
std::pair< TER, std::unique_ptr< Step > > make_BookStepXI(StrandContext const &ctx, Issue const &out)
std::pair< TER, std::unique_ptr< Step > > make_BookStepIX(StrandContext const &ctx, Issue const &in)
template bool isDirectXrpToXrp< XRPAmount, IOUAmount >(Strand const &strand)
Currency const & xrpCurrency()
XRP currency.
bool checkNear(IOUAmount const &expected, IOUAmount const &actual)
Definition PaySteps.cpp:15
@ tesSUCCESS
Definition TER.h:226
std::pair< TER, std::unique_ptr< Step > > make_BookStepII(StrandContext const &ctx, Issue const &in, Issue const &out)
std::pair< TER, std::unique_ptr< Step > > make_DirectStepI(StrandContext const &ctx, AccountID const &src, AccountID const &dst, Currency const &c)
bool isDirectXrpToXrp(Strand const &strand)
Definition PaySteps.cpp:611
T get(Section const &section, std::string const &name, T const &defaultValue=T{})
Retrieve a key/value pair from a section.
bool isTemMalformed(TER x) noexcept
Definition TER.h:641
static bool isXRPAccount(STPathElement const &pe)
Definition PaySteps.cpp:45
std::pair< TER, std::vector< Strand > > toStrands(ReadView const &view, AccountID const &src, AccountID const &dst, Issue const &deliver, std::optional< Quality > const &limitQuality, std::optional< Issue > const &sendMax, STPathSet const &paths, bool addDefaultPath, bool ownerPaysTransferFee, OfferCrossing offerCrossing, AMMContext &ammContext, std::optional< uint256 > const &domainID, beast::Journal j)
Create a Strand for each specified path (including the default path, if indicated)
Definition PaySteps.cpp:456
std::pair< TER, Strand > toStrand(ReadView const &view, AccountID const &src, AccountID const &dst, Issue const &deliver, std::optional< Quality > const &limitQuality, std::optional< Issue > const &sendMaxIssue, STPath const &path, bool ownerPaysTransferFee, OfferCrossing offerCrossing, AMMContext &ammContext, std::optional< uint256 > const &domainID, beast::Journal j)
Create a Strand for the specified path.
Definition PaySteps.cpp:117
bool isDirectXrpToXrp< XRPAmount, XRPAmount >(Strand const &strand)
Definition PaySteps.cpp:618
constexpr Number abs(Number x) noexcept
Definition Number.h:331
@ temBAD_PATH
Definition TER.h:77
@ temRIPPLE_EMPTY
Definition TER.h:94
T push_back(T... args)
T rbegin(T... args)
T rend(T... args)
T reserve(T... args)
T size(T... args)
Context needed to build Strand Steps and for error checking.
Definition Steps.h:514
beast::Journal const j
Definition Steps.h:543
bool const isFirst
true if Step is first in Strand
Definition Steps.h:520
bool const isLast
true if Step is last in Strand
Definition Steps.h:521
StrandContext(ReadView const &view_, std::vector< std::unique_ptr< Step > > const &strand_, AccountID const &strandSrc_, AccountID const &strandDst_, Issue const &strandDeliver_, std::optional< Quality > const &limitQuality_, bool isLast_, bool ownerPaysTransferFee_, OfferCrossing offerCrossing_, bool isDefaultPath_, std::array< boost::container::flat_set< Issue >, 2 > &seenDirectIssues_, boost::container::flat_set< Issue > &seenBookOuts_, AMMContext &ammContext_, std::optional< uint256 > const &domainID, beast::Journal j_)
StrandContext constructor.
Definition PaySteps.cpp:571