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