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 ripple {
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 ->info()
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 ->info()
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 implictly removed by a
970 // successfuly 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 hyrbid)
1546
1547 // Test hybrid offers
1552 }
1553};
1554
1555BEAST_DEFINE_TESTSUITE(PermissionedDEX, app, ripple);
1556
1557} // namespace test
1558} // namespace ripple
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
void testOfferTokenIssuerInDomain(FeatureBitset features)
void testPayment(FeatureBitset features)
void testRippling(FeatureBitset features)
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)
void testOfferCreate(FeatureBitset features)
void testHybridBookStep(FeatureBitset features)
bool checkDirectorySize(Env const &env, uint256 directory, std::uint32_t dirSize)
void testHybridOfferCreate(FeatureBitset features)
void testAutoBridge(FeatureBitset features)
std::optional< uint256 > getDefaultOfferDirKey(Env const &env, Account const &account, std::uint32_t offerSeq)
void testBookStep(FeatureBitset features)
void testHybridOfferDirectories(FeatureBitset features)
bool offerExists(Env const &env, Account const &account, std::uint32_t offerSeq)
uint256 getBookDirKey(Book const &book, STAmount const &takerPays, STAmount const &takerGets)
void testHybridInvalidOffer(FeatureBitset features)
void testRemoveUnfundedOffer(FeatureBitset features)
void testAmmNotUsed(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
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:250
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:528
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:312
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:103
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:302
void enableFeature(uint256 const feature)
Definition Env.cpp:656
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:271
std::shared_ptr< SLE const > le(Account const &account) const
Return an account root.
Definition Env.cpp:259
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:261
static book_t const book
Definition Indexes.h:86
Keylet page(uint256 const &root, std::uint64_t index=0) noexcept
A page in a directory.
Definition Indexes.cpp:361
Keylet offer(AccountID const &id, std::uint32_t seq) noexcept
An offer from an account.
Definition Indexes.cpp:255
Json::Value create(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:13
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 setTx(AccountID const &account, Credentials const &credentials, std::optional< uint256 > domain)
std::map< uint256, Json::Value > getObjects(Account const &account, Env &env, bool withType)
std::uint32_t ownerCount(Env const &env, Account const &account)
Json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:13
Json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:11
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
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
Json::Value offer_cancel(Account const &account, std::uint32_t offerSeq)
Cancel an offer.
Definition offer.cpp:27
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
constexpr std::uint32_t tfHybrid
Definition TxFlags.h:83
constexpr std::uint32_t tfPassive
Definition TxFlags.h:79
std::uint64_t getRate(STAmount const &offerOut, STAmount const &offerIn)
Definition STAmount.cpp:444
@ tecNO_PERMISSION
Definition TER.h:287
@ tecPATH_PARTIAL
Definition TER.h:264
@ tecPATH_DRY
Definition TER.h:276
constexpr std::uint32_t tfSetNoRipple
Definition TxFlags.h:97
@ temINVALID_FLAG
Definition TER.h:92
@ temDISABLED
Definition TER.h:95
T reserve(T... args)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
uint256 key
Definition Keylet.h:21