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