rippled
Loading...
Searching...
No Matches
PermissionedDEX_test.cpp
1#include <test/jtx.h>
2#include <test/jtx/AMM.h>
3#include <test/jtx/AMMTest.h>
4
5#include <xrpld/app/tx/detail/PermissionedDomainSet.h>
6
7#include <xrpl/basics/Blob.h>
8#include <xrpl/basics/Slice.h>
9#include <xrpl/beast/unit_test/suite.h>
10#include <xrpl/ledger/ApplyViewImpl.h>
11#include <xrpl/protocol/Feature.h>
12#include <xrpl/protocol/IOUAmount.h>
13#include <xrpl/protocol/Indexes.h>
14#include <xrpl/protocol/Issue.h>
15#include <xrpl/protocol/Keylet.h>
16#include <xrpl/protocol/LedgerFormats.h>
17#include <xrpl/protocol/STAmount.h>
18#include <xrpl/protocol/TER.h>
19#include <xrpl/protocol/TxFlags.h>
20#include <xrpl/protocol/jss.h>
21
22#include <atomic>
23#include <cstdint>
24#include <exception>
25#include <map>
26#include <optional>
27#include <string>
28#include <utility>
29#include <vector>
30
31namespace xrpl {
32namespace test {
33
34using namespace jtx;
35
37{
38 [[nodiscard]] bool
39 offerExists(Env const& env, Account const& account, std::uint32_t offerSeq)
40 {
41 return static_cast<bool>(env.le(keylet::offer(account.id(), offerSeq)));
42 }
43
44 [[nodiscard]] bool
46 Env const& env,
47 Account const& account,
48 std::uint32_t offerSeq,
49 STAmount const& takerPays,
50 STAmount const& takerGets,
51 uint32_t const flags = 0,
52 bool const domainOffer = false)
53 {
54 auto offerInDir = [&](uint256 const& directory,
55 uint64_t const pageIndex,
57 std::nullopt) -> bool {
58 auto const page = env.le(keylet::page(directory, pageIndex));
59 if (!page)
60 return false;
61
62 if (domain != (*page)[~sfDomainID])
63 return false;
64
65 auto const& indexes = page->getFieldV256(sfIndexes);
66 for (auto const& index : indexes)
67 {
68 if (index == keylet::offer(account, offerSeq).key)
69 return true;
70 }
71
72 return false;
73 };
74
75 auto const sle = env.le(keylet::offer(account.id(), offerSeq));
76 if (!sle)
77 return false;
78 if (sle->getFieldAmount(sfTakerGets) != takerGets)
79 return false;
80 if (sle->getFieldAmount(sfTakerPays) != takerPays)
81 return false;
82 if (sle->getFlags() != flags)
83 return false;
84 if (domainOffer && !sle->isFieldPresent(sfDomainID))
85 return false;
86 if (!domainOffer && sle->isFieldPresent(sfDomainID))
87 return false;
88 if (!offerInDir(
89 sle->getFieldH256(sfBookDirectory),
90 sle->getFieldU64(sfBookNode),
91 (*sle)[~sfDomainID]))
92 return false;
93
94 if (sle->isFlag(lsfHybrid))
95 {
96 if (!sle->isFieldPresent(sfDomainID))
97 return false;
98 if (!sle->isFieldPresent(sfAdditionalBooks))
99 return false;
100 if (sle->getFieldArray(sfAdditionalBooks).size() != 1)
101 return false;
102
103 auto const& additionalBookDirs =
104 sle->getFieldArray(sfAdditionalBooks);
105
106 for (auto const& bookDir : additionalBookDirs)
107 {
108 auto const& dirIndex = bookDir.getFieldH256(sfBookDirectory);
109 auto const& dirNode = bookDir.getFieldU64(sfBookNode);
110
111 // the directory is for the open order book, so the dir
112 // doesn't have domainID
113 if (!offerInDir(dirIndex, dirNode, std::nullopt))
114 return false;
115 }
116 }
117 else
118 {
119 if (sle->isFieldPresent(sfAdditionalBooks))
120 return false;
121 }
122
123 return true;
124 }
125
126 uint256
128 Book const& book,
129 STAmount const& takerPays,
130 STAmount const& takerGets)
131 {
132 return keylet::quality(
133 keylet::book(book), getRate(takerGets, takerPays))
134 .key;
135 }
136
139 Env const& env,
140 Account const& account,
141 std::uint32_t offerSeq)
142 {
143 if (auto const sle = env.le(keylet::offer(account.id(), offerSeq)))
144 return Keylet(ltDIR_NODE, (*sle)[sfBookDirectory]).key;
145
146 return {};
147 }
148
149 [[nodiscard]] bool
150 checkDirectorySize(Env const& env, uint256 directory, std::uint32_t dirSize)
151 {
152 std::optional<std::uint64_t> pageIndex{0};
153 std::uint32_t dirCnt = 0;
154
155 do
156 {
157 auto const page = env.le(keylet::page(directory, *pageIndex));
158 if (!page)
159 break;
160
161 pageIndex = (*page)[~sfIndexNext];
162 dirCnt += (*page)[sfIndexes].size();
163
164 } while (pageIndex.value_or(0));
165
166 return dirCnt == dirSize;
167 }
168
169 void
171 {
172 testcase("OfferCreate");
173
174 // test preflight
175 {
176 Env env(*this, features - featurePermissionedDEX);
177 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
178 PermissionedDEX(env);
179
180 env(offer(bob, XRP(10), USD(10)),
181 domain(domainID),
183 env.close();
184
185 env.enableFeature(featurePermissionedDEX);
186 env.close();
187 env(offer(bob, XRP(10), USD(10)), domain(domainID));
188 env.close();
189 }
190
191 // preclaim - someone outside of the domain cannot create domain offer
192 {
193 Env env(*this, features);
194 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
195 PermissionedDEX(env);
196
197 // create devin account who is not part of the domain
198 Account devin("devin");
199 env.fund(XRP(1000), devin);
200 env.close();
201 env.trust(USD(1000), devin);
202 env.close();
203 env(pay(gw, devin, USD(100)));
204 env.close();
205
206 env(offer(devin, XRP(10), USD(10)),
207 domain(domainID),
209 env.close();
210
211 // domain owner also issues a credential for devin
212 env(credentials::create(devin, domainOwner, credType));
213 env.close();
214
215 // devin still cannot create offer since he didn't accept credential
216 env(offer(devin, XRP(10), USD(10)),
217 domain(domainID),
219 env.close();
220
221 env(credentials::accept(devin, domainOwner, credType));
222 env.close();
223
224 env(offer(devin, XRP(10), USD(10)), domain(domainID));
225 env.close();
226 }
227
228 // preclaim - someone with expired cred cannot create domain offer
229 {
230 Env env(*this, features);
231 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
232 PermissionedDEX(env);
233
234 // create devin account who is not part of the domain
235 Account devin("devin");
236 env.fund(XRP(1000), devin);
237 env.close();
238 env.trust(USD(1000), devin);
239 env.close();
240 env(pay(gw, devin, USD(100)));
241 env.close();
242
243 auto jv = credentials::create(devin, domainOwner, credType);
244 uint32_t const t = env.current()
245 ->header()
246 .parentCloseTime.time_since_epoch()
247 .count();
248 jv[sfExpiration.jsonName] = t + 20;
249 env(jv);
250
251 env(credentials::accept(devin, domainOwner, credType));
252 env.close();
253
254 // devin can still create offer while his cred is not expired
255 env(offer(devin, XRP(10), USD(10)), domain(domainID));
256 env.close();
257
258 // time advance
260
261 // devin cannot create offer with expired cred
262 env(offer(devin, XRP(10), USD(10)),
263 domain(domainID),
265 env.close();
266 }
267
268 // preclaim - cannot create an offer in a non existent domain
269 {
270 Env env(*this, features);
271 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
272 PermissionedDEX(env);
273 uint256 const badDomain{
274 "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134"
275 "E5"};
276
277 env(offer(bob, XRP(10), USD(10)),
278 domain(badDomain),
280 env.close();
281 }
282
283 // apply - offer can be created even if takergets issuer is not in
284 // domain
285 {
286 Env env(*this, features);
287 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
288 PermissionedDEX(env);
289
291 domainOwner, gw, domainOwner, credType));
292 env.close();
293
294 auto const bobOfferSeq{env.seq(bob)};
295 env(offer(bob, XRP(10), USD(10)), domain(domainID));
296 env.close();
297
298 BEAST_EXPECT(
299 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
300 }
301
302 // apply - offer can be created even if takerpays issuer is not in
303 // domain
304 {
305 Env env(*this, features);
306 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
307 PermissionedDEX(env);
308
310 domainOwner, gw, domainOwner, credType));
311 env.close();
312
313 auto const bobOfferSeq{env.seq(bob)};
314 env(offer(bob, USD(10), XRP(10)), domain(domainID));
315 env.close();
316
317 BEAST_EXPECT(
318 checkOffer(env, bob, bobOfferSeq, USD(10), XRP(10), 0, true));
319 }
320
321 // apply - two domain offers cross with each other
322 {
323 Env env(*this, features);
324 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
325 PermissionedDEX(env);
326
327 auto const bobOfferSeq{env.seq(bob)};
328 env(offer(bob, XRP(10), USD(10)), domain(domainID));
329 env.close();
330
331 BEAST_EXPECT(
332 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
333 BEAST_EXPECT(ownerCount(env, bob) == 3);
334
335 // a non domain offer cannot cross with domain offer
336 env(offer(carol, USD(10), XRP(10)));
337 env.close();
338
339 BEAST_EXPECT(
340 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
341
342 auto const aliceOfferSeq{env.seq(alice)};
343 env(offer(alice, USD(10), XRP(10)), domain(domainID));
344 env.close();
345
346 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
347 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
348 BEAST_EXPECT(ownerCount(env, alice) == 2);
349 }
350
351 // apply - create lots of domain offers
352 {
353 Env env(*this, features);
354 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
355 PermissionedDEX(env);
356
358 offerSeqs.reserve(100);
359
360 for (size_t i = 0; i <= 100; i++)
361 {
362 auto const bobOfferSeq{env.seq(bob)};
363 offerSeqs.emplace_back(bobOfferSeq);
364
365 env(offer(bob, XRP(10), USD(10)), domain(domainID));
366 env.close();
367 BEAST_EXPECT(checkOffer(
368 env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
369 }
370
371 for (auto const offerSeq : offerSeqs)
372 {
373 env(offer_cancel(bob, offerSeq));
374 env.close();
375 BEAST_EXPECT(!offerExists(env, bob, offerSeq));
376 }
377 }
378 }
379
380 void
382 {
383 testcase("Payment");
384
385 // test preflight - without enabling featurePermissionedDEX amendment
386 {
387 Env env(*this, features - featurePermissionedDEX);
388 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
389 PermissionedDEX(env);
390
391 env(pay(bob, alice, USD(10)),
392 path(~USD),
393 sendmax(XRP(10)),
394 domain(domainID),
396 env.close();
397
398 env.enableFeature(featurePermissionedDEX);
399 env.close();
400
401 env(offer(bob, XRP(10), USD(10)), domain(domainID));
402 env.close();
403
404 env(pay(bob, alice, USD(10)),
405 path(~USD),
406 sendmax(XRP(10)),
407 domain(domainID));
408 env.close();
409 }
410
411 // preclaim - cannot send payment with non existent domain
412 {
413 Env env(*this, features);
414 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
415 PermissionedDEX(env);
416 uint256 const badDomain{
417 "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134"
418 "E5"};
419
420 env(pay(bob, alice, USD(10)),
421 path(~USD),
422 sendmax(XRP(10)),
423 domain(badDomain),
425 env.close();
426 }
427
428 // preclaim - payment with non-domain destination fails
429 {
430 Env env(*this, features);
431 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
432 PermissionedDEX(env);
433
434 env(offer(bob, XRP(10), USD(10)), domain(domainID));
435 env.close();
436
437 // create devin account who is not part of the domain
438 Account devin("devin");
439 env.fund(XRP(1000), devin);
440 env.close();
441 env.trust(USD(1000), devin);
442 env.close();
443 env(pay(gw, devin, USD(100)));
444 env.close();
445
446 // devin is not part of domain
447 env(pay(alice, devin, USD(10)),
448 path(~USD),
449 sendmax(XRP(10)),
450 domain(domainID),
452 env.close();
453
454 // domain owner also issues a credential for devin
455 env(credentials::create(devin, domainOwner, credType));
456 env.close();
457
458 // devin has not yet accepted cred
459 env(pay(alice, devin, USD(10)),
460 path(~USD),
461 sendmax(XRP(10)),
462 domain(domainID),
464 env.close();
465
466 env(credentials::accept(devin, domainOwner, credType));
467 env.close();
468
469 // devin can now receive payment after he is in domain
470 env(pay(alice, devin, USD(10)),
471 path(~USD),
472 sendmax(XRP(10)),
473 domain(domainID));
474 env.close();
475 }
476
477 // preclaim - non-domain sender cannot send payment
478 {
479 Env env(*this, features);
480 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
481 PermissionedDEX(env);
482
483 env(offer(bob, XRP(10), USD(10)), domain(domainID));
484 env.close();
485
486 // create devin account who is not part of the domain
487 Account devin("devin");
488 env.fund(XRP(1000), devin);
489 env.close();
490 env.trust(USD(1000), devin);
491 env.close();
492 env(pay(gw, devin, USD(100)));
493 env.close();
494
495 // devin tries to send domain payment
496 env(pay(devin, alice, USD(10)),
497 path(~USD),
498 sendmax(XRP(10)),
499 domain(domainID),
501 env.close();
502
503 // domain owner also issues a credential for devin
504 env(credentials::create(devin, domainOwner, credType));
505 env.close();
506
507 // devin has not yet accepted cred
508 env(pay(devin, alice, USD(10)),
509 path(~USD),
510 sendmax(XRP(10)),
511 domain(domainID),
513 env.close();
514
515 env(credentials::accept(devin, domainOwner, credType));
516 env.close();
517
518 // devin can now send payment after he is in domain
519 env(pay(devin, alice, USD(10)),
520 path(~USD),
521 sendmax(XRP(10)),
522 domain(domainID));
523 env.close();
524 }
525
526 // apply - domain owner can always send and receive domain payment
527 {
528 Env env(*this, features);
529 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
530 PermissionedDEX(env);
531
532 env(offer(bob, XRP(10), USD(10)), domain(domainID));
533 env.close();
534
535 // domain owner can always be destination
536 env(pay(alice, domainOwner, USD(10)),
537 path(~USD),
538 sendmax(XRP(10)),
539 domain(domainID));
540 env.close();
541
542 env(offer(bob, XRP(10), USD(10)), domain(domainID));
543 env.close();
544
545 // domain owner can send
546 env(pay(domainOwner, alice, USD(10)),
547 path(~USD),
548 sendmax(XRP(10)),
549 domain(domainID));
550 env.close();
551 }
552 }
553
554 void
556 {
557 testcase("Book step");
558
559 // test domain cross currency payment consuming one offer
560 {
561 Env env(*this, features);
562 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
563 PermissionedDEX(env);
564
565 // create a regular offer without domain
566 auto const regularOfferSeq{env.seq(bob)};
567 env(offer(bob, XRP(10), USD(10)));
568 env.close();
569 BEAST_EXPECT(
570 checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10)));
571
572 auto const regularDirKey =
573 getDefaultOfferDirKey(env, bob, regularOfferSeq);
574 BEAST_EXPECT(regularDirKey);
575 BEAST_EXPECT(checkDirectorySize(env, *regularDirKey, 1));
576
577 // a domain payment cannot consume regular offers
578 env(pay(alice, carol, USD(10)),
579 path(~USD),
580 sendmax(XRP(10)),
581 domain(domainID),
583 env.close();
584
585 // create a domain offer
586 auto const domainOfferSeq{env.seq(bob)};
587 env(offer(bob, XRP(10), USD(10)), domain(domainID));
588 env.close();
589
590 BEAST_EXPECT(checkOffer(
591 env, bob, domainOfferSeq, XRP(10), USD(10), 0, true));
592
593 auto const domainDirKey =
594 getDefaultOfferDirKey(env, bob, domainOfferSeq);
595 BEAST_EXPECT(domainDirKey);
596 BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 1));
597
598 // cross-currency permissioned payment consumed
599 // domain offer instead of regular offer
600 env(pay(alice, carol, USD(10)),
601 path(~USD),
602 sendmax(XRP(10)),
603 domain(domainID));
604 env.close();
605 BEAST_EXPECT(!offerExists(env, bob, domainOfferSeq));
606 BEAST_EXPECT(
607 checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10)));
608
609 // domain directory is empty
610 BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 0));
611 BEAST_EXPECT(checkDirectorySize(env, *regularDirKey, 1));
612 }
613
614 // test domain payment consuming two offers in the path
615 {
616 Env env(*this, features);
617 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
618 PermissionedDEX(env);
619
620 auto const EUR = gw["EUR"];
621 env.trust(EUR(1000), alice);
622 env.close();
623 env.trust(EUR(1000), bob);
624 env.close();
625 env.trust(EUR(1000), carol);
626 env.close();
627 env(pay(gw, bob, EUR(100)));
628 env.close();
629
630 // create XRP/USD domain offer
631 auto const usdOfferSeq{env.seq(bob)};
632 env(offer(bob, XRP(10), USD(10)), domain(domainID));
633 env.close();
634
635 BEAST_EXPECT(
636 checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true));
637
638 // payment fail because there isn't eur offer
639 env(pay(alice, carol, EUR(10)),
640 path(~USD, ~EUR),
641 sendmax(XRP(10)),
642 domain(domainID),
644 env.close();
645 BEAST_EXPECT(
646 checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true));
647
648 // bob creates a regular USD/EUR offer
649 auto const regularOfferSeq{env.seq(bob)};
650 env(offer(bob, USD(10), EUR(10)));
651 env.close();
652 BEAST_EXPECT(
653 checkOffer(env, bob, regularOfferSeq, USD(10), EUR(10)));
654
655 // alice tries to pay again, but still fails because the regular
656 // offer cannot be consumed
657 env(pay(alice, carol, EUR(10)),
658 path(~USD, ~EUR),
659 sendmax(XRP(10)),
660 domain(domainID),
662 env.close();
663
664 // bob creates a domain USD/EUR offer
665 auto const eurOfferSeq{env.seq(bob)};
666 env(offer(bob, USD(10), EUR(10)), domain(domainID));
667 env.close();
668 BEAST_EXPECT(
669 checkOffer(env, bob, eurOfferSeq, USD(10), EUR(10), 0, true));
670
671 // alice successfully consume two domain offers: xrp/usd and usd/eur
672 env(pay(alice, carol, EUR(5)),
673 sendmax(XRP(5)),
674 domain(domainID),
675 path(~USD, ~EUR));
676 env.close();
677
678 BEAST_EXPECT(
679 checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true));
680 BEAST_EXPECT(
681 checkOffer(env, bob, eurOfferSeq, USD(5), EUR(5), 0, true));
682
683 // alice successfully consume two domain offers and deletes them
684 // we compute path this time using `paths`
685 env(pay(alice, carol, EUR(5)),
686 sendmax(XRP(5)),
687 domain(domainID),
688 paths(XRP));
689 env.close();
690
691 BEAST_EXPECT(!offerExists(env, bob, usdOfferSeq));
692 BEAST_EXPECT(!offerExists(env, bob, eurOfferSeq));
693
694 // regular offer is not consumed
695 BEAST_EXPECT(
696 checkOffer(env, bob, regularOfferSeq, USD(10), EUR(10)));
697 }
698
699 // domain payment cannot consume offer from another domain
700 {
701 Env env(*this, features);
702 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
703 PermissionedDEX(env);
704
705 // Fund devin and create USD trustline
706 Account badDomainOwner("badDomainOwner");
707 Account devin("devin");
708 env.fund(XRP(1000), badDomainOwner, devin);
709 env.close();
710 env.trust(USD(1000), devin);
711 env.close();
712 env(pay(gw, devin, USD(100)));
713 env.close();
714
715 auto const badCredType = "badCred";
716 pdomain::Credentials credentials{{badDomainOwner, badCredType}};
717 env(pdomain::setTx(badDomainOwner, credentials));
718
719 auto objects = pdomain::getObjects(badDomainOwner, env);
720 auto const badDomainID = objects.begin()->first;
721
722 env(credentials::create(devin, badDomainOwner, badCredType));
723 env.close();
724 env(credentials::accept(devin, badDomainOwner, badCredType));
725
726 // devin creates a domain offer in another domain
727 env(offer(devin, XRP(10), USD(10)), domain(badDomainID));
728 env.close();
729
730 // domain payment can't consume an offer from another domain
731 env(pay(alice, carol, USD(10)),
732 path(~USD),
733 sendmax(XRP(10)),
734 domain(domainID),
736 env.close();
737
738 // bob creates an offer under the right domain
739 auto const bobOfferSeq{env.seq(bob)};
740 env(offer(bob, XRP(10), USD(10)), domain(domainID));
741 env.close();
742 BEAST_EXPECT(
743 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
744
745 // domain payment now consumes from the right domain
746 env(pay(alice, carol, USD(10)),
747 path(~USD),
748 sendmax(XRP(10)),
749 domain(domainID));
750 env.close();
751
752 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
753 }
754
755 // sanity check: devin, who is part of the domain but doesn't have a
756 // trustline with USD issuer, can successfully make a payment using
757 // offer
758 {
759 Env env(*this, features);
760 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
761 PermissionedDEX(env);
762
763 env(offer(bob, XRP(10), USD(10)), domain(domainID));
764 env.close();
765
766 // fund devin but don't create a USD trustline with gateway
767 Account devin("devin");
768 env.fund(XRP(1000), devin);
769 env.close();
770
771 // domain owner also issues a credential for devin
772 env(credentials::create(devin, domainOwner, credType));
773 env.close();
774
775 env(credentials::accept(devin, domainOwner, credType));
776 env.close();
777
778 // successful payment because offer is consumed
779 env(pay(devin, alice, USD(10)), sendmax(XRP(10)), domain(domainID));
780 env.close();
781 }
782
783 // offer becomes unfunded when offer owner's cred expires
784 {
785 Env env(*this, features);
786 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
787 PermissionedDEX(env);
788
789 // create devin account who is not part of the domain
790 Account devin("devin");
791 env.fund(XRP(1000), devin);
792 env.close();
793 env.trust(USD(1000), devin);
794 env.close();
795 env(pay(gw, devin, USD(100)));
796 env.close();
797
798 auto jv = credentials::create(devin, domainOwner, credType);
799 uint32_t const t = env.current()
800 ->header()
801 .parentCloseTime.time_since_epoch()
802 .count();
803 jv[sfExpiration.jsonName] = t + 20;
804 env(jv);
805
806 env(credentials::accept(devin, domainOwner, credType));
807 env.close();
808
809 // devin can still create offer while his cred is not expired
810 auto const offerSeq{env.seq(devin)};
811 env(offer(devin, XRP(10), USD(10)), domain(domainID));
812 env.close();
813
814 // devin's offer can still be consumed while his cred isn't expired
815 env(pay(alice, carol, USD(5)),
816 path(~USD),
817 sendmax(XRP(5)),
818 domain(domainID));
819 env.close();
820 BEAST_EXPECT(
821 checkOffer(env, devin, offerSeq, XRP(5), USD(5), 0, true));
822
823 // advance time
825
826 // devin's offer is unfunded now due to expired cred
827 env(pay(alice, carol, USD(5)),
828 path(~USD),
829 sendmax(XRP(5)),
830 domain(domainID),
832 env.close();
833 BEAST_EXPECT(
834 checkOffer(env, devin, offerSeq, XRP(5), USD(5), 0, true));
835 }
836
837 // offer becomes unfunded when offer owner's cred is removed
838 {
839 Env env(*this, features);
840 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
841 PermissionedDEX(env);
842
843 auto const offerSeq{env.seq(bob)};
844 env(offer(bob, XRP(10), USD(10)), domain(domainID));
845 env.close();
846
847 // bob's offer can still be consumed while his cred exists
848 env(pay(alice, carol, USD(5)),
849 path(~USD),
850 sendmax(XRP(5)),
851 domain(domainID));
852 env.close();
853 BEAST_EXPECT(
854 checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true));
855
856 // remove bob's cred
858 domainOwner, bob, domainOwner, credType));
859 env.close();
860
861 // bob's offer is unfunded now due to expired cred
862 env(pay(alice, carol, USD(5)),
863 path(~USD),
864 sendmax(XRP(5)),
865 domain(domainID),
867 env.close();
868 BEAST_EXPECT(
869 checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true));
870 }
871 }
872
873 void
875 {
876 testcase("Rippling");
877
878 // test a non-domain account can still be part of rippling in a domain
879 // payment. If the domain wishes to control who is allowed to ripple
880 // through, they should set the rippling individually
881 Env env(*this, features);
882 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
883 PermissionedDEX(env);
884
885 auto const EURA = alice["EUR"];
886 auto const EURB = bob["EUR"];
887
888 env.trust(EURA(100), bob);
889 env.trust(EURB(100), carol);
890 env.close();
891
892 // remove bob from domain
893 env(credentials::deleteCred(domainOwner, bob, domainOwner, credType));
894 env.close();
895
896 // alice can still ripple through bob even though he's not part
897 // of the domain, this is intentional
898 env(pay(alice, carol, EURB(10)), paths(EURA), domain(domainID));
899 env.close();
900 env.require(balance(bob, EURA(10)), balance(carol, EURB(10)));
901
902 // carol sets no ripple on bob
903 env(trust(carol, bob["EUR"](0), bob, tfSetNoRipple));
904 env.close();
905
906 // payment no longer works because carol has no ripple on bob
907 env(pay(alice, carol, EURB(5)),
908 paths(EURA),
909 domain(domainID),
911 env.close();
912 env.require(balance(bob, EURA(10)), balance(carol, EURB(10)));
913 }
914
915 void
917 {
918 testcase("Offer token issuer in domain");
919
920 // whether the issuer is in the domain should NOT affect whether an
921 // offer can be consumed in domain payment
922 Env env(*this, features);
923 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
924 PermissionedDEX(env);
925
926 // create an xrp/usd offer with usd as takergets
927 auto const bobOffer1Seq{env.seq(bob)};
928 env(offer(bob, XRP(10), USD(10)), domain(domainID));
929 env.close();
930
931 // create an usd/xrp offer with usd as takerpays
932 auto const bobOffer2Seq{env.seq(bob)};
933 env(offer(bob, USD(10), XRP(10)), domain(domainID), txflags(tfPassive));
934 env.close();
935
936 BEAST_EXPECT(
937 checkOffer(env, bob, bobOffer1Seq, XRP(10), USD(10), 0, true));
938 BEAST_EXPECT(checkOffer(
939 env, bob, bobOffer2Seq, USD(10), XRP(10), lsfPassive, true));
940
941 // remove gateway from domain
942 env(credentials::deleteCred(domainOwner, gw, domainOwner, credType));
943 env.close();
944
945 // payment succeeds even if issuer is not in domain
946 // xrp/usd offer is consumed
947 env(pay(alice, carol, USD(10)),
948 path(~USD),
949 sendmax(XRP(10)),
950 domain(domainID));
951 env.close();
952 BEAST_EXPECT(!offerExists(env, bob, bobOffer1Seq));
953
954 // payment succeeds even if issuer is not in domain
955 // usd/xrp offer is consumed
956 env(pay(alice, carol, XRP(10)),
957 path(~XRP),
958 sendmax(USD(10)),
959 domain(domainID));
960 env.close();
961 BEAST_EXPECT(!offerExists(env, bob, bobOffer2Seq));
962 }
963
964 void
966 {
967 testcase("Remove unfunded offer");
968
969 // checking that an unfunded offer will be implicitly removed by a
970 // successful payment tx
971 Env env(*this, features);
972 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
973 PermissionedDEX(env);
974
975 auto const aliceOfferSeq{env.seq(alice)};
976 env(offer(alice, XRP(100), USD(100)), domain(domainID));
977 env.close();
978
979 auto const bobOfferSeq{env.seq(bob)};
980 env(offer(bob, XRP(20), USD(20)), domain(domainID));
981 env.close();
982
983 BEAST_EXPECT(
984 checkOffer(env, bob, bobOfferSeq, XRP(20), USD(20), 0, true));
985 BEAST_EXPECT(
986 checkOffer(env, alice, aliceOfferSeq, XRP(100), USD(100), 0, true));
987
988 auto const domainDirKey = getDefaultOfferDirKey(env, bob, bobOfferSeq);
989 BEAST_EXPECT(domainDirKey);
990 BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 2));
991
992 // remove alice from domain and thus alice's offer becomes unfunded
993 env(credentials::deleteCred(domainOwner, alice, domainOwner, credType));
994 env.close();
995
996 env(pay(gw, carol, USD(10)),
997 path(~USD),
998 sendmax(XRP(10)),
999 domain(domainID));
1000 env.close();
1001
1002 BEAST_EXPECT(
1003 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
1004
1005 // alice's unfunded offer is removed implicitly
1006 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
1007 BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 1));
1008 }
1009
1010 void
1012 {
1013 testcase("AMM not used");
1014
1015 Env env(*this, features);
1016 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1017 PermissionedDEX(env);
1018 AMM amm(env, alice, XRP(10), USD(50));
1019
1020 // a domain payment isn't able to consume AMM
1021 env(pay(bob, carol, USD(5)),
1022 path(~USD),
1023 sendmax(XRP(5)),
1024 domain(domainID),
1026 env.close();
1027
1028 // a non domain payment can use AMM
1029 env(pay(bob, carol, USD(5)), path(~USD), sendmax(XRP(5)));
1030 env.close();
1031
1032 // USD amount in AMM is changed
1033 auto [xrp, usd, lpt] = amm.balances(XRP, USD);
1034 BEAST_EXPECT(usd == USD(45));
1035 }
1036
1037 void
1039 {
1040 testcase("Hybrid offer create");
1041
1042 // test preflight - invalid hybrid flag
1043 {
1044 Env env(*this, features - featurePermissionedDEX);
1045 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1046 PermissionedDEX(env);
1047
1048 env(offer(bob, XRP(10), USD(10)),
1049 domain(domainID),
1051 ter(temDISABLED));
1052 env.close();
1053
1054 env(offer(bob, XRP(10), USD(10)),
1057 env.close();
1058
1059 env.enableFeature(featurePermissionedDEX);
1060 env.close();
1061
1062 // hybrid offer must have domainID
1063 env(offer(bob, XRP(10), USD(10)),
1066 env.close();
1067
1068 // hybrid offer must have domainID
1069 auto const offerSeq{env.seq(bob)};
1070 env(offer(bob, XRP(10), USD(10)),
1072 domain(domainID));
1073 env.close();
1074 BEAST_EXPECT(checkOffer(
1075 env, bob, offerSeq, XRP(10), USD(10), lsfHybrid, true));
1076 }
1077
1078 // apply - domain offer can cross with hybrid
1079 {
1080 Env env(*this, features);
1081 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1082 PermissionedDEX(env);
1083
1084 auto const bobOfferSeq{env.seq(bob)};
1085 env(offer(bob, XRP(10), USD(10)),
1087 domain(domainID));
1088 env.close();
1089
1090 BEAST_EXPECT(checkOffer(
1091 env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true));
1092 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1093 BEAST_EXPECT(ownerCount(env, bob) == 3);
1094
1095 auto const aliceOfferSeq{env.seq(alice)};
1096 env(offer(alice, USD(10), XRP(10)), domain(domainID));
1097 env.close();
1098
1099 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
1100 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
1101 BEAST_EXPECT(ownerCount(env, alice) == 2);
1102 }
1103
1104 // apply - open offer can cross with hybrid
1105 {
1106 Env env(*this, features);
1107 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1108 PermissionedDEX(env);
1109
1110 auto const bobOfferSeq{env.seq(bob)};
1111 env(offer(bob, XRP(10), USD(10)),
1113 domain(domainID));
1114 env.close();
1115
1116 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1117 BEAST_EXPECT(ownerCount(env, bob) == 3);
1118 BEAST_EXPECT(checkOffer(
1119 env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true));
1120
1121 auto const aliceOfferSeq{env.seq(alice)};
1122 env(offer(alice, USD(10), XRP(10)));
1123 env.close();
1124
1125 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
1126 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
1127 BEAST_EXPECT(ownerCount(env, alice) == 2);
1128 }
1129
1130 // apply - by default, hybrid offer tries to cross with offers in the
1131 // domain book
1132 {
1133 Env env(*this, features);
1134 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1135 PermissionedDEX(env);
1136
1137 auto const bobOfferSeq{env.seq(bob)};
1138 env(offer(bob, XRP(10), USD(10)), domain(domainID));
1139 env.close();
1140
1141 BEAST_EXPECT(
1142 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
1143 BEAST_EXPECT(ownerCount(env, bob) == 3);
1144
1145 // hybrid offer auto crosses with domain offer
1146 auto const aliceOfferSeq{env.seq(alice)};
1147 env(offer(alice, USD(10), XRP(10)),
1148 domain(domainID),
1149 txflags(tfHybrid));
1150 env.close();
1151
1152 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
1153 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
1154 BEAST_EXPECT(ownerCount(env, alice) == 2);
1155 }
1156
1157 // apply - hybrid offer does not automatically cross with open offers
1158 // because by default, it only tries to cross domain offers
1159 {
1160 Env env(*this, features);
1161 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1162 PermissionedDEX(env);
1163
1164 auto const bobOfferSeq{env.seq(bob)};
1165 env(offer(bob, XRP(10), USD(10)));
1166 env.close();
1167
1168 BEAST_EXPECT(
1169 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false));
1170 BEAST_EXPECT(ownerCount(env, bob) == 3);
1171
1172 // hybrid offer auto crosses with domain offer
1173 auto const aliceOfferSeq{env.seq(alice)};
1174 env(offer(alice, USD(10), XRP(10)),
1175 domain(domainID),
1176 txflags(tfHybrid));
1177 env.close();
1178
1179 BEAST_EXPECT(offerExists(env, alice, aliceOfferSeq));
1180 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1181 BEAST_EXPECT(
1182 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false));
1183 BEAST_EXPECT(checkOffer(
1184 env, alice, aliceOfferSeq, USD(10), XRP(10), lsfHybrid, true));
1185 BEAST_EXPECT(ownerCount(env, alice) == 3);
1186 }
1187 }
1188
1189 void
1191 {
1192 testcase("Hybrid invalid offer");
1193
1194 // bob has a hybrid offer and then he is removed from domain.
1195 // in this case, the hybrid offer will be considered as unfunded even in
1196 // a regular payment
1197 Env env(*this, features);
1198 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1199 PermissionedDEX(env);
1200
1201 auto const hybridOfferSeq{env.seq(bob)};
1202 env(offer(bob, XRP(50), USD(50)), txflags(tfHybrid), domain(domainID));
1203 env.close();
1204
1205 // remove bob from domain
1206 env(credentials::deleteCred(domainOwner, bob, domainOwner, credType));
1207 env.close();
1208
1209 // bob's hybrid offer is unfunded and can not be consumed in a domain
1210 // payment
1211 env(pay(alice, carol, USD(5)),
1212 path(~USD),
1213 sendmax(XRP(5)),
1214 domain(domainID),
1216 env.close();
1217 BEAST_EXPECT(checkOffer(
1218 env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true));
1219
1220 // bob's unfunded hybrid offer can't be consumed even with a regular
1221 // payment
1222 env(pay(alice, carol, USD(5)),
1223 path(~USD),
1224 sendmax(XRP(5)),
1226 env.close();
1227 BEAST_EXPECT(checkOffer(
1228 env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true));
1229
1230 // create a regular offer
1231 auto const regularOfferSeq{env.seq(bob)};
1232 env(offer(bob, XRP(10), USD(10)));
1233 env.close();
1234 BEAST_EXPECT(offerExists(env, bob, regularOfferSeq));
1235 BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10)));
1236
1237 auto const sleHybridOffer =
1238 env.le(keylet::offer(bob.id(), hybridOfferSeq));
1239 BEAST_EXPECT(sleHybridOffer);
1240 auto const openDir =
1241 sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(
1242 sfBookDirectory);
1243 BEAST_EXPECT(checkDirectorySize(env, openDir, 2));
1244
1245 // this normal payment should consume the regular offer and remove the
1246 // unfunded hybrid offer
1247 env(pay(alice, carol, USD(5)), path(~USD), sendmax(XRP(5)));
1248 env.close();
1249
1250 BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq));
1251 BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(5), USD(5)));
1252 BEAST_EXPECT(checkDirectorySize(env, openDir, 1));
1253 }
1254
1255 void
1257 {
1258 testcase("Hybrid book step");
1259
1260 // both non domain and domain payments can consume hybrid offer
1261 {
1262 Env env(*this, features);
1263 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1264 PermissionedDEX(env);
1265
1266 auto const hybridOfferSeq{env.seq(bob)};
1267 env(offer(bob, XRP(10), USD(10)),
1269 domain(domainID));
1270 env.close();
1271
1272 env(pay(alice, carol, USD(5)),
1273 path(~USD),
1274 sendmax(XRP(5)),
1275 domain(domainID));
1276 env.close();
1277 BEAST_EXPECT(checkOffer(
1278 env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true));
1279
1280 // hybrid offer can't be consumed since bob is not in domain anymore
1281 env(pay(alice, carol, USD(5)), path(~USD), sendmax(XRP(5)));
1282 env.close();
1283
1284 BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq));
1285 }
1286
1287 // someone from another domain can't cross hybrid if they specified
1288 // wrong domainID
1289 {
1290 Env env(*this, features);
1291 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1292 PermissionedDEX(env);
1293
1294 // Fund accounts
1295 Account badDomainOwner("badDomainOwner");
1296 Account devin("devin");
1297 env.fund(XRP(1000), badDomainOwner, devin);
1298 env.close();
1299
1300 auto const badCredType = "badCred";
1301 pdomain::Credentials credentials{{badDomainOwner, badCredType}};
1302 env(pdomain::setTx(badDomainOwner, credentials));
1303
1304 auto objects = pdomain::getObjects(badDomainOwner, env);
1305 auto const badDomainID = objects.begin()->first;
1306
1307 env(credentials::create(devin, badDomainOwner, badCredType));
1308 env.close();
1309 env(credentials::accept(devin, badDomainOwner, badCredType));
1310 env.close();
1311
1312 auto const hybridOfferSeq{env.seq(bob)};
1313 env(offer(bob, XRP(10), USD(10)),
1315 domain(domainID));
1316 env.close();
1317
1318 // other domains can't consume the offer
1319 env(pay(devin, badDomainOwner, USD(5)),
1320 path(~USD),
1321 sendmax(XRP(5)),
1322 domain(badDomainID),
1323 ter(tecPATH_DRY));
1324 env.close();
1325 BEAST_EXPECT(checkOffer(
1326 env, bob, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true));
1327
1328 env(pay(alice, carol, USD(5)),
1329 path(~USD),
1330 sendmax(XRP(5)),
1331 domain(domainID));
1332 env.close();
1333 BEAST_EXPECT(checkOffer(
1334 env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true));
1335
1336 // hybrid offer can't be consumed since bob is not in domain anymore
1337 env(pay(alice, carol, USD(5)), path(~USD), sendmax(XRP(5)));
1338 env.close();
1339
1340 BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq));
1341 }
1342
1343 // test domain payment consuming two offers w/ hybrid offer
1344 {
1345 Env env(*this, features);
1346 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1347 PermissionedDEX(env);
1348
1349 auto const EUR = gw["EUR"];
1350 env.trust(EUR(1000), alice);
1351 env.close();
1352 env.trust(EUR(1000), bob);
1353 env.close();
1354 env.trust(EUR(1000), carol);
1355 env.close();
1356 env(pay(gw, bob, EUR(100)));
1357 env.close();
1358
1359 auto const usdOfferSeq{env.seq(bob)};
1360 env(offer(bob, XRP(10), USD(10)), domain(domainID));
1361 env.close();
1362
1363 BEAST_EXPECT(
1364 checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true));
1365
1366 // payment fail because there isn't eur offer
1367 env(pay(alice, carol, EUR(5)),
1368 path(~USD, ~EUR),
1369 sendmax(XRP(5)),
1370 domain(domainID),
1372 env.close();
1373 BEAST_EXPECT(
1374 checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true));
1375
1376 // bob creates a hybrid eur offer
1377 auto const eurOfferSeq{env.seq(bob)};
1378 env(offer(bob, USD(10), EUR(10)),
1379 domain(domainID),
1380 txflags(tfHybrid));
1381 env.close();
1382 BEAST_EXPECT(checkOffer(
1383 env, bob, eurOfferSeq, USD(10), EUR(10), lsfHybrid, true));
1384
1385 // alice successfully consume two domain offers: xrp/usd and usd/eur
1386 env(pay(alice, carol, EUR(5)),
1387 path(~USD, ~EUR),
1388 sendmax(XRP(5)),
1389 domain(domainID));
1390 env.close();
1391
1392 BEAST_EXPECT(
1393 checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true));
1394 BEAST_EXPECT(checkOffer(
1395 env, bob, eurOfferSeq, USD(5), EUR(5), lsfHybrid, true));
1396 }
1397
1398 // test regular payment using a regular offer and a hybrid offer
1399 {
1400 Env env(*this, features);
1401 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1402 PermissionedDEX(env);
1403
1404 auto const EUR = gw["EUR"];
1405 env.trust(EUR(1000), alice);
1406 env.close();
1407 env.trust(EUR(1000), bob);
1408 env.close();
1409 env.trust(EUR(1000), carol);
1410 env.close();
1411 env(pay(gw, bob, EUR(100)));
1412 env.close();
1413
1414 // bob creates a regular usd offer
1415 auto const usdOfferSeq{env.seq(bob)};
1416 env(offer(bob, XRP(10), USD(10)));
1417 env.close();
1418
1419 BEAST_EXPECT(
1420 checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, false));
1421
1422 // bob creates a hybrid eur offer
1423 auto const eurOfferSeq{env.seq(bob)};
1424 env(offer(bob, USD(10), EUR(10)),
1425 domain(domainID),
1426 txflags(tfHybrid));
1427 env.close();
1428 BEAST_EXPECT(checkOffer(
1429 env, bob, eurOfferSeq, USD(10), EUR(10), lsfHybrid, true));
1430
1431 // alice successfully consume two offers: xrp/usd and usd/eur
1432 env(pay(alice, carol, EUR(5)), path(~USD, ~EUR), sendmax(XRP(5)));
1433 env.close();
1434
1435 BEAST_EXPECT(
1436 checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, false));
1437 BEAST_EXPECT(checkOffer(
1438 env, bob, eurOfferSeq, USD(5), EUR(5), lsfHybrid, true));
1439 }
1440 }
1441
1442 void
1444 {
1445 Env env(*this, features);
1446 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1447 PermissionedDEX(env);
1448
1450 offerSeqs.reserve(100);
1451
1452 Book domainBook{Issue(XRP), Issue(USD), domainID};
1453 Book openBook{Issue(XRP), Issue(USD), std::nullopt};
1454
1455 auto const domainDir = getBookDirKey(domainBook, XRP(10), USD(10));
1456 auto const openDir = getBookDirKey(openBook, XRP(10), USD(10));
1457
1458 size_t dirCnt = 100;
1459
1460 for (size_t i = 1; i <= dirCnt; i++)
1461 {
1462 auto const bobOfferSeq{env.seq(bob)};
1463 offerSeqs.emplace_back(bobOfferSeq);
1464 env(offer(bob, XRP(10), USD(10)),
1466 domain(domainID));
1467 env.close();
1468
1469 auto const sleOffer = env.le(keylet::offer(bob.id(), bobOfferSeq));
1470 BEAST_EXPECT(sleOffer);
1471 BEAST_EXPECT(sleOffer->getFieldH256(sfBookDirectory) == domainDir);
1472 BEAST_EXPECT(
1473 sleOffer->getFieldArray(sfAdditionalBooks).size() == 1);
1474 BEAST_EXPECT(
1475 sleOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(
1476 sfBookDirectory) == openDir);
1477
1478 BEAST_EXPECT(checkOffer(
1479 env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true));
1480 BEAST_EXPECT(checkDirectorySize(env, domainDir, i));
1481 BEAST_EXPECT(checkDirectorySize(env, openDir, i));
1482 }
1483
1484 for (auto const offerSeq : offerSeqs)
1485 {
1486 env(offer_cancel(bob, offerSeq));
1487 env.close();
1488 dirCnt--;
1489 BEAST_EXPECT(!offerExists(env, bob, offerSeq));
1490 BEAST_EXPECT(checkDirectorySize(env, domainDir, dirCnt));
1491 BEAST_EXPECT(checkDirectorySize(env, openDir, dirCnt));
1492 }
1493 }
1494
1495 void
1497 {
1498 testcase("Auto bridge");
1499
1500 Env env(*this, features);
1501 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1502 PermissionedDEX(env);
1503 auto const EUR = gw["EUR"];
1504
1505 for (auto const& account : {alice, bob, carol})
1506 {
1507 env(trust(account, EUR(10000)));
1508 env.close();
1509 }
1510
1511 env(pay(gw, carol, EUR(1)));
1512 env.close();
1513
1514 auto const aliceOfferSeq{env.seq(alice)};
1515 auto const bobOfferSeq{env.seq(bob)};
1516 env(offer(alice, XRP(100), USD(1)), domain(domainID));
1517 env(offer(bob, EUR(1), XRP(100)), domain(domainID));
1518 env.close();
1519
1520 // carol's offer should cross bob and alice's offers due to auto
1521 // bridging
1522 auto const carolOfferSeq{env.seq(carol)};
1523 env(offer(carol, USD(1), EUR(1)), domain(domainID));
1524 env.close();
1525
1526 BEAST_EXPECT(!offerExists(env, bob, aliceOfferSeq));
1527 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
1528 BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq));
1529 }
1530
1531public:
1532 void
1533 run() override
1534 {
1536
1537 // Test domain offer (w/o hybrid)
1546
1547 // Test hybrid offers
1552 }
1553};
1554
1555BEAST_DEFINE_TESTSUITE(PermissionedDEX, app, xrpl);
1556
1557} // namespace test
1558} // namespace xrpl
A testsuite class.
Definition suite.h:52
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:152
Specifies an order book.
Definition Book.h:17
A currency issued by an account.
Definition Issue.h:14
bool checkOffer(Env const &env, Account const &account, std::uint32_t offerSeq, STAmount const &takerPays, STAmount const &takerGets, uint32_t const flags=0, bool const domainOffer=false)
bool checkDirectorySize(Env const &env, uint256 directory, std::uint32_t dirSize)
void testRippling(FeatureBitset features)
void testOfferCreate(FeatureBitset features)
void testHybridOfferDirectories(FeatureBitset features)
void testHybridOfferCreate(FeatureBitset features)
void testAutoBridge(FeatureBitset features)
void testOfferTokenIssuerInDomain(FeatureBitset features)
void run() override
Runs the suite.
void testPayment(FeatureBitset features)
void testHybridBookStep(FeatureBitset features)
void testAmmNotUsed(FeatureBitset features)
void testBookStep(FeatureBitset features)
uint256 getBookDirKey(Book const &book, STAmount const &takerPays, STAmount const &takerGets)
void testHybridInvalidOffer(FeatureBitset features)
std::optional< uint256 > getDefaultOfferDirKey(Env const &env, Account const &account, std::uint32_t offerSeq)
bool offerExists(Env const &env, Account const &account, std::uint32_t offerSeq)
void testRemoveUnfundedOffer(FeatureBitset features)
Convenience class to test AMM functionality.
Definition AMM.h:105
Immutable cryptographic account descriptor.
Definition Account.h:20
A transaction testing environment.
Definition Env.h:102
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:104
std::shared_ptr< SLE const > le(Account const &account) const
Return an account root.
Definition Env.cpp:260
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:272
void enableFeature(uint256 const feature)
Definition Env.cpp:659
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:251
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:303
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:530
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:314
A balance matches.
Definition balance.h:20
Set the domain on a JTx.
Definition domain.h:11
Match set account flags.
Definition flags.h:109
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:14
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:16
Set the flags on a JTx.
Definition txflags.h:12
T emplace_back(T... args)
T is_same_v
Keylet quality(Keylet const &k, std::uint64_t q) noexcept
The initial directory page for a specific quality.
Definition Indexes.cpp:262
static book_t const book
Definition Indexes.h:86
Keylet offer(AccountID const &id, std::uint32_t seq) noexcept
An offer from an account.
Definition Indexes.cpp:256
Keylet page(uint256 const &root, std::uint64_t index=0) noexcept
A page in a directory.
Definition Indexes.cpp:362
Json::Value accept(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:29
Json::Value deleteCred(jtx::Account const &acc, jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:43
Json::Value create(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:13
Json::Value setTx(AccountID const &account, Credentials const &credentials, std::optional< uint256 > domain)
std::map< uint256, Json::Value > getObjects(Account const &account, Env &env, bool withType)
Json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:13
std::uint32_t ownerCount(Env const &env, Account const &account)
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
Json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:11
Json::Value offer_cancel(Account const &account, std::uint32_t offerSeq)
Cancel an offer.
Definition offer.cpp:27
FeatureBitset testable_amendments()
Definition Env.h:55
Json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:10
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
constexpr std::uint32_t tfPassive
Definition TxFlags.h:79
constexpr std::uint32_t tfHybrid
Definition TxFlags.h:83
constexpr std::uint32_t tfSetNoRipple
Definition TxFlags.h:97
std::uint64_t getRate(STAmount const &offerOut, STAmount const &offerIn)
Definition STAmount.cpp:443
@ temINVALID_FLAG
Definition TER.h:92
@ temDISABLED
Definition TER.h:95
@ tecPATH_PARTIAL
Definition TER.h:264
@ tecPATH_DRY
Definition TER.h:276
@ tecNO_PERMISSION
Definition TER.h:287
T reserve(T... args)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
uint256 key
Definition Keylet.h:21