xrpld
Loading...
Searching...
No Matches
FlowMPT_test.cpp
1#include <test/jtx/Account.h>
2#include <test/jtx/Env.h>
3#include <test/jtx/PathSet.h>
4#include <test/jtx/TestHelpers.h>
5#include <test/jtx/amount.h>
6#include <test/jtx/balance.h>
7#include <test/jtx/mpt.h>
8#include <test/jtx/offer.h>
9#include <test/jtx/owners.h>
10#include <test/jtx/paths.h>
11#include <test/jtx/pay.h>
12#include <test/jtx/sendmax.h>
13#include <test/jtx/ter.h>
14#include <test/jtx/trust.h>
15#include <test/jtx/txflags.h>
16
17#include <xrpl/basics/base_uint.h>
18#include <xrpl/beast/unit_test/suite.h>
19#include <xrpl/core/ServiceRegistry.h>
20#include <xrpl/ledger/ApplyView.h>
21#include <xrpl/ledger/OpenView.h>
22#include <xrpl/ledger/PaymentSandbox.h>
23#include <xrpl/ledger/Sandbox.h>
24#include <xrpl/ledger/helpers/DirectoryHelpers.h>
25#include <xrpl/ledger/helpers/OfferHelpers.h>
26#include <xrpl/protocol/AccountID.h>
27#include <xrpl/protocol/Asset.h>
28#include <xrpl/protocol/Feature.h>
29#include <xrpl/protocol/Indexes.h>
30#include <xrpl/protocol/Keylet.h>
31#include <xrpl/protocol/LedgerFormats.h>
32#include <xrpl/protocol/SField.h>
33#include <xrpl/protocol/STAmount.h>
34#include <xrpl/protocol/STPathSet.h>
35#include <xrpl/protocol/SeqProxy.h>
36#include <xrpl/protocol/TER.h>
37#include <xrpl/protocol/TxFlags.h>
38#include <xrpl/protocol/XRPAmount.h>
39#include <xrpl/tx/paths/Flow.h>
40#include <xrpl/tx/paths/detail/Steps.h>
41
42#include <cstdint>
43#include <optional>
44#include <string>
45#include <type_traits>
46#include <vector>
47
48namespace xrpl::test {
49
51{
53
54 void
56 {
57 testcase("Direct Step");
58
59 using namespace jtx;
60 auto const alice = Account("alice");
61 auto const bob = Account("bob");
62 auto const carol = Account("carol");
63 auto const gw = Account("gw");
64 {
65 // Pay USD, trivial path
66 Env env(*this, features);
67
68 env.fund(XRP(10000), alice, bob, gw);
69 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
70 env(pay(gw, alice, usd(100)));
71 env(pay(alice, bob, usd(10)), Paths(usd));
72 env.require(Balance(bob, usd(10)));
73 }
74 {
75 // Partial payments
76 Env env(*this, features);
77
78 env.fund(XRP(10000), alice, bob, gw);
79 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
80 env(pay(gw, alice, usd(100)));
81 env(pay(alice, bob, usd(110)), Paths(usd), Ter(tecPATH_PARTIAL));
82 env.require(Balance(bob, usd(0)));
83 env(pay(alice, bob, usd(110)), Paths(usd), Txflags(tfPartialPayment));
84 env.require(Balance(bob, usd(100)));
85 }
86
87 {
88 // Limit quality
89 auto test = [&](auto&& issue1, auto&& issue2) {
90 Env env(*this, features);
91
92 env.fund(XRP(10'000), gw, alice, bob, carol);
93 env.close();
94
95 auto const usd =
96 issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice, carol}});
97 auto const eur =
98 issue2({.env = env, .token = "EUR", .issuer = gw, .holders = {bob}});
99
100 env(pay(gw, alice, usd(100)));
101 env(pay(gw, bob, eur(100)));
102
103 env(offer(alice, eur(4), usd(4)));
104 env.close();
105
106 env(pay(bob, carol, usd(5)),
107 Sendmax(eur(4)),
108 Txflags(tfLimitQuality | tfPartialPayment),
110 env.require(Balance(carol, usd(0)));
111
112 env(pay(bob, carol, usd(5)), Sendmax(eur(4)), Txflags(tfPartialPayment));
113 env.require(Balance(carol, usd(4)));
114 };
116 }
117 }
118
119 void
121 {
122 testcase("Book Step");
123
124 using namespace jtx;
125
126 auto const gw = Account("gateway");
127 Account const alice("alice");
128 Account const bob("bob");
129 Account const carol("carol");
130
131 {
132 // simple [MPT|IOU]/[IOU|MPT] offer
133 auto test = [&](auto&& issue1, auto&& issue2) {
134 Env env(*this, features);
135
136 env.fund(XRP(10'000), alice, bob, carol, gw);
137 env.close();
138
139 auto const usd = issue1(
140 {.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob, carol}});
141 auto const btc = issue2(
142 {.env = env, .token = "BTC", .issuer = gw, .holders = {alice, bob, carol}});
143
144 env(pay(gw, alice, btc(50)));
145 env(pay(gw, bob, usd(50)));
146
147 env(offer(bob, btc(50), usd(50)));
148
149 env(pay(alice, carol, usd(50)), Path(~usd), Sendmax(btc(50)));
150
151 env.require(Balance(alice, btc(0)));
152 env.require(Balance(bob, btc(50)));
153 env.require(Balance(bob, usd(0)));
154 env.require(Balance(carol, usd(50)));
155 BEAST_EXPECT(!isOffer(env, bob, btc(50), usd(50)));
156 };
158 }
159 {
160 // simple [MPT|IOU]/XRP XRP/[IOU|MPT] offer
161 auto test = [&](auto&& issue1, auto&& issue2) {
162 Env env(*this, features);
163
164 env.fund(XRP(10'000), alice, bob, carol, gw);
165 env.close();
166
167 auto const usd = issue1(
168 {.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob, carol}});
169 auto const btc = issue2(
170 {.env = env, .token = "BTC", .issuer = gw, .holders = {alice, bob, carol}});
171
172 env(pay(gw, alice, btc(50)));
173 env(pay(gw, bob, usd(50)));
174
175 env(offer(bob, btc(50), XRP(50)));
176 env(offer(bob, XRP(50), usd(50)));
177
178 env(pay(alice, carol, usd(50)), Path(~XRP, ~usd), Sendmax(btc(50)));
179
180 env.require(Balance(alice, btc(0)));
181 env.require(Balance(bob, btc(50)));
182 env.require(Balance(bob, usd(0)));
183 env.require(Balance(carol, usd(50)));
184 BEAST_EXPECT(!isOffer(env, bob, XRP(50), usd(50)));
185 BEAST_EXPECT(!isOffer(env, bob, btc(50), XRP(50)));
186 };
188 }
189 {
190 // simple XRP -> USD through offer and sendmax
191 Env env(*this, features);
192
193 env.fund(XRP(10'000), alice, bob, carol, gw);
194 env.close();
195
196 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, carol}});
197 MPT const btc = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, carol}});
198
199 env(pay(gw, bob, usd(50)));
200
201 env(offer(bob, XRP(50), usd(50)));
202
203 env(pay(alice, carol, usd(50)), Path(~usd), Sendmax(XRP(50)));
204
205 // fee: MPTokenAuthorize * 2(EUR, USD) + pay
206 env.require(Balance(alice, XRP(10'000 - 50) - txFee(env, 3)));
207 // fee: MPTokenAuthorize * 2(EUR, USD) + offer
208 env.require(Balance(bob, XRP(10'000 + 50) - txFee(env, 3)));
209 env.require(Balance(bob, usd(0)));
210 env.require(Balance(carol, usd(50)));
211 BEAST_EXPECT(!isOffer(env, bob, XRP(50), usd(50)));
212 }
213 {
214 // simple USD -> XRP through offer and sendmax
215 Env env(*this, features);
216
217 env.fund(XRP(10'000), alice, bob, carol, gw);
218 env.close();
219
220 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, carol}});
221 MPT const btc = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, carol}});
222
223 env(pay(gw, alice, usd(50)));
224
225 env(offer(bob, usd(50), XRP(50)));
226
227 env(pay(alice, carol, XRP(50)), Path(~XRP), Sendmax(usd(50)));
228
229 env.require(Balance(alice, usd(0)));
230 env.require(Balance(bob, XRP(10'000 - 50) - txFee(env, 3)));
231 env.require(Balance(bob, usd(50)));
232 env.require(Balance(carol, XRP(10'000 + 50) - txFee(env, 2)));
233 BEAST_EXPECT(!isOffer(env, bob, usd(50), XRP(50)));
234 }
235 {
236 // test unfunded offers are removed when payment succeeds
237 auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
238 Env env(*this, features);
239
240 env.fund(XRP(10'000), alice, bob, carol, gw);
241 env.close();
242
243 auto const usd = issue1(
244 {.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob, carol}});
245 auto const btc = issue2(
246 {.env = env, .token = "BTC", .issuer = gw, .holders = {alice, bob, carol}});
247 auto const eur = issue3(
248 {.env = env, .token = "EUR", .issuer = gw, .holders = {alice, bob, carol}});
249
250 env(pay(gw, alice, btc(60)));
251 env(pay(gw, bob, usd(50)));
252 env(pay(gw, bob, eur(50)));
253
254 env(offer(bob, btc(50), usd(50)));
255 env(offer(bob, btc(40), eur(50)));
256 env(offer(bob, eur(50), usd(50)));
257
258 // unfund offer
259 env(pay(bob, gw, eur(50)));
260 env.require(Balance(bob, eur(0)));
261 BEAST_EXPECT(isOffer(env, bob, btc(50), usd(50)));
262 BEAST_EXPECT(isOffer(env, bob, btc(40), eur(50)));
263 BEAST_EXPECT(isOffer(env, bob, eur(50), usd(50)));
264
265 env(pay(alice, carol, usd(50)), Path(~usd), Path(~eur, ~usd), Sendmax(btc(60)));
266
267 env.require(Balance(alice, btc(10)));
268 env.require(Balance(bob, btc(50)));
269 env.require(Balance(bob, usd(0)));
270 env.require(Balance(bob, eur(0)));
271 env.require(Balance(carol, usd(50)));
272 // used in the payment
273 BEAST_EXPECT(!isOffer(env, bob, btc(50), usd(50)));
274 // found unfunded
275 BEAST_EXPECT(!isOffer(env, bob, btc(40), eur(50)));
276 // unfunded, but should not yet be found unfunded
277 BEAST_EXPECT(isOffer(env, bob, eur(50), usd(50)));
278 };
280 }
281 {
282 // test unfunded offers are returned when the payment fails.
283 // bob makes two offers: a funded 5000 USD for 50 BTC and an
284 // unfunded 5000 EUR for 60 BTC. alice pays carol 6100 USD with 61
285 // BTC. alice only has 60 BTC, so the payment will fail. The payment
286 // uses two paths: one through bob's funded offer and one through
287 // his unfunded offer. When the payment fails `flow` should return
288 // the unfunded offer. This test is intentionally similar to the one
289 // that removes unfunded offers when the payment succeeds.
290 auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
291 Env env(*this, features);
292
293 env.fund(XRP(10'000), alice, bob, carol, gw);
294 env.close();
295
296 auto const usd = issue1(
297 {.env = env,
298 .token = "USD",
299 .issuer = gw,
300 .holders = {alice, bob, carol},
301 .limit = 100'000});
302 auto const btc = issue2(
303 {.env = env,
304 .token = "BTC",
305 .issuer = gw,
306 .holders = {alice, bob, carol},
307 .limit = 100'000});
308 auto const eur = issue3(
309 {.env = env,
310 .token = "EUR",
311 .issuer = gw,
312 .holders = {alice, bob, carol},
313 .limit = 100'000});
314
315 env(pay(gw, alice, btc(60)));
316 env(pay(gw, bob, usd(6'000)));
317 env(pay(gw, bob, eur(5'000)));
318 env(pay(gw, carol, eur(100)));
319
320 env(offer(bob, btc(50), usd(5'000)));
321 env(offer(bob, btc(60), eur(5'000)));
322 env(offer(carol, btc(1'000), eur(100)));
323 env(offer(bob, eur(5'000), usd(5'000)));
324
325 // unfund offer
326 env(pay(bob, gw, eur(5'000)));
327 BEAST_EXPECT(isOffer(env, bob, btc(50), usd(5'000)));
328 BEAST_EXPECT(isOffer(env, bob, btc(60), eur(5'000)));
329 BEAST_EXPECT(isOffer(env, carol, btc(1'000), eur(100)));
330
331 auto flowJournal = env.app().getLogs().journal("Flow");
332 auto const flowResult = [&] {
333 STAmount const deliver(usd(5'100));
334 STAmount smax(btc(61));
335 PaymentSandbox sb(env.current().get(), TapNone);
336 STPathSet paths;
337 auto ipe = [](Asset const& asset) {
338 return STPathElement(
340 xrpAccount(),
341 asset,
342 asset.getIssuer());
343 };
344 {
345 // BTC -> USD
346 STPath const p1({ipe(usd)});
347 paths.pushBack(p1);
348 // BTC -> EUR -> USD
349 STPath const p2({ipe(eur), ipe(usd)});
350 paths.pushBack(p2);
351 }
352
353 return flow(
354 sb,
355 deliver,
356 alice,
357 carol,
358 paths,
359 false,
360 false,
361 true,
363 std::nullopt,
364 smax,
365 std::nullopt,
366 flowJournal);
367 }();
368
369 BEAST_EXPECT(flowResult.removableOffers.size() == 1);
370 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
371 if (flowResult.removableOffers.empty())
372 return false;
373 Sandbox sb(&view, TapNone);
374 for (auto const& o : flowResult.removableOffers)
375 {
376 if (auto ok = sb.peek(keylet::offer(o)))
377 offerDelete(sb, ok, flowJournal);
378 }
379 sb.apply(view);
380 return true;
381 });
382
383 // used in payment, but since payment failed should
384 // be untouched
385 BEAST_EXPECT(isOffer(env, bob, btc(50), usd(5'000)));
386 BEAST_EXPECT(isOffer(env, carol, btc(1'000), eur(100)));
387 // found unfunded
388 BEAST_EXPECT(!isOffer(env, bob, btc(60), eur(5'000)));
389 };
391 }
392 {
393 // Do not produce more in the forward pass than the
394 // reverse pass. This test uses a path whose reverse
395 // pass will compute a 0.5 USD input required for a 1
396 // EUR output. It sets a sendmax of 0.4 USD, so the
397 // payment engine will need to do a forward pass.
398 // Without limits, the 0.4 USD would produce 1000 EUR in
399 // the forward pass. This test checks that the payment
400 // produces 1 EUR, as expected.
401 auto test = [&](auto&& issue1, auto&& issue2) {
402 Env env(*this, features);
403 env.fund(XRP(10'000), alice, bob, carol, gw);
404
405 auto const usd = issue1(
406 {.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob, carol}});
407 auto const eur = issue1(
408 {.env = env, .token = "EUR", .issuer = gw, .holders = {alice, bob, carol}});
409
410 env(pay(gw, alice, usd(1'000)));
411 env(pay(gw, bob, eur(1'000)));
412
413 Keylet const bobUsdOffer = keylet::offer(bob, SeqProxy::rawSequence(env.seq(bob)));
414 env(offer(bob, usd(10), drops(2)), Txflags(tfPassive));
415 env(offer(bob, drops(1), eur(1'000)), Txflags(tfPassive));
416
417 bool const reducedOffersV2 = features[fixReducedOffersV2];
418
419 // With reducedOffersV2, it is not allowed to accept
420 // less than USD(0.5) of bob's USD offer. If we
421 // provide 1 drop for less than USD(0.5), then the
422 // remaining fractional offer would block the order
423 // book.
424 TER const expectedTER = reducedOffersV2 ? TER(tecPATH_DRY) : TER(tesSUCCESS);
425 env(pay(alice, carol, eur(1)),
426 Path(~XRP, ~eur),
427 Sendmax(usd(4)),
428 Txflags(tfNoRippleDirect | tfPartialPayment),
429 Ter(expectedTER));
430
431 if (!reducedOffersV2)
432 {
433 env.require(Balance(carol, eur(1)));
434 env.require(Balance(bob, usd(4)));
435 env.require(Balance(bob, eur(999)));
436
437 // Show that bob's USD offer is now a blocker.
438 SLE::const_pointer const usdOffer = env.le(bobUsdOffer);
439 if (BEAST_EXPECT(usdOffer))
440 {
441 std::uint64_t const bookRate = [&usdOffer]() {
442 // Extract the least significant 64
443 // bits from the book page. That's
444 // where the quality is stored.
445 std::string bookDirStr = to_string(usdOffer->at(sfBookDirectory));
446 bookDirStr.erase(0, 48);
447 return std::stoull(bookDirStr, nullptr, 16);
448 }();
449 std::uint64_t const actualRate =
450 getRate(usdOffer->at(sfTakerGets), usdOffer->at(sfTakerPays));
451
452 // We expect the actual rate of the offer to
453 // be worse (larger) than the rate of the
454 // book page holding the offer. This is a
455 // defect which is corrected by
456 // fixReducedOffersV2.
457 BEAST_EXPECT(actualRate > bookRate);
458 }
459 }
460 };
462 }
463 }
464
465 void
467 {
468 testcase("Transfer Rate");
469
470 using namespace jtx;
471
472 auto const gw = Account("gateway");
473 Account const alice("alice");
474 Account const bob("bob");
475 Account const carol("carol");
476
477 {
478 // Simple payment through a gateway with a
479 // transfer rate
480 Env env(*this, features);
481
482 env.fund(XRP(10000), alice, bob, carol, gw);
483
484 MPT const usd = MPTTester(
485 {.env = env,
486 .issuer = gw,
487 .holders = {alice, bob, carol},
488 .transferFee = 25'000,
489 .maxAmt = 1'000});
490
491 env(pay(gw, alice, usd(50)));
492 env.require(Balance(alice, usd(50)));
493 env(pay(alice, bob, usd(40)), Sendmax(usd(50)));
494 env.require(Balance(bob, usd(40)), Balance(alice, usd(0)));
495 }
496 {
497 // transfer rate is not charged when issuer is src or
498 // dst
499 Env env(*this, features);
500
501 env.fund(XRP(10'000), alice, bob, carol, gw);
502
503 MPT const usd = MPTTester(
504 {.env = env,
505 .issuer = gw,
506 .holders = {alice, bob, carol},
507 .transferFee = 25'000,
508 .maxAmt = 1'000});
509
510 env(pay(gw, alice, usd(50)));
511 env.require(Balance(alice, usd(50)));
512 env(pay(alice, gw, usd(40)), Sendmax(usd(40)));
513 env.require(Balance(alice, usd(10)));
514 }
515 {
516 // transfer fee on an offer
517 Env env(*this, features);
518
519 env.fund(XRP(10'000), alice, bob, carol, gw);
520
521 MPT const usd = MPTTester(
522 {.env = env,
523 .issuer = gw,
524 .holders = {alice, bob, carol},
525 .transferFee = 25'000,
526 .maxAmt = 10'000});
527
528 // scale by 1
529 env(pay(gw, bob, usd(650)));
530
531 env(offer(bob, XRP(50), usd(500)));
532
533 env(pay(alice, carol, usd(500)),
534 Path(~usd),
535 Sendmax(XRP(50)),
536 Txflags(tfPartialPayment));
537
538 // bob pays 25% on 500USD -> 100USD; 400USD goes to carol
539 env.require(
540 Balance(alice, XRP(10'000 - 50) - txFee(env, 2)),
541 Balance(bob, usd(150)),
542 Balance(carol, usd(400)));
543 }
544 {
545 // Transfer fee two consecutive offers
546 auto test = [&](auto&& issue1, auto&& issue2) {
547 Env env(*this, features);
548
549 env.fund(XRP(10'000), alice, bob, carol, gw);
550 env.close();
551
552 auto const usd = issue1(
553 {.env = env,
554 .token = "USD",
555 .issuer = gw,
556 .holders = {alice, bob, carol},
557 .limit = 1'000,
558 .transferFee = 25'000});
559 auto const eur = issue2(
560 {.env = env,
561 .token = "EUR",
562 .issuer = gw,
563 .holders = {alice, bob, carol},
564 .limit = 1'000,
565 .transferFee = 25'000});
566
567 env(pay(gw, bob, usd(50)));
568 env(pay(gw, bob, eur(50)));
569
570 env(offer(bob, XRP(50), usd(50)));
571 env(offer(bob, usd(50), eur(50)));
572
573 env(pay(alice, carol, eur(40)),
574 Path(~usd, ~eur),
575 Sendmax(XRP(40)),
576 Txflags(tfPartialPayment));
577 // +1 for fset in helperIssueIOU
578 using tEUR = std::decay_t<decltype(eur)>;
579 auto const fee = txFee(env, 3);
580 // bob pays 25% on 40USD (40 since sendmax is 40XRP)
581 // 8USD goes to gw and 32USD goes back to bob ->
582 // bob's USD balance is 42USD. USD/EUR offer is 32USD/32EUR.
583 // bob pays 25% on 32EUR -> 7EUR if MPT, 6.4EUR if IOU,
584 // therefore carl gets 25EUR if MPT, 25.6EUR if IOU.
585 auto const carolEUR = [&]() {
586 if constexpr (std::is_same_v<tEUR, IOU>)
587 {
588 return eur(25.6);
589 }
590 else
591 {
592 return eur(25);
593 }
594 }();
595 env.require(
596 Balance(alice, XRP(10'000 - 40) - fee),
597 Balance(bob, usd(42)),
598 Balance(bob, eur(18)),
599 Balance(carol, carolEUR));
600 };
602 }
603 {
604 // Offer where the owner is also the issuer, sender pays
605 // fee
606 Env env(*this, features);
607
608 env.fund(XRP(10'000), alice, bob, gw);
609
610 MPT const usd = MPTTester(
611 {.env = env,
612 .issuer = gw,
613 .holders = {alice, bob},
614 .transferFee = 25'000,
615 .maxAmt = 1'000});
616
617 env(offer(gw, XRP(100), usd(100)));
618 env(pay(alice, bob, usd(100)), Sendmax(XRP(100)), Txflags(tfPartialPayment));
619 env.require(Balance(alice, XRP(10'000 - 100) - txFee(env, 2)), Balance(bob, usd(80)));
620 }
621 {
622 // Offer where the owner is also the issuer, sender pays
623 // fee
624 Env env(*this, features);
625
626 env.fund(XRP(10'000), alice, bob, gw);
627
628 MPT const usd = MPTTester(
629 {.env = env,
630 .issuer = gw,
631 .holders = {alice, bob},
632 .transferFee = 25'000,
633 .maxAmt = 1'000});
634
635 env(offer(gw, XRP(125), usd(125)));
636 env(pay(alice, bob, usd(100)), Sendmax(XRP(200)));
637 env.require(Balance(alice, XRP(10'000 - 125) - txFee(env, 2)), Balance(bob, usd(100)));
638 }
639 }
640
641 void
643 {
644 testcase("falseDryChanges");
645
646 using namespace jtx;
647
648 auto const gw = Account("gateway");
649 Account const alice("alice");
650 Account const bob("bob");
651 Account const carol("carol");
652
653 auto test = [&](auto&& issue1, auto&& issue2) {
654 Env env(*this, features);
655
656 env.fund(XRP(10'000), alice, carol, gw);
657 env.fund(reserve(env, 5), bob);
658 env.close();
659
660 auto const usd =
661 issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice, carol, bob}});
662 auto const eur =
663 issue2({.env = env, .token = "EUR", .issuer = gw, .holders = {alice, carol, bob}});
664
665 env(pay(gw, alice, eur(50)));
666 env(pay(gw, bob, usd(50)));
667
668 // Bob has _just_ slightly less than 50 xrp available
669 // If his owner count changes, he will have more liquidity.
670 // This is one error case to test (when Flow is used).
671 // Computing the incoming xrp to the XRP/USD offer will
672 // require two recursive calls to the EUR/XRP offer. The
673 // second call will return tecPATH_DRY, but the entire path
674 // should not be marked as dry. This is the second error
675 // case to test (when flowV1 is used).
676 env(offer(bob, eur(50), XRP(50)));
677 env(offer(bob, XRP(50), usd(50)));
678
679 env(pay(alice, carol, usd(1'000'000)),
680 Path(~XRP, ~usd),
681 Sendmax(eur(500)),
682 Txflags(tfNoRippleDirect | tfPartialPayment));
683
684 auto const carolUSD = env.balance(carol, usd).value();
685 BEAST_EXPECT(carolUSD > usd(0) && carolUSD < usd(50));
686 };
688 }
689
690 void
692 {
693 // Single path with two offers and limit quality. The
694 // quality limit is such that the first offer should be
695 // taken but the second should not. The total amount
696 // delivered should be the sum of the two offers and sendMax
697 // should be more than the first offer.
698 testcase("limitQuality");
699 using namespace jtx;
700
701 auto const gw = Account("gateway");
702 Account const alice("alice");
703 Account const bob("bob");
704 Account const carol("carol");
705
706 {
707 Env env(*this);
708
709 env.fund(XRP(10'000), alice, bob, carol, gw);
710
711 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, carol}});
712
713 env(pay(gw, bob, usd(100)));
714 env(offer(bob, XRP(50), usd(50)));
715 env(offer(bob, XRP(100), usd(50)));
716
717 env(pay(alice, carol, usd(100)),
718 Path(~usd),
719 Sendmax(XRP(100)),
720 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
721
722 env.require(Balance(carol, usd(50)));
723 }
724 }
725
726 // Helper function that returns the reserve on an account based on
727 // the passed in number of owners.
728 static XRPAmount
730 {
731 return env.current()->fees().accountReserve(count, 1);
732 }
733
734 // Helper function that returns the Offers on an account.
737 {
739 forEachItem(*env.current(), account, [&result](SLE::const_ref sle) {
740 if (sle->getType() == ltOFFER)
741 result.push_back(sle);
742 });
743 return result;
744 }
745
746 void
748 {
749 using namespace jtx;
750 Account const alice("alice");
751 Account const bob("bob");
752 Account const carol("carol");
753 Account const gw("gw");
754
755 {
756 testcase("Reserve-edge offer owner cannot create another object");
757
758 Env env(*this, features);
759
760 auto const baseFee = env.current()->fees().base;
761 auto const ownerIncrement = reserve(env, 1) - reserve(env, 0);
762 auto const xrpOffer = ownerIncrement - drops(1);
763 auto const bobStart = reserve(env, 2) - drops(1) + baseFee;
764
765 env.fund(XRP(10'000), alice, gw);
766 env.fund(bobStart, bob);
767 env.close();
768
769 MPTTester const usd({.env = env, .issuer = gw, .maxAmt = 10});
770
771 env(offer(bob, usd(1), xrpOffer));
772 env.close();
773
774 env.require(Balance(bob, reserve(env, 2) - drops(1)), Owners(bob, 1));
775
776 // This mirrors the full-crossing setup below. Bob has enough XRP
777 // for the resting offer, but not enough to pay a fee and add
778 // another owner-count object while the offer remains on ledger.
779 env(check::create(bob, alice, drops(1)), Ter(tecINSUFFICIENT_RESERVE));
780 env.close();
781
782 env.require(Owners(bob, 1));
783 BEAST_EXPECT(offersOnAccount(env, bob).size() == 1);
784 }
785
786 {
787 testcase("Reserve-edge offer owner creates MPToken during consume");
788
789 Env env(*this, features);
790
791 auto const baseFee = env.current()->fees().base;
792 auto const ownerIncrement = reserve(env, 1) - reserve(env, 0);
793 auto const xrpOffer = ownerIncrement - drops(1);
794 auto const bobStart = reserve(env, 2) - drops(1) + baseFee;
795
796 env.fund(XRP(10'000), alice, carol, gw);
797 env.fund(bobStart, bob);
798 env.close();
799
800 MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10});
801
802 env(pay(gw, alice, usd(1)));
803 env(offer(bob, usd(1), xrpOffer));
804 env.close();
805
806 env.require(Balance(bob, reserve(env, 2) - drops(1)), Owners(bob, 1));
807 BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
808 auto const carolXRP = env.balance(carol);
809
810 // Bob has enough XRP for the resting offer but is close to
811 // reserve. The payment should not create Bob's USD MPToken until
812 // the offer is actually consumed, otherwise the temporary owner
813 // count increase can make the offer look underfunded during path
814 // execution.
815 env(pay(alice, carol, xrpOffer),
816 Path(~XRP),
817 Sendmax(usd(1)),
818 Txflags(tfNoRippleDirect));
819 env.close();
820
821 env.require(Balance(carol, carolXRP + xrpOffer));
822 env.require(Balance(bob, usd(1)));
823 env.require(Balance(bob, reserve(env, 1)), Owners(bob, 1));
824 BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
825 BEAST_EXPECT(offersOnAccount(env, bob).empty());
826 }
827
828 {
829 testcase("Partial offer owner creates MPToken during consume");
830
831 Env env(*this, features);
832
833 auto const baseFee = env.current()->fees().base;
834 auto const ownerIncrement = reserve(env, 1) - reserve(env, 0);
835 auto const bobStart = reserve(env, 3) + baseFee;
836
837 env.fund(XRP(10'000), alice, carol, gw);
838 env.fund(bobStart, bob);
839 env.close();
840
841 MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10});
842
843 env(pay(gw, alice, usd(1)));
844 env(offer(bob, usd(2), drops(2 * ownerIncrement)));
845 env.close();
846
847 env.require(Balance(bob, reserve(env, 3)), Owners(bob, 1));
848 BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
849 auto const carolXRP = env.balance(carol);
850
851 // Partial consumption leaves Bob's offer on the ledger, so he ends
852 // up owning both the remaining offer and a newly created MPToken.
853 // The MPToken is created regardless of reserve; this setup simply
854 // funds Bob enough that he still meets reserve(2) afterward (the
855 // under-reserved case is covered in OfferMPT_test's no-reserve-check
856 // testcase).
857 env(pay(alice, carol, drops(ownerIncrement)),
858 Path(~XRP),
859 Sendmax(usd(1)),
860 Txflags(tfNoRippleDirect));
861 env.close();
862
863 env.require(Balance(carol, carolXRP + drops(ownerIncrement)));
864 env.require(Balance(bob, usd(1)));
865 env.require(Balance(bob, reserve(env, 2)), Owners(bob, 2));
866 BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
867 BEAST_EXPECT(offersOnAccount(env, bob).size() == 1);
868 BEAST_EXPECT(isOffer(env, bob, usd(1), drops(ownerIncrement)));
869 }
870
871 {
872 testcase("Issuer-owned offer does not create issuer MPToken");
873
874 Env env(*this, features);
875
876 env.fund(XRP(10'000), alice, carol, gw);
877 env.close();
878
879 MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10});
880
881 env(pay(gw, alice, usd(1)));
882 env(offer(gw, usd(1), drops(1'000)));
883 env.close();
884
885 BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), gw.id())));
886 auto const carolXRP = env.balance(carol);
887
888 // The issuer can own an offer that receives its own MPT without an
889 // MPToken. Consuming that offer should keep the issuer side
890 // tokenless.
891 env(pay(alice, carol, drops(1'000)),
892 Path(~XRP),
893 Sendmax(usd(1)),
894 Txflags(tfNoRippleDirect));
895 env.close();
896
897 env.require(Balance(alice, usd(0)));
898 env.require(Balance(carol, carolXRP + drops(1'000)));
899 BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), gw.id())));
900 BEAST_EXPECT(offersOnAccount(env, gw).empty());
901 }
902 }
903
904 void
906 {
907 testcase("Self-payment 1");
908
909 // In this test case the new flow code mis-computes the
910 // amount of money to move. Fortunately the new code's
911 // re-execute check catches the problem and throws out the
912 // transaction.
913 //
914 // The old payment code handles the payment correctly.
915 using namespace jtx;
916
917 auto test = [&](auto&& issue1, auto&& issue2) {
918 auto const gw1 = Account("gw1");
919 auto const gw2 = Account("gw2");
920 auto const alice = Account("alice");
921
922 Env env(*this, features);
923
924 env.fund(XRP(1'000'000), gw1, gw2);
925 env.close();
926
927 // The fee that's charged for transactions.
928 auto const f = env.current()->fees().base;
929
930 env.fund(reserve(env, 3) + f * 4, alice);
931 env.close();
932
933 auto const usd = issue1(
934 {.env = env, .token = "USD", .issuer = gw1, .holders = {alice}, .limit = 20'000});
935 auto const eur = issue2(
936 {.env = env, .token = "EUR", .issuer = gw2, .holders = {alice}, .limit = 20'000});
937
938 env(pay(gw1, alice, usd(10)));
939 env(pay(gw2, alice, eur(10'000)));
940 env.close();
941
942 env(offer(alice, usd(5'000), eur(6'000)));
943 env.close();
944
945 env.require(Owners(alice, 3));
946 env.require(Balance(alice, usd(10)));
947 env.require(Balance(alice, eur(10'000)));
948
949 auto aliceOffers = offersOnAccount(env, alice);
950 BEAST_EXPECT(aliceOffers.size() == 1);
951 for (auto const& offerPtr : aliceOffers)
952 {
953 auto const offer = *offerPtr;
954 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
955 BEAST_EXPECT(offer[sfTakerGets] == eur(6'000));
956 BEAST_EXPECT(offer[sfTakerPays] == usd(5'000));
957 }
958
959 env(pay(alice, alice, eur(6'000)), Sendmax(usd(5'000)), Txflags(tfPartialPayment));
960 env.close();
961
962 env.require(Owners(alice, 3));
963 env.require(Balance(alice, usd(10)));
964 env.require(Balance(alice, eur(10'000)));
965 aliceOffers = offersOnAccount(env, alice);
966 BEAST_EXPECT(aliceOffers.size() == 1);
967 for (auto const& offerPtr : aliceOffers)
968 {
969 auto const offer = *offerPtr;
970 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
971 if constexpr (std::is_same_v<std::decay_t<decltype(eur)>, IOU>)
972 {
973 BEAST_EXPECT(offer[sfTakerGets] == eur(5'988));
974 }
975 else
976 {
977 BEAST_EXPECT(offer[sfTakerGets] == eur(5'989));
978 }
979 BEAST_EXPECT(offer[sfTakerPays] == usd(4'990));
980 }
981 };
983 }
984
985 template <typename TGets, typename TPays>
993
994 void
996 {
997 testcase("Self-payment 2");
998
999 using namespace jtx;
1000
1001 // This test shows a difference between IOU and MPT
1002 // self-payment result depending on IOU trustline limit.
1003
1004 auto const gw1 = Account("gw1");
1005 auto const gw2 = Account("gw2");
1006 auto const alice = Account("alice");
1007
1008 auto initMPT = [&](Env& env) {
1009 MPT const usd =
1010 MPTTester({.env = env, .issuer = gw1, .holders = {alice}, .maxAmt = 506});
1011 MPT const eur =
1012 MPTTester({.env = env, .issuer = gw2, .holders = {alice}, .maxAmt = 606});
1013 // Payment's engine last step overflows
1014 // OutstandingAmount since it doesn't know if the
1015 // BookStep redeems or not. The BookStep then has 600EUR
1016 // available. Consequently, the entire offer is crossed.
1017 // Note remaining takerGets is 541 rather than 540 due to integral
1018 // rounding. XRP has a similar result.
1019 return TokenData<MPT, MPT>{
1020 .gets = eur, .pays = usd, .remTakerGets = eur(541), .remTakerPays = usd(450)};
1021 };
1022
1023 auto initXRP = [&](Env& env) {
1024 MPT const usd =
1025 MPTTester({.env = env, .issuer = gw1, .holders = {alice}, .maxAmt = 1'000});
1026 // Payment's engine last step overflows
1027 // OutstandingAmount since it doesn't know if the
1028 // BookStep redeems or not. The BookStep then has 600EUR
1029 // available. Consequently, the entire offer is crossed.
1030 // Note remaining takerGets is 540.000001 rather than 540 due to
1031 // integral rounding.
1032 return TokenData<XrpT, MPT>{
1033 .gets = XRP,
1034 .pays = usd,
1035 .remTakerGets = XRP(540.000001),
1036 .remTakerPays = usd(450)};
1037 };
1038
1039 auto initIOU = [&](Env& env) {
1040 auto const usd = gw1["USD"];
1041 auto const eur = gw2["EUR"];
1042 env(trust(alice, usd(506)));
1043 env(trust(alice, eur(606)));
1044 env.close();
1045 // Payment's engine last step is limited by alice's
1046 // trustline - 606. Therefore, only 6EUR is delivered
1047 // and the offer is partially crossed.
1048 return TokenData<IOU, IOU>{
1049 .gets = eur, .pays = usd, .remTakerGets = eur(594), .remTakerPays = usd(495)};
1050 };
1051
1052 auto initIOU1 = [&](Env& env) {
1053 auto const usd = gw1["USD"];
1054 auto const eur = gw2["EUR"];
1055 env(trust(alice, usd(1'000)));
1056 env(trust(alice, eur(1'000)));
1057 env.close();
1058 // Payment's engine last step is not limited by alice's
1059 // trustline. Therefore, the entire offer is crossed.
1060 // This the same result as with MPT.
1061 return TokenData<IOU, IOU>{
1062 .gets = eur, .pays = usd, .remTakerGets = eur(540), .remTakerPays = usd(450)};
1063 };
1064
1065 auto test = [&](auto&& initToken) {
1066 Env env(*this, features);
1067
1068 env.fund(XRP(2'000), gw1, gw2, alice);
1069 env.close();
1070
1071 auto const f = env.current()->fees().base;
1072
1073 auto const tok = initToken(env);
1074
1075 auto const& toK1 = tok.pays;
1076 auto const& toK2 = tok.gets;
1077 bool const isTakerGetsXRP = isXRP(Asset{toK2});
1078 std::uint32_t const ownerCnt = isTakerGetsXRP ? 2 : 3;
1079
1080 env(pay(gw1, alice, toK1(500)));
1081 if (!isTakerGetsXRP)
1082 env(pay(gw2, alice, toK2(600)));
1083 env.close();
1084
1085 env(offer(alice, toK1(500), toK2(600)));
1086 env.close();
1087
1088 env.require(Owners(alice, ownerCnt));
1089 env.require(Balance(alice, toK1(500)));
1090 if (isTakerGetsXRP)
1091 {
1092 env.require(Balance(alice, toK2(2'000) - 2 * f));
1093 }
1094 else
1095 {
1096 env.require(Balance(alice, toK2(600)));
1097 }
1098
1099 auto aliceOffers = offersOnAccount(env, alice);
1100 BEAST_EXPECT(aliceOffers.size() == 1);
1101 for (auto const& offerPtr : aliceOffers)
1102 {
1103 auto const offer = *offerPtr;
1104 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
1105 BEAST_EXPECT(offer[sfTakerGets] == toK2(600));
1106 BEAST_EXPECT(offer[sfTakerPays] == toK1(500));
1107 }
1108
1109 env(pay(alice, alice, toK2(60)), Sendmax(toK1(50)), Txflags(tfPartialPayment));
1110 env.close();
1111
1112 env.require(Owners(alice, ownerCnt));
1113 env.require(Balance(alice, toK1(500)));
1114 if (isTakerGetsXRP)
1115 {
1116 env.require(Balance(alice, toK2(2'000) - 3 * f));
1117 }
1118 else
1119 {
1120 env.require(Balance(alice, toK2(600)));
1121 }
1122 aliceOffers = offersOnAccount(env, alice);
1123 BEAST_EXPECT(aliceOffers.size() == 1);
1124 for (auto const& offerPtr : aliceOffers)
1125 {
1126 auto const offer = *offerPtr;
1127 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
1128 BEAST_EXPECT(offer[sfTakerGets] == tok.remTakerGets);
1129 BEAST_EXPECT(offer[sfTakerPays] == tok.remTakerPays);
1130 }
1131 };
1132
1133 test(initXRP);
1134 test(initMPT);
1135 test(initIOU);
1136 test(initIOU1);
1137 }
1138
1139 void
1140 testSelfFundedXRPEndpoint(bool consumeOffer, FeatureBitset features)
1141 {
1142 // Test that the deferred credit table is not bypassed for
1143 // XRPEndpointSteps. If the account in the first step is
1144 // sending XRP and that account also owns an offer that
1145 // receives XRP, it should not be possible for that step to
1146 // use the XRP received in the offer as part of the payment.
1147 testcase("Self funded XRPEndpoint");
1148
1149 using namespace jtx;
1150
1151 Env env(*this, features);
1152
1153 auto const alice = Account("alice");
1154 auto const gw = Account("gw");
1155
1156 env.fund(XRP(10'000), alice, gw);
1157
1158 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 20});
1159
1160 env(pay(gw, alice, usd(10)));
1161 env(offer(alice, XRP(50'000), usd(10)));
1162
1163 // Consuming the offer changes the owner count, which could
1164 // also cause liquidity to decrease in the forward pass
1165 auto const toSend = consumeOffer ? usd(10) : usd(9);
1166 env(pay(alice, alice, toSend),
1167 Path(~usd),
1168 Sendmax(XRP(20'000)),
1169 Txflags(tfPartialPayment | tfNoRippleDirect));
1170 }
1171
1172 void
1174 {
1175 testcase("Unfunded Offer");
1176
1177 using namespace jtx;
1178 {
1179 // Test reverse
1180 Env env(*this, features);
1181
1182 auto const alice = Account("alice");
1183 auto const bob = Account("bob");
1184 auto const gw = Account("gw");
1185
1186 env.fund(XRP(100'000), alice, bob, gw);
1187
1188 MPT const usd =
1189 MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 20E+17});
1190
1191 // scale by 17
1192 STAmount const tinyAmt1{usd, 9'000'000'000'000'000ll, 0, false, STAmount::Unchecked{}};
1193 STAmount const tinyAmt3{usd, 9'000'000'000'000'003ll, 0, false, STAmount::Unchecked{}};
1194
1195 env(offer(gw, drops(9'000'000'000), tinyAmt3));
1196
1197 env(pay(alice, bob, tinyAmt1),
1198 Path(~usd),
1199 Sendmax(drops(9'000'000'000)),
1200 Txflags(tfNoRippleDirect));
1201
1202 BEAST_EXPECT(!isOffer(env, gw, XRP(0), usd(0)));
1203 }
1204 {
1205 // Test forward
1206 Env env(*this, features);
1207
1208 auto const alice = Account("alice");
1209 auto const bob = Account("bob");
1210 auto const gw = Account("gw");
1211
1212 env.fund(XRP(100'000), alice, bob, gw);
1213
1214 MPT const usd =
1215 MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 20E+17});
1216
1217 // scale by 17
1218 STAmount const tinyAmt1{usd, 9'000'000'000'000'000ll, 0, false, STAmount::Unchecked{}};
1219 STAmount const tinyAmt3{usd, 9'000'000'000'000'003ll, 0, false, STAmount::Unchecked{}};
1220
1221 env(pay(gw, alice, tinyAmt1));
1222
1223 env(offer(gw, tinyAmt3, drops(9'000'000'000)));
1224 env(pay(alice, bob, drops(9'000'000'000)),
1225 Path(~XRP),
1226 Sendmax(usd(static_cast<std::uint64_t>(1E+17))),
1227 Txflags(tfNoRippleDirect));
1228
1229 BEAST_EXPECT(!isOffer(env, gw, usd(0), XRP(0)));
1230 }
1231 }
1232
1233 void
1235 {
1236 testcase("ReexecuteDirectStep");
1237
1238 using namespace jtx;
1239 Env env(*this, features);
1240
1241 auto const alice = Account("alice");
1242 auto const bob = Account("bob");
1243 auto const gw = Account("gw");
1244
1245 env.fund(XRP(10'000), alice, bob, gw);
1246
1247 // scale by 16
1248 MPT const usd =
1249 MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 100E+16});
1250
1251 env(
1252 pay(gw,
1253 alice,
1254 // 12.55....
1255 STAmount{usd, std::uint64_t(1255555555555555ull), 2, false}));
1256
1257 env(offer(
1258 gw,
1259 // 5.0...
1260 STAmount{usd, std::uint64_t(5000000000000000ull), 1, false},
1261 XRP(1000)));
1262
1263 env(offer(
1264 gw,
1265 // .555...
1266 STAmount{usd, std::uint64_t(5555555555555555ull), 0, false},
1267 XRP(10)));
1268
1269 env(offer(
1270 gw,
1271 // 4.44....
1272 STAmount{usd, std::uint64_t(4444444444444444ull), 1, false},
1273 XRP(.1)));
1274
1275 env(offer(
1276 alice,
1277 // 17
1278 STAmount{usd, std::uint64_t(1700000000000000ull), 0, false},
1279 XRP(.001)));
1280
1281 env(pay(alice, bob, XRP(10'000)),
1282 Path(~XRP),
1283 Sendmax(usd(static_cast<std::uint64_t>(100E+16))),
1284 Txflags(tfPartialPayment | tfNoRippleDirect));
1285 }
1286
1287 void
1289 {
1290 // The new payment code used to assert if an offer was made
1291 // for more XRP than the offering account held. This unit
1292 // test reproduces that failing case.
1293 testcase("Self crossing low quality offer");
1294
1295 using namespace jtx;
1296
1297 Env env(*this, features);
1298
1299 auto const ann = Account("ann");
1300 auto const gw = Account("gateway");
1301
1302 auto const fee = env.current()->fees().base;
1303 env.fund(reserve(env, 2) + drops(9999640) + fee, ann);
1304 env.fund(reserve(env, 2) + fee * 4, gw);
1305
1306 // scale by 5
1307 MPT const ctb = MPTTester(
1308 {.env = env,
1309 .issuer = gw,
1310 .holders = {ann},
1311 .transferFee = 2'000, // 2%
1312 .maxAmt = 1'000'000});
1313
1314 env(pay(gw, ann, ctb(285'600)));
1315 env.close();
1316
1317 env(offer(ann, drops(365'611'702'030), ctb(571'300)));
1318 env.close();
1319
1320 // This payment caused assert.
1321 env(pay(ann, ann, ctb(68'700)), Sendmax(drops(20'000'000'000)), Txflags(tfPartialPayment));
1322 }
1323
1324 void
1326 {
1327 testcase("Empty Strand");
1328 using namespace jtx;
1329
1330 auto const alice = Account("alice");
1331
1332 Env env(*this, features);
1333
1334 env.fund(XRP(10000), alice);
1335
1336 MPT const usd;
1337
1338 env(pay(alice, alice, usd(100)), Path(~usd), Ter(temBAD_PATH));
1339 }
1340
1341 void
1343 {
1344 testcase("Circular XRP");
1345
1346 using namespace jtx;
1347 auto const alice = Account("alice");
1348 auto const bob = Account("bob");
1349 auto const gw = Account("gw");
1350
1351 {
1352 // Payment path starting with XRP
1353 auto test = [&](auto&& issue1, auto&& issue2) {
1354 Env env(*this);
1355 env.fund(XRP(10'000), alice, bob, gw);
1356
1357 auto const usd =
1358 issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob}});
1359 auto const eur =
1360 issue2({.env = env, .token = "EUR", .issuer = gw, .holders = {alice, bob}});
1361 env(pay(gw, alice, usd(100)));
1362 env(pay(gw, alice, eur(100)));
1363 env.close();
1364
1365 env(offer(alice, XRP(100), usd(100)), Txflags(tfPassive));
1366 env(offer(alice, usd(100), XRP(100)), Txflags(tfPassive));
1367 env(offer(alice, XRP(100), eur(100)), Txflags(tfPassive));
1368 env.close();
1369
1370 TER const expectedTer = TER{temBAD_PATH_LOOP};
1371 env(pay(alice, bob, eur(1)),
1372 Path(~usd, ~XRP, ~eur),
1373 Sendmax(XRP(1)),
1374 Txflags(tfNoRippleDirect),
1375 Ter(expectedTer));
1376 };
1378 }
1379 {
1380 // Payment path ending with XRP
1381 auto test = [&](auto&& issue1, auto&& issue2) {
1382 Env env(*this);
1383 env.fund(XRP(10'000), alice, bob, gw);
1384 auto const usd =
1385 issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob}});
1386 auto const eur =
1387 issue2({.env = env, .token = "EUR", .issuer = gw, .holders = {alice, bob}});
1388 env(pay(gw, alice, usd(100)));
1389 env(pay(gw, alice, eur(100)));
1390 env.close();
1391
1392 env(offer(alice, XRP(100), usd(100)), Txflags(tfPassive));
1393 env(offer(alice, eur(100), XRP(100)), Txflags(tfPassive));
1394 env.close();
1395 // EUR -> //XRP -> //USD ->XRP
1396 env(pay(alice, bob, XRP(1)),
1397 Path(~XRP, ~usd, ~XRP),
1398 Sendmax(eur(1)),
1399 Txflags(tfNoRippleDirect),
1401 };
1403 }
1404 {
1405 // Payment where loop is formed in the middle of the
1406 // path, not on an endpoint
1407 auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
1408 Env env(*this);
1409 env.fund(XRP(10'000), alice, bob, gw);
1410 env.close();
1411 auto const usd =
1412 issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob}});
1413 auto const eur =
1414 issue2({.env = env, .token = "EUR", .issuer = gw, .holders = {alice, bob}});
1415 auto const jpy =
1416 issue3({.env = env, .token = "JPY", .issuer = gw, .holders = {alice, bob}});
1417 env(pay(gw, alice, usd(100)));
1418 env(pay(gw, alice, eur(100)));
1419 env(pay(gw, alice, jpy(100)));
1420 env.close();
1421
1422 env(offer(alice, usd(100), XRP(100)), Txflags(tfPassive));
1423 env(offer(alice, XRP(100), eur(100)), Txflags(tfPassive));
1424 env(offer(alice, eur(100), XRP(100)), Txflags(tfPassive));
1425 env(offer(alice, XRP(100), jpy(100)), Txflags(tfPassive));
1426 env.close();
1427
1428 env(pay(alice, bob, jpy(1)),
1429 Path(~XRP, ~eur, ~XRP, ~jpy),
1430 Sendmax(usd(1)),
1431 Txflags(tfNoRippleDirect),
1433 };
1435 }
1436 }
1437
1438 void
1440 {
1441 testcase("Max Flow/Self Payment Edge Cases");
1442 using namespace jtx;
1443 Account const gw("gw");
1444 Account const alice("alice");
1445 Account const carol("carol");
1446 Account const bob("bob");
1447
1448 // Direct payment between holders.
1449 {
1450 Env env(*this);
1451
1452 env.fund(XRP(1'000), gw, alice, carol);
1453
1454 MPT const usd =
1455 MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}, .maxAmt = 100});
1456
1457 env(pay(gw, alice, usd(100)));
1458
1459 env(pay(alice, carol, usd(100)));
1460
1461 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1462 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1463 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
1464 }
1465
1466 // Direct payment between holders. Partial payment limited
1467 // by holder funds.
1468 {
1469 Env env(*this);
1470
1471 env.fund(XRP(1'000), gw, alice, carol);
1472
1473 MPT const usd =
1474 MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}, .maxAmt = 100});
1475
1476 env(pay(gw, alice, usd(80)));
1477
1478 env(pay(alice, carol, usd(100)), Txflags(tfPartialPayment));
1479
1480 BEAST_EXPECT(env.balance(gw, usd) == usd(-80));
1481 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
1482 BEAST_EXPECT(env.balance(carol, usd) == usd(80));
1483 }
1484
1485 // Direct payment between holders. Partial payment limited
1486 // by holder funds. OutstandingAmount is already at max
1487 // before the payment.
1488 {
1489 Env env(*this);
1490
1491 env.fund(XRP(1'000), gw, alice, carol, bob);
1492
1493 MPT const usd = MPTTester(
1494 {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .maxAmt = 100});
1495
1496 env(pay(gw, bob, usd(20)));
1497 env(pay(gw, alice, usd(80)));
1498
1499 env(pay(alice, carol, usd(100)), Txflags(tfPartialPayment));
1500
1501 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1502 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
1503 BEAST_EXPECT(env.balance(carol, usd) == usd(80));
1504 }
1505
1506 // Cross-currency payment holder to holder. Holder owns an
1507 // offer. OutstandingAmount is already at max before the
1508 // payment.
1509 {
1510 Env env(*this);
1511
1512 env.fund(XRP(1'000), gw, alice, carol, bob);
1513
1514 MPT const usd =
1515 MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}, .maxAmt = 100});
1516
1517 env(pay(gw, alice, usd(100)));
1518
1519 env(offer(alice, XRP(100), usd(100)));
1520
1521 env(pay(bob, carol, usd(100)), Sendmax(XRP(100)), Path(~usd));
1522
1523 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1524 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
1525 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1526 }
1527
1528 // Cross-currency payment holder to holder. Issuer owns an
1529 // offer. OutstandingAmount is already at max before the
1530 // payment. Since an issuer owns the offer, it issues more
1531 // tokens to another holder, and the payment fails.
1532 {
1533 Env env(*this);
1534
1535 env.fund(XRP(1'000), gw, alice, carol);
1536
1537 MPT const usd =
1538 MPTTester({.env = env, .issuer = gw, .holders = {carol}, .maxAmt = 100});
1539
1540 env(pay(gw, carol, usd(100)));
1541
1542 env(offer(gw, XRP(100), usd(100)));
1543
1544 env(pay(alice, carol, usd(100)),
1545 Sendmax(XRP(100)),
1546 Path(~usd),
1547 Txflags(tfPartialPayment),
1548 Ter(tecPATH_DRY));
1549
1550 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1551 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1552 }
1553
1554 // Cross-currency payment holder to holder. Issuer owns an
1555 // offer. OutstandingAmount is at 80USD before the payment.
1556 // Consequently, the issuer can issue 20USD more.
1557 {
1558 Env env(*this);
1559
1560 env.fund(XRP(1'000), gw, alice, carol);
1561
1562 MPT const usd =
1563 MPTTester({.env = env, .issuer = gw, .holders = {carol}, .maxAmt = 100});
1564
1565 env(pay(gw, carol, usd(80)));
1566
1567 env(offer(gw, XRP(100), usd(100)));
1568
1569 env(pay(alice, carol, usd(100)),
1570 Sendmax(XRP(100)),
1571 Path(~usd),
1572 Txflags(tfPartialPayment));
1573
1574 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1575 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1576 }
1577
1578 // Cross-currency payment holder to holder. Holder owns an
1579 // offer. The offer buys more MPT's. The payment fails since
1580 // OutstandingAmount is already at max.
1581 {
1582 Env env(*this);
1583
1584 env.fund(XRP(1'000), gw, alice);
1585
1586 MPT const usd =
1587 MPTTester({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 100});
1588
1589 env(pay(gw, alice, usd(100)));
1590
1591 env(offer(alice, usd(100), XRP(100)));
1592
1593 env(pay(gw, alice, XRP(100)), Sendmax(usd(100)), Path(~XRP), Ter(tecPATH_PARTIAL));
1594
1595 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1596 BEAST_EXPECT(env.balance(alice, usd) == usd(100));
1597 }
1598
1599 // Cross-currency payment issuer to holder. Holder owns an
1600 // offer. The offer buys EUR, OutstandingAmount goes to max,
1601 // no overflow. The offer redeems USD to the issuer. While
1602 // OutstandingAmount is already at max, the payment succeeds
1603 // since USD is redeemed.
1604 {
1605 auto test = [&](auto&& issue1, auto&& issue2) {
1606 Env env(*this);
1607
1608 env.fund(XRP(1'000), gw, alice, carol);
1609 env.close();
1610
1611 auto const usd = issue1(
1612 {.env = env,
1613 .token = "USD",
1614 .issuer = gw,
1615 .holders = {alice, carol},
1616 .limit = 100});
1617 using tUSD = std::decay_t<decltype(usd)>;
1618 auto const eur = issue2(
1619 {.env = env,
1620 .token = "EUR",
1621 .issuer = gw,
1622 .holders = {alice, carol},
1623 .limit = 100});
1624
1625 env(pay(gw, alice, usd(100)));
1626
1627 env(offer(alice, eur(100), usd(100)));
1628
1629 env(pay(gw, carol, usd(100)), Sendmax(eur(100)), Path(~usd));
1630
1631 if constexpr (std::is_same_v<tUSD, MPT>)
1632 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1633 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
1634 BEAST_EXPECT(env.balance(alice, eur) == eur(100));
1635 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1636 };
1638 }
1639
1640 // Cross-currency payment holder to holder. Offer is owned
1641 // by destination account. OutstandingAmount is not at max.
1642 {
1643 Env env(*this);
1644
1645 env.fund(XRP(1'000), gw, alice, carol);
1646
1647 MPT const usd =
1648 MPTTester({.env = env, .issuer = gw, .holders = {carol}, .maxAmt = 120});
1649
1650 env(pay(gw, carol, usd(100)));
1651
1652 env(offer(carol, XRP(100), usd(100)));
1653
1654 env(pay(alice, carol, usd(100)),
1655 Path(~usd),
1656 Sendmax(XRP(100)),
1657 Txflags(tfPartialPayment));
1658
1659 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1660 }
1661
1662 // Cross-currency payment holder to holder. Offer is owned
1663 // by destination account. OutstandingAmount is already at
1664 // max.
1665 {
1666 Env env(*this);
1667
1668 env.fund(XRP(1'000), gw, alice, carol);
1669
1670 MPT const usd =
1671 MPTTester({.env = env, .issuer = gw, .holders = {carol}, .maxAmt = 100});
1672
1673 env(pay(gw, carol, usd(100)));
1674
1675 env(offer(carol, XRP(100), usd(100)));
1676
1677 env(pay(alice, carol, usd(100)),
1678 Path(~usd),
1679 Sendmax(XRP(100)),
1680 Txflags(tfPartialPayment));
1681
1682 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1683 }
1684
1685 // Cross-currency payment holder to holder. Multiple offers
1686 // with different owners - some holders, some issuer.
1687 {
1688 auto test = [&](auto&& issue1, auto&& issue2) {
1689 Env env(*this);
1690
1691 env.fund(XRP(1'000), gw, alice, carol, bob);
1692 env.close();
1693
1694 auto const usd = issue1(
1695 {.env = env,
1696 .token = "USD",
1697 .issuer = gw,
1698 .holders = {alice, carol, bob},
1699 .limit = 1'000});
1700 using tUSD = std::decay_t<decltype(usd)>;
1701 auto const eur = issue2(
1702 {.env = env,
1703 .token = "EUR",
1704 .issuer = gw,
1705 .holders = {alice, carol, bob},
1706 .limit = 1'000});
1707 using tEUR = std::decay_t<decltype(eur)>;
1708
1709 env(pay(gw, alice, usd(600)));
1710 env(pay(gw, carol, eur(700)));
1711
1712 env(offer(alice, eur(100), usd(105)));
1713 env(offer(gw, eur(100), usd(104)));
1714 env(offer(gw, eur(100), usd(103)));
1715 env(offer(gw, eur(100), usd(102)));
1716 env(offer(gw, eur(100), usd(101)));
1717 env(offer(gw, eur(100), usd(100)));
1718
1719 env(pay(carol, bob, usd(2'000)),
1720 Sendmax(eur(2'000)),
1721 Path(~usd),
1722 Txflags(tfPartialPayment));
1723
1724 if constexpr (std::is_same_v<tUSD, MPT>)
1725 {
1726 BEAST_EXPECT(env.balance(gw, usd) == usd(-1'000));
1727 BEAST_EXPECT(env.balance(alice, usd) == usd(495));
1728 BEAST_EXPECT(env.balance(bob, usd) == usd(505));
1729 }
1730 else
1731 {
1732 BEAST_EXPECT(env.balance(gw, usd) == usd(0));
1733 BEAST_EXPECT(env.balance(alice, usd) == usd(495));
1734 // all offers are consumed since the limit is different
1735 // for the holders
1736 BEAST_EXPECT(env.balance(bob, usd) == usd(615));
1737 }
1738 if constexpr (std::is_same_v<tEUR, MPT>)
1739 {
1740 if constexpr (std::is_same_v<tUSD, MPT>)
1741 {
1742 BEAST_EXPECT(env.balance(carol, eur) == eur(210));
1743 }
1744 else
1745 {
1746 // carol sells 600USD since all offers are consumed
1747 BEAST_EXPECT(env.balance(carol, eur) == eur(100));
1748 }
1749 }
1750 else
1751 {
1752 BEAST_EXPECT(
1753 env.balance(carol, eur) == STAmount(eur, UINT64_C(209'9009900990099), -13));
1754 }
1755 // 100/101 is partially crossed (90/91) and 100/100 is
1756 // unfunded when MPT. All offers are consumed if IOU.
1757 env.require(offers(gw, 0));
1758 // alice's offer is consumed.
1759 env.require(offers(alice, 0));
1760 };
1762 }
1763
1764 // Cross-currency payment holder to holder. Multiple offers
1765 // with different owners - some holders, some issuer. Source
1766 // and destination account is the same.
1767 {
1768 Env env(*this);
1769
1770 env.fund(XRP(1'000), gw, alice, carol);
1771
1772 MPT const usd =
1773 MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}, .maxAmt = 2'000});
1774
1775 env(pay(gw, carol, usd(1'000)));
1776 env(pay(gw, alice, usd(600)));
1777
1778 env(offer(gw, XRP(5), usd(11)));
1779 env(offer(gw, XRP(6), usd(13)));
1780 env(offer(carol, XRP(7), usd(15)));
1781 env(offer(carol, XRP(17), usd(35)));
1782 env(offer(carol, XRP(23), usd(47)));
1783 env(offer(alice, XRP(10), usd(19)));
1784 env(offer(alice, XRP(15), usd(28)));
1785 env(offer(alice, XRP(25), usd(46)));
1786
1787 env(pay(carol, carol, usd(200)), Sendmax(XRP(100)), Txflags(tfPartialPayment));
1788
1789 BEAST_EXPECT(env.balance(gw, usd) == usd(-1'624));
1790 BEAST_EXPECT(env.balance(carol, usd) == usd(1'102));
1791 env.require(offers(carol, 0));
1792 env.require(offers(gw, 0));
1793 // 100 XRP's = 5+6+7+17+23+10+15+17(25-8)
1794 BEAST_EXPECT(isOffer(env, alice, XRP(8), usd(15)));
1795 }
1796
1797 // Cross-currency payment holder to holder. Multiple offers
1798 // with different owners - some holders, some issuer.
1799 {
1800 Env env(*this);
1801 env.fund(XRP(1'000), gw, alice, carol, bob);
1802
1803 MPT const usd =
1804 MPTTester({.env = env, .issuer = gw, .holders = {alice, carol, bob}, .maxAmt = 30});
1805
1806 env(pay(gw, alice, usd(12))); // 12, 15, 20
1807 env(pay(gw, bob, usd(5))); // 5, 5, 10
1808
1809 env(offer(alice, XRP(10), usd(12)));
1810 env(offer(gw, XRP(10), usd(11)));
1811 env(offer(bob, XRP(10), usd(10)));
1812
1813 env(pay(carol, bob, usd(30)), Sendmax(XRP(30)), Txflags(tfPartialPayment), Path(~usd));
1814 BEAST_EXPECT(env.balance(gw, usd) == usd(-28));
1815 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
1816 // 12+11+5
1817 BEAST_EXPECT(env.balance(bob, usd) == usd(28));
1818 }
1819
1820 // Cross-currency payment two steps. Second book step
1821 // issues, first book step redeems.
1822 {
1823 Account const dan{"dan"};
1824 Account const john{"john"};
1825 Account const ed{"ed"};
1826 Account const sam{"sam"};
1827 Account const bill{"bill"};
1828
1829 struct TestData
1830 {
1831 int maxAmt;
1832 int sendMax;
1833 int dstTrustLimit;
1834 int dstExpectEUR;
1835 int outstandingUSD;
1836 int expEdBuyUSD;
1837 int expDanBuyUSD;
1838 int expBobSellUSD;
1839 int expGwXRP; // whole XRP excluding the fees
1840 std::uint8_t expOffersGw;
1841 bool lastGwBuyUSD;
1842 [[nodiscard]] std::uint8_t
1843 expOffersBob() const
1844 {
1845 return expBobSellUSD == 0 ? 1 : 0;
1846 }
1847 [[nodiscard]] std::uint8_t
1848 expOffersEd() const
1849 {
1850 // partially crossed if < 100
1851 return expEdBuyUSD < 100 ? 1 : 0;
1852 }
1853 [[nodiscard]] std::uint8_t
1854 expOffersDan() const
1855 {
1856 return expDanBuyUSD == 0 ? 1 : 0;
1857 }
1858 };
1859
1860 auto test = [&](TestData const& d) {
1861 Env env(*this);
1862 env.fund(XRP(1'000), gw, alice, carol, bob, dan, john, ed, sam, bill);
1863 env.close();
1864
1865 MPT const usd = MPTTester(
1866 {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .maxAmt = d.maxAmt});
1867 auto const eur = gw["EUR"];
1868
1869 env(pay(gw, alice, usd(100)));
1870 env(pay(gw, carol, usd(100)));
1871 env(pay(gw, bob, usd(100)));
1872
1873 BEAST_EXPECT(env.balance(gw, usd) == usd(-300));
1874
1875 env(trust(john, eur(100)));
1876 env(trust(dan, eur(100)));
1877 env(trust(ed, eur(100)));
1878 env(trust(bill, eur(d.dstTrustLimit)));
1879
1880 env(pay(gw, john, eur(100)));
1881 env(pay(gw, dan, eur(100)));
1882 env(pay(gw, ed, eur(100)));
1883 env.close();
1884
1885 // Sell USD
1886 env(offer(alice, XRP(100), usd(100)));
1887 env.close(); // close after each create to ensure
1888 // the order
1889 env(offer(carol, XRP(100), usd(100)));
1890 env.close();
1891 if (!d.lastGwBuyUSD)
1892 {
1893 env(offer(gw, XRP(100), usd(100)));
1894 env.close();
1895 }
1896 env(offer(bob, XRP(100), usd(100)));
1897 env.close();
1898 if (d.lastGwBuyUSD)
1899 {
1900 env(offer(gw, XRP(100), usd(100)));
1901 env.close();
1902 }
1903 BEAST_EXPECT(expectOffers(env, alice, 1));
1904 BEAST_EXPECT(expectOffers(env, carol, 1));
1905 BEAST_EXPECT(expectOffers(env, gw, 1));
1906 BEAST_EXPECT(expectOffers(env, bob, 1));
1907
1908 // Buy USD
1909 env(offer(john, usd(100), eur(100)));
1910 env.close();
1911 env(offer(gw, usd(100), eur(100)));
1912 env.close();
1913 env(offer(dan, usd(100), eur(100)));
1914 env.close();
1915 env(offer(ed, usd(100), eur(100)));
1916 env.close();
1917 BEAST_EXPECT(expectOffers(env, john, 1));
1918 BEAST_EXPECT(expectOffers(env, gw, 2));
1919 BEAST_EXPECT(expectOffers(env, dan, 1));
1920 BEAST_EXPECT(expectOffers(env, ed, 1));
1921
1922 env(pay(sam, bill, eur(400)),
1923 Sendmax(XRP(d.sendMax)),
1924 Path(~usd, ~eur),
1925 Txflags(tfPartialPayment | tfNoRippleDirect));
1926 env.close();
1927
1928 auto const baseFee = env.current()->fees().base.drops();
1929 BEAST_EXPECT(env.balance(bill, eur) == eur(d.dstExpectEUR));
1930 BEAST_EXPECT(env.balance(john, usd) == usd(100));
1931 BEAST_EXPECT(env.balance(dan, usd) == usd(d.expDanBuyUSD));
1932 BEAST_EXPECT(env.balance(ed, usd) == usd(d.expEdBuyUSD));
1933 BEAST_EXPECT(env.balance(gw, usd) == usd(-d.outstandingUSD));
1934 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
1935 BEAST_EXPECT(env.balance(carol, usd) == usd(0));
1936 BEAST_EXPECT(env.balance(bob, usd) == usd(100 - d.expBobSellUSD));
1937 BEAST_EXPECT(env.balance(gw) == XRPAmount{d.expGwXRP * kDropsPerXrp - baseFee * 9});
1938 BEAST_EXPECT(expectOffers(env, john, 0));
1939 BEAST_EXPECT(expectOffers(env, gw, d.expOffersGw));
1940 BEAST_EXPECT(expectOffers(env, dan, d.expOffersDan()));
1941 BEAST_EXPECT(expectOffers(env, ed, d.expOffersEd()));
1942 BEAST_EXPECT(expectOffers(env, alice, 0));
1943 BEAST_EXPECT(expectOffers(env, carol, 0));
1944 BEAST_EXPECT(expectOffers(env, bob, d.expOffersBob()));
1945 };
1946
1947 // clang-format off
1949 // Sell USD: alice, carol, bob, gw are consumed.
1950 // Buy USD: john, gw, dan, ed are consumed.
1951 // gw's sell USD is consumed because there is sufficient available balance (100USD).
1952 // but OutstandingAmount is 300USD because gw's sell offer is balanced out by
1953 // gw's buy offer.
1954 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
1955 { .maxAmt=400, .sendMax=400, .dstTrustLimit=400, .dstExpectEUR=400, .outstandingUSD=300, .expEdBuyUSD=100, .expDanBuyUSD=100, .expBobSellUSD=100, .expGwXRP=1100, .expOffersGw=0, .lastGwBuyUSD=false},
1956 // Sell USD: alice, carol, bob, gw are consumed.
1957 // Buy USD: john, gw, dan, ed (partially) are consumed.
1958 // gw's sell USD is partially consumed because there is available balance (50USD).
1959 // OutstandingAmount is 250USD because gw's sell offer is partially balanced by
1960 // gw's buy offer. ed's offer is on the books because it's partially crossed.
1961 // gw's offer is removed from the order book because it's partially consumed and
1962 // the remaining offer is unfunded.
1963 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
1964 { .maxAmt=350, .sendMax=400, .dstTrustLimit=400, .dstExpectEUR=350, .outstandingUSD=250, .expEdBuyUSD=50, .expDanBuyUSD=100, .expBobSellUSD=100, .expGwXRP=1050, .expOffersGw=0, .lastGwBuyUSD=false},
1965 // Sell USD: alice, carol, bob are consumed; gw's is unfunded
1966 // since OutstandingAmount is initially at MaximumAmount.
1967 // Buy USD: john, gw, dan are consumed; ed's remains on the order
1968 // book since 300USD is the sell limit.
1969 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
1970 { .maxAmt=300, .sendMax=400, .dstTrustLimit=400, .dstExpectEUR=300, .outstandingUSD=200, .expEdBuyUSD=0, .expDanBuyUSD=100, .expBobSellUSD=100, .expGwXRP=1000, .expOffersGw=0, .lastGwBuyUSD=false},
1971 // Same as above. bill's trustline limit sets the output to 300USD.
1972 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
1973 { .maxAmt=300, .sendMax=400, .dstTrustLimit=300, .dstExpectEUR=300, .outstandingUSD=200, .expEdBuyUSD=0, .expDanBuyUSD=100, .expBobSellUSD=100, .expGwXRP=1000, .expOffersGw=0, .lastGwBuyUSD=false},
1974 // Sell USD: alice, carol, bob are consumed; gw's removed from
1975 // the order book since it's unfunded.
1976 // Buy USD: john, gw, dan are consumed; ed's remains on the order
1977 // book since 300USD is the limit.
1978 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
1979 { .maxAmt=300, .sendMax=400, .dstTrustLimit=300, .dstExpectEUR=300, .outstandingUSD=200, .expEdBuyUSD=0, .expDanBuyUSD=100, .expBobSellUSD=100, .expGwXRP=1000, .expOffersGw=0, .lastGwBuyUSD=true},
1980 // Sell USD: alice, carol are consumed; gw's removed from
1981 // the order book in rev pass since it's unfunded; bob's
1982 // remains on the order book.
1983 // Buy USD: john, gw; ed's, dan's remains on the order
1984 // book since 300USD is the limit.
1985 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
1986 { .maxAmt=300, .sendMax=200, .dstTrustLimit=300, .dstExpectEUR=200, .outstandingUSD=200, .expEdBuyUSD=0, .expDanBuyUSD=0, .expBobSellUSD=0, .expGwXRP=1000, .expOffersGw=0, .lastGwBuyUSD=false},
1987 // Same as three tests above since limited by buy 300USD (gw offer is unfunded)
1988 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
1989 { .maxAmt=300, .sendMax=380, .dstTrustLimit=400, .dstExpectEUR=300, .outstandingUSD=200, .expEdBuyUSD=0, .expDanBuyUSD=100, .expBobSellUSD=100, .expGwXRP=1000, .expOffersGw=0, .lastGwBuyUSD=false},
1990 };
1991 // clang-format on
1992 for (auto const& t : tests)
1993 test(t);
1994 }
1995
1996 // Cross-currency payment. BookStep issues, the first step
1997 // redeems.
1998 {
1999 Account const ed{"ed"};
2000
2001 struct TestData
2002 {
2003 int maxAmt;
2004 int sendMax;
2005 int gwOffer; // quality == 1
2006 int dstExpectXRP;
2007 int outstandingUSD;
2008 int expBobBuyUSD;
2009 int expGwXRP; // whole XRP excluding the fees
2010 std::uint8_t expOffersGw;
2011 bool lastGwBuyUSD;
2012 [[nodiscard]] std::uint8_t
2013 expOffersBob() const
2014 {
2015 // partially crossed if < 100
2016 return expBobBuyUSD < 100 ? 1 : 0;
2017 }
2018 };
2019
2020 auto test = [&](TestData const& d) {
2021 Env env(*this);
2022 env.fund(XRP(1'000), gw, alice, carol, bob, ed);
2023 env.close();
2024
2025 MPT const usd =
2026 MPTTester({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = d.maxAmt});
2027
2028 env(pay(gw, alice, usd(300)));
2029 env.close();
2030
2031 env(offer(carol, usd(100), XRP(100)));
2032 env.close();
2033 if (!d.lastGwBuyUSD)
2034 {
2035 env(offer(gw, usd(d.gwOffer), XRP(d.gwOffer)));
2036 env.close();
2037 }
2038 env(offer(bob, usd(100), XRP(100)));
2039 env.close();
2040 if (d.lastGwBuyUSD)
2041 {
2042 env(offer(gw, usd(d.gwOffer), XRP(d.gwOffer)));
2043 env.close();
2044 }
2045
2046 BEAST_EXPECT(expectOffers(env, carol, 1));
2047 BEAST_EXPECT(expectOffers(env, bob, 1));
2048 BEAST_EXPECT(expectOffers(env, gw, 1));
2049 BEAST_EXPECT(env.balance(gw, usd) == usd(-300));
2050
2051 env(pay(alice, ed, XRP(300)),
2052 Sendmax(usd(d.sendMax)),
2053 Path(~XRP),
2054 Txflags(tfPartialPayment | tfNoRippleDirect));
2055 env.close();
2056
2057 auto const baseFee = env.current()->fees().base.drops();
2058 BEAST_EXPECT(env.balance(alice, usd) == usd(300 - d.sendMax));
2059 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
2060 BEAST_EXPECT(env.balance(bob, usd) == usd(d.expBobBuyUSD));
2061 BEAST_EXPECT(env.balance(ed) == XRP(d.dstExpectXRP));
2062 BEAST_EXPECT(env.balance(gw, usd) == usd(-d.outstandingUSD));
2063 BEAST_EXPECT(env.balance(gw) == XRPAmount{d.expGwXRP * kDropsPerXrp - baseFee * 3});
2064 BEAST_EXPECT(expectOffers(env, carol, 0));
2065 BEAST_EXPECT(expectOffers(env, bob, d.expOffersBob()));
2066 BEAST_EXPECT(expectOffers(env, gw, d.expOffersGw));
2067 };
2068
2069 // clang-format off
2071 // Buy USD: carol, gw, bob are consumed.
2072 // Gw gets 300USD from alice; carol and bob buy 200USD,
2073 // therefore OutstandingAmount is 200.
2074 //*maxAmt sendMax gwOffer dstXRP outstandingUSD bobBuy gwXRP offersGw lastGw
2075 { .maxAmt=300, .sendMax=300, .gwOffer=100, .dstExpectXRP=1300, .outstandingUSD=200, .expBobBuyUSD=100, .expGwXRP=900, .expOffersGw=0, .lastGwBuyUSD=false},
2076 // Same as above. Gw offer location in the order book doesn't matter
2077 //*maxAmt sendMax gwOffer dstXRP outstandingUSD bobBuy gwXRP offersGw lastGw
2078 { .maxAmt=300, .sendMax=300, .gwOffer=100, .dstExpectXRP=1300, .outstandingUSD=200, .expBobBuyUSD=100, .expGwXRP=900, .expOffersGw=0, .lastGwBuyUSD=true},
2079 // Buy USD: carol, gw are consumed. bob's offer remains on the order book.
2080 // Gw gets 300USD from alice; carol buys 100USD,
2081 // therefore OutstandingAmount is 100.
2082 //*maxAmt sendMax gwOffer dstXRP outstandingUSD bobBuy gwXRP offersGw lastGw
2083 { .maxAmt=300, .sendMax=300, .gwOffer=200, .dstExpectXRP=1300, .outstandingUSD=100, .expBobBuyUSD=0, .expGwXRP=800, .expOffersGw=0, .lastGwBuyUSD=false},
2084 // Buy USD: carol, bob are consumed; gw's is partially consumed (100/100) since it's last.
2085 // Gw gets 300USD from alice; carol and bob buy 200USD,
2086 // therefore OutstandingAmount is 200.
2087 //*maxAmt sendMax gwOffer dstXRP outstandingUSD bobBuy gwXRP offersGw lastGw
2088 { .maxAmt=300, .sendMax=300, .gwOffer=200, .dstExpectXRP=1300, .outstandingUSD=200, .expBobBuyUSD=100, .expGwXRP=900, .expOffersGw=1, .lastGwBuyUSD=true},
2089 // Buy USD: carol, bob are consumed; gw's is partially consumed (50/50) since it's last
2090 // and sendMax limits the output.
2091 // Gw gets 250USD from alice; carol and bob buy 200USD, alice has 50USD left,
2092 // therefore OutstandingAmount is 200.
2093 //*maxAmt sendMax gwOffer dstXRP outstandingUSD bobBuy gwXRP offersGw lastGw
2094 { .maxAmt=300, .sendMax=250, .gwOffer=200, .dstExpectXRP=1250, .outstandingUSD=250, .expBobBuyUSD=100, .expGwXRP=950, .expOffersGw=1, .lastGwBuyUSD=true},
2095 };
2096 // clang-format on
2097 for (auto const& t : tests)
2098 test(t);
2099 }
2100
2101 // Cross-currency payment. BookStep redeems, the last step
2102 // issues.
2103 {
2104 Account const ed{"ed"};
2105
2106 struct TestData
2107 {
2108 int maxAmt;
2109 int sendMax;
2110 int initDst;
2111 int gwOffer; // quality == 1
2112 int dstExpectUSD;
2113 int outstandingUSD;
2114 int expAliceXRP; // whole XRP excluding the fees
2115 int expBobSellUSD;
2116 int expGwXRP;
2117 std::uint8_t expOffersGw;
2118 bool lastGwBuyUSD;
2119 [[nodiscard]] std::uint8_t
2120 expOffersBob() const
2121 {
2122 return expBobSellUSD > 0 && expBobSellUSD < 100 ? 1 : 0;
2123 }
2124 };
2125
2126 auto test = [&](TestData const& d) {
2127 Env env(*this);
2128 env.fund(XRP(1'000), gw, alice, carol, bob, ed);
2129 env.close();
2130
2131 MPT const usd = MPTTester(
2132 {.env = env, .issuer = gw, .holders = {carol, bob, ed}, .maxAmt = d.maxAmt});
2133
2134 if (d.initDst != 0)
2135 env(pay(gw, ed, usd(d.initDst)));
2136 env(pay(gw, carol, usd(100)));
2137 env(pay(gw, bob, usd(100)));
2138 env.close();
2139
2140 env(offer(carol, XRP(100), usd(100)));
2141 env.close();
2142 if (!d.lastGwBuyUSD)
2143 {
2144 env(offer(gw, XRP(d.gwOffer), usd(d.gwOffer)));
2145 env.close();
2146 }
2147 env(offer(bob, XRP(100), usd(100)));
2148 env.close();
2149 if (d.lastGwBuyUSD)
2150 {
2151 env(offer(gw, XRP(d.gwOffer), usd(d.gwOffer)));
2152 env.close();
2153 }
2154
2155 BEAST_EXPECT(expectOffers(env, carol, 1));
2156 BEAST_EXPECT(expectOffers(env, bob, 1));
2157 BEAST_EXPECT(expectOffers(env, gw, 1));
2158 BEAST_EXPECT(env.balance(gw, usd) == usd(-200 - d.initDst));
2159
2160 env(pay(alice, ed, usd(300)),
2161 Sendmax(XRP(d.sendMax)),
2162 Path(~usd),
2163 Txflags(tfPartialPayment | tfNoRippleDirect));
2164 env.close();
2165
2166 auto const baseFee = env.current()->fees().base.drops();
2167 BEAST_EXPECT(
2168 env.balance(alice) == XRPAmount{d.expAliceXRP * kDropsPerXrp - baseFee});
2169 BEAST_EXPECT(env.balance(carol, usd) == usd(0));
2170 BEAST_EXPECT(env.balance(bob, usd) == usd(100 - d.expBobSellUSD));
2171 BEAST_EXPECT(env.balance(ed, usd) == usd(d.dstExpectUSD));
2172 BEAST_EXPECT(env.balance(gw, usd) == usd(-d.outstandingUSD));
2173 BEAST_EXPECT(
2174 env.balance(gw) ==
2175 XRPAmount{
2176 d.expGwXRP * kDropsPerXrp - baseFee * (4 + (d.initDst != 0 ? 1 : 0))});
2177 BEAST_EXPECT(expectOffers(env, carol, 0));
2178 BEAST_EXPECT(expectOffers(env, bob, d.expOffersBob()));
2179 BEAST_EXPECT(expectOffers(env, gw, d.expOffersGw));
2180 };
2181
2182 // clang-format off
2184 // Sell USD: carol, gw, bob are consumed.
2185 // ed buys 300USD from carol, gw, bob therefore OutstandingAmount is 300.
2186 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2187 { .maxAmt=300, .sendMax=300, .initDst=0, .gwOffer=100, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=700, .expBobSellUSD=100, .expGwXRP=1100, .expOffersGw=0, .lastGwBuyUSD=false},
2188 // Same as above. Gw offer location in the order book doesn't matter
2189 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2190 { .maxAmt=300, .sendMax=300, .initDst=0, .gwOffer=100, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=700, .expBobSellUSD=100, .expGwXRP=1100, .expOffersGw=0, .lastGwBuyUSD=true},
2191 // Sell USD: carol, bob are consumed, gw is partially consumed.
2192 // ed buys 200 from carol and bob and 50 from gw because gw can only issue 50
2193 // (300(max) - 200(carol+bob) - 50(ed)). ed buys 250 from carol, gw, bob and has 50 initially,
2194 // therefore OutstandingAmount is 300.
2195 // gw's offer is removed from the order book because it's partially consumed and the remaining
2196 // offer is unfunded.
2197 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2198 { .maxAmt=300, .sendMax=300, .initDst=50, .gwOffer=100, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=750, .expBobSellUSD=100, .expGwXRP=1050, .expOffersGw=0, .lastGwBuyUSD=false},
2199 // Same as above. Gw offer location in the order book doesn't matter.
2200 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2201 { .maxAmt=300, .sendMax=300, .initDst=50, .gwOffer=100, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=750, .expBobSellUSD=100, .expGwXRP=1050, .expOffersGw=0, .lastGwBuyUSD=true},
2202 // Same as above. Gw offer size doesn't matter.
2203 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2204 { .maxAmt=300, .sendMax=300, .initDst=50, .gwOffer=200, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=750, .expBobSellUSD=100, .expGwXRP=1050, .expOffersGw=0, .lastGwBuyUSD=true},
2205 // Sell USD: carol, gw are consumed, bob is partially consumed.
2206 // ed buys 200 from carol and gw and 50 form bob because of sendMax limit. bob keeps 50,
2207 // therefore OutstandingAmount is 300.
2208 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2209 { .maxAmt=300, .sendMax=250, .initDst=0, .gwOffer=100, .dstExpectUSD=250, .outstandingUSD=300, .expAliceXRP=750, .expBobSellUSD=50, .expGwXRP=1100, .expOffersGw=0, .lastGwBuyUSD=false},
2210 // Sell USD: carol, bob are consumed, gw is partially consumed because of sendMax limit.
2211 // ed buys 200 from carol and bob and 50 from gw. Therefore, OutstandingAmount is 250.
2212 // gw's offer remains on the order book because it's partially consumed and has more funds.
2213 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2214 { .maxAmt=300, .sendMax=250, .initDst=0, .gwOffer=100, .dstExpectUSD=250, .outstandingUSD=250, .expAliceXRP=750, .expBobSellUSD=100, .expGwXRP=1050, .expOffersGw=1, .lastGwBuyUSD=true},
2215 // Sell USD: carol, bob are consumed, gw is partially consumed because of sendMax limit, also
2216 // there is only 50 available to issue. ed buys 200 from carol and bob and 50 from gw, plus
2217 // he has initially 50, therefore OutstandingAmount is 300.
2218 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2219 { .maxAmt=300, .sendMax=250, .initDst=50, .gwOffer=100, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=750, .expBobSellUSD=100, .expGwXRP=1050, .expOffersGw=0, .lastGwBuyUSD=true},
2220 // Sell USD: carol, bob are consumed, gw is not consumed because there is not available funds
2221 // to issue. ed buys 200 from carol and bob and, plus he has initially 100,
2222 // therefore OutstandingAmount is 300. gw offer is removed because it's unfunded.
2223 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2224 { .maxAmt=300, .sendMax=250, .initDst=100, .gwOffer=100, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=800, .expBobSellUSD=100, .expGwXRP=1000, .expOffersGw=0, .lastGwBuyUSD=true},
2225 };
2226 // clang-format on
2227 for (auto const& t : tests)
2228 test(t);
2229 }
2230
2231 // Cross-currency payment with BookStep as the first step.
2232 // BookStep limits the buy amount.
2233 {
2234 auto test = [&](int sendMax, std::uint16_t dstXRP, std::uint8_t expGwOffers) {
2235 Env env(*this);
2236 env.fund(XRP(1'000), gw, alice, carol);
2237
2238 MPT const usd = MPTTester({.env = env, .issuer = gw, .maxAmt = 300});
2239
2240 env(offer(carol, usd(400), XRP(400)));
2241 env(offer(gw, usd(100), XRP(100)));
2242 BEAST_EXPECT(expectOffers(env, carol, 1));
2243 BEAST_EXPECT(expectOffers(env, gw, 1));
2244
2245 env(pay(gw, alice, XRP(500)),
2246 Sendmax(usd(sendMax)),
2247 Path(~XRP),
2248 Txflags(tfPartialPayment | tfNoRippleDirect));
2249
2250 BEAST_EXPECT(env.balance(alice) == XRP(dstXRP));
2251 BEAST_EXPECT(env.balance(gw, usd) == usd(-300));
2252 BEAST_EXPECT(env.balance(carol, usd) == usd(300));
2253 BEAST_EXPECT(expectOffers(env, carol, 0));
2254 BEAST_EXPECT(expectOffers(env, gw, expGwOffers));
2255 };
2256 // carol's offer is partially consumed - 300USD/300XRP
2257 // because available amount to issue is 300USD. gw's
2258 // offer is fully consumed because it doesn't change
2259 // OutstandingAmount. Both offers are removed from the
2260 // order book - carol's offer is unfunded and gw's offer
2261 // is fully consumed.
2262 test(500, 1'400, 0);
2263 // carol's offer is partially consumed - 300USD/300XRP
2264 // because available amount to issue is 300USD. gw's
2265 // offer is partially consumed because of sendMax limit.
2266 // carol's offer is removed from the order book because
2267 // it's unfunded. gw's offer remains on the order book
2268 // because it's partially consumed and gw has more
2269 // funds.
2270 test(350, 1'350, 1);
2271 }
2272 }
2273
2274 void
2276 {
2277 using namespace jtx;
2278
2280 testFalseDry(features);
2281 testDirectStep(features);
2282 testBookStep(features);
2283 testOfferOwnerMPTCreation(features);
2284 testTransferRate(features);
2285 testSelfPayment1(features);
2286 testSelfPayment2(features);
2287 testSelfFundedXRPEndpoint(false, features);
2288 testSelfFundedXRPEndpoint(true, features);
2289 testUnfundedOffer(features);
2290 testReExecuteDirectStep(features);
2292 }
2293
2294 void
2295 run() override
2296 {
2297 using namespace jtx;
2298 auto const sa = testableAmendments();
2301 testWithFeats(sa);
2302 testEmptyStrand(sa);
2303 }
2304};
2305
2307
2308} // namespace xrpl::test
A generic endpoint for log messages.
Definition Journal.h:44
A testsuite class.
Definition suite.h:52
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
beast::Journal journal(std::string const &name)
Definition Log.cpp:137
bool modify(modify_type const &f)
Modify the open ledger.
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:59
A wrapper which makes credits unavailable to balances.
std::shared_ptr< STLedgerEntry const > const & const_ref
std::shared_ptr< STLedgerEntry const > const_pointer
void pushBack(STPath const &e)
Definition STPathSet.h:549
Discardable, editable view to a ledger.
Definition Sandbox.h:18
void apply(RawView &to)
Definition Sandbox.h:38
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
virtual OpenLedger & getOpenLedger()=0
virtual Logs & getLogs()=0
SLE::pointer peek(Keylet const &k) override
Prepare to modify the SLE associated with key.
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
Application & app()
Definition Env.h:300
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
PrettyAmount limit(Account const &account, Issue const &issue) const
Returns the IOU limit on an account.
Definition Env.cpp:254
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:764
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
Converts to IOU Issue or STAmount.
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:447
Converts to MPT Issue or STAmount.
Match the number of items in the account's owner directory.
Definition owners.h:55
Add a path.
Definition paths.h:47
Set Paths, SendMax on a JTx.
Definition paths.h:23
Sets the SendMax on a JTx.
Definition sendmax.h:16
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
Set the flags on a JTx.
Definition txflags.h:14
T erase(T... args)
T is_same_v
Keylet offer(AccountID const &id, SeqProxy const &seq) noexcept
An offer from an account.
Definition Indexes.cpp:276
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:543
json::Value create(A const &account, A const &dest, STAmount const &sendMax)
Create a check.
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
bool expectOffers(Env &env, AccountID const &account, std::uint16_t size, std::vector< Amounts > const &toMatch)
void testHelper2TokensMix(TTester &&tester)
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
XRPAmount txFee(Env const &env, std::uint16_t n)
FeatureBitset testableAmendments()
Definition Env.h:92
void testHelper3TokensMix(TTester &&tester)
STPathElement ipe(Asset const &asset)
json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:14
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
OwnerCount< ltOFFER > offers
Match the number of offers in the account's owner directory.
Definition owners.h:137
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:54
BEAST_DEFINE_TESTSUITE_PRIO(AccountDelete, app, xrpl, 2)
bool isOffer(jtx::Env &env, jtx::Account const &account, STAmount const &takerPays, STAmount const &takerGets)
An offer exists.
Definition PathSet.h:60
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
bool isXRP(AccountID const &c)
Definition AccountID.h:84
TER offerDelete(ApplyView &view, SLE::ref sle, beast::Journal j)
Delete an offer.
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
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:103
std::uint64_t getRate(STAmount const &offerOut, STAmount const &offerIn)
Definition STAmount.cpp:422
@ TapNone
Definition ApplyView.h:28
@ temBAD_PATH
Definition TER.h:84
@ temBAD_PATH_LOOP
Definition TER.h:85
TERSubset< CanCvtToTER > TER
Definition TER.h:647
void forEachItem(ReadView const &view, Keylet const &root, std::function< void(SLE::const_ref)> const &f)
Iterate all items in the given directory.
AccountID const & xrpAccount()
Compute AccountID from public key.
@ tecPATH_PARTIAL
Definition TER.h:285
@ tecPATH_DRY
Definition TER.h:297
@ tecINSUFFICIENT_RESERVE
Definition TER.h:310
@ tesSUCCESS
Definition TER.h:245
T stoull(T... args)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
void run() override
Runs the suite.
void testSelfFundedXRPEndpoint(bool consumeOffer, FeatureBitset features)
void testFalseDry(FeatureBitset features)
void testSelfPayment1(FeatureBitset features)
void testWithFeats(FeatureBitset features)
void testTransferRate(FeatureBitset features)
void testBookStep(FeatureBitset features)
std::vector< jtx::Account > Accounts
static std::vector< SLE::const_pointer > offersOnAccount(jtx::Env &env, jtx::Account account)
void testOfferOwnerMPTCreation(FeatureBitset features)
void testSelfPayment2(FeatureBitset features)
void testEmptyStrand(FeatureBitset features)
void testDirectStep(FeatureBitset features)
void testMaxAndSelfPaymentEdgeCases(FeatureBitset features)
void testSelfPayLowQualityOffer(FeatureBitset features)
static XRPAmount reserve(jtx::Env &env, std::uint32_t count)
void testReExecuteDirectStep(FeatureBitset features)
void testUnfundedOffer(FeatureBitset features)
Represents an XRP, IOU, or MPT quantity This customizes the string conversion and supports XRP conver...
STAmount const & value() const