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
26#include <xrpl/basics/Blob.h>
27#include <xrpl/basics/Slice.h>
28#include <xrpl/beast/unit_test/suite.h>
29#include <xrpl/ledger/ApplyViewImpl.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 // preclaim - someone outside of the domain cannot create domain offer
211 {
212 Env env(*this, features);
213 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
214 PermissionedDEX(env);
215
216 // create devin account who is not part of the domain
217 Account devin("devin");
218 env.fund(XRP(1000), devin);
219 env.close();
220 env.trust(USD(1000), devin);
221 env.close();
222 env(pay(gw, devin, USD(100)));
223 env.close();
224
225 env(offer(devin, XRP(10), USD(10)),
226 domain(domainID),
228 env.close();
229
230 // domain owner also issues a credential for devin
231 env(credentials::create(devin, domainOwner, credType));
232 env.close();
233
234 // devin still cannot create offer since he didn't accept credential
235 env(offer(devin, XRP(10), USD(10)),
236 domain(domainID),
238 env.close();
239
240 env(credentials::accept(devin, domainOwner, credType));
241 env.close();
242
243 env(offer(devin, XRP(10), USD(10)), domain(domainID));
244 env.close();
245 }
246
247 // preclaim - someone with expired cred cannot create domain offer
248 {
249 Env env(*this, features);
250 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
251 PermissionedDEX(env);
252
253 // create devin account who is not part of the domain
254 Account devin("devin");
255 env.fund(XRP(1000), devin);
256 env.close();
257 env.trust(USD(1000), devin);
258 env.close();
259 env(pay(gw, devin, USD(100)));
260 env.close();
261
262 auto jv = credentials::create(devin, domainOwner, credType);
263 uint32_t const t = env.current()
264 ->info()
265 .parentCloseTime.time_since_epoch()
266 .count();
267 jv[sfExpiration.jsonName] = t + 20;
268 env(jv);
269
270 env(credentials::accept(devin, domainOwner, credType));
271 env.close();
272
273 // devin can still create offer while his cred is not expired
274 env(offer(devin, XRP(10), USD(10)), domain(domainID));
275 env.close();
276
277 // time advance
279
280 // devin cannot create offer with expired cred
281 env(offer(devin, XRP(10), USD(10)),
282 domain(domainID),
284 env.close();
285 }
286
287 // preclaim - cannot create an offer in a non existent domain
288 {
289 Env env(*this, features);
290 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
291 PermissionedDEX(env);
292 uint256 const badDomain{
293 "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134"
294 "E5"};
295
296 env(offer(bob, XRP(10), USD(10)),
297 domain(badDomain),
299 env.close();
300 }
301
302 // apply - offer can be created even if takergets 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, XRP(10), USD(10)), domain(domainID));
315 env.close();
316
317 BEAST_EXPECT(
318 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
319 }
320
321 // apply - offer can be created even if takerpays issuer is not in
322 // domain
323 {
324 Env env(*this, features);
325 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
326 PermissionedDEX(env);
327
329 domainOwner, gw, domainOwner, credType));
330 env.close();
331
332 auto const bobOfferSeq{env.seq(bob)};
333 env(offer(bob, USD(10), XRP(10)), domain(domainID));
334 env.close();
335
336 BEAST_EXPECT(
337 checkOffer(env, bob, bobOfferSeq, USD(10), XRP(10), 0, true));
338 }
339
340 // apply - two domain offers cross with each other
341 {
342 Env env(*this, features);
343 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
344 PermissionedDEX(env);
345
346 auto const bobOfferSeq{env.seq(bob)};
347 env(offer(bob, XRP(10), USD(10)), domain(domainID));
348 env.close();
349
350 BEAST_EXPECT(
351 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
352 BEAST_EXPECT(ownerCount(env, bob) == 3);
353
354 // a non domain offer cannot cross with domain offer
355 env(offer(carol, USD(10), XRP(10)));
356 env.close();
357
358 BEAST_EXPECT(
359 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
360
361 auto const aliceOfferSeq{env.seq(alice)};
362 env(offer(alice, USD(10), XRP(10)), domain(domainID));
363 env.close();
364
365 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
366 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
367 BEAST_EXPECT(ownerCount(env, alice) == 2);
368 }
369
370 // apply - create lots of domain offers
371 {
372 Env env(*this, features);
373 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
374 PermissionedDEX(env);
375
377 offerSeqs.reserve(100);
378
379 for (size_t i = 0; i <= 100; i++)
380 {
381 auto const bobOfferSeq{env.seq(bob)};
382 offerSeqs.emplace_back(bobOfferSeq);
383
384 env(offer(bob, XRP(10), USD(10)), domain(domainID));
385 env.close();
386 BEAST_EXPECT(checkOffer(
387 env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
388 }
389
390 for (auto const offerSeq : offerSeqs)
391 {
392 env(offer_cancel(bob, offerSeq));
393 env.close();
394 BEAST_EXPECT(!offerExists(env, bob, offerSeq));
395 }
396 }
397 }
398
399 void
401 {
402 testcase("Payment");
403
404 // test preflight - without enabling featurePermissionedDEX amendment
405 {
406 Env env(*this, features - featurePermissionedDEX);
407 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
408 PermissionedDEX(env);
409
410 env(pay(bob, alice, USD(10)),
411 path(~USD),
412 sendmax(XRP(10)),
413 domain(domainID),
415 env.close();
416
417 env.enableFeature(featurePermissionedDEX);
418 env.close();
419
420 env(offer(bob, XRP(10), USD(10)), domain(domainID));
421 env.close();
422
423 env(pay(bob, alice, USD(10)),
424 path(~USD),
425 sendmax(XRP(10)),
426 domain(domainID));
427 env.close();
428 }
429
430 // preclaim - cannot send payment with non existent domain
431 {
432 Env env(*this, features);
433 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
434 PermissionedDEX(env);
435 uint256 const badDomain{
436 "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134"
437 "E5"};
438
439 env(pay(bob, alice, USD(10)),
440 path(~USD),
441 sendmax(XRP(10)),
442 domain(badDomain),
444 env.close();
445 }
446
447 // preclaim - payment with non-domain destination fails
448 {
449 Env env(*this, features);
450 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
451 PermissionedDEX(env);
452
453 env(offer(bob, XRP(10), USD(10)), domain(domainID));
454 env.close();
455
456 // create devin account who is not part of the domain
457 Account devin("devin");
458 env.fund(XRP(1000), devin);
459 env.close();
460 env.trust(USD(1000), devin);
461 env.close();
462 env(pay(gw, devin, USD(100)));
463 env.close();
464
465 // devin is not part of domain
466 env(pay(alice, devin, USD(10)),
467 path(~USD),
468 sendmax(XRP(10)),
469 domain(domainID),
471 env.close();
472
473 // domain owner also issues a credential for devin
474 env(credentials::create(devin, domainOwner, credType));
475 env.close();
476
477 // devin has not yet accepted cred
478 env(pay(alice, devin, USD(10)),
479 path(~USD),
480 sendmax(XRP(10)),
481 domain(domainID),
483 env.close();
484
485 env(credentials::accept(devin, domainOwner, credType));
486 env.close();
487
488 // devin can now receive payment after he is in domain
489 env(pay(alice, devin, USD(10)),
490 path(~USD),
491 sendmax(XRP(10)),
492 domain(domainID));
493 env.close();
494 }
495
496 // preclaim - non-domain sender cannot send payment
497 {
498 Env env(*this, features);
499 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
500 PermissionedDEX(env);
501
502 env(offer(bob, XRP(10), USD(10)), domain(domainID));
503 env.close();
504
505 // create devin account who is not part of the domain
506 Account devin("devin");
507 env.fund(XRP(1000), devin);
508 env.close();
509 env.trust(USD(1000), devin);
510 env.close();
511 env(pay(gw, devin, USD(100)));
512 env.close();
513
514 // devin tries to send domain payment
515 env(pay(devin, alice, USD(10)),
516 path(~USD),
517 sendmax(XRP(10)),
518 domain(domainID),
520 env.close();
521
522 // domain owner also issues a credential for devin
523 env(credentials::create(devin, domainOwner, credType));
524 env.close();
525
526 // devin has not yet accepted cred
527 env(pay(devin, alice, USD(10)),
528 path(~USD),
529 sendmax(XRP(10)),
530 domain(domainID),
532 env.close();
533
534 env(credentials::accept(devin, domainOwner, credType));
535 env.close();
536
537 // devin can now send payment after he is in domain
538 env(pay(devin, alice, USD(10)),
539 path(~USD),
540 sendmax(XRP(10)),
541 domain(domainID));
542 env.close();
543 }
544
545 // apply - domain owner can always send and receive domain payment
546 {
547 Env env(*this, features);
548 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
549 PermissionedDEX(env);
550
551 env(offer(bob, XRP(10), USD(10)), domain(domainID));
552 env.close();
553
554 // domain owner can always be destination
555 env(pay(alice, domainOwner, USD(10)),
556 path(~USD),
557 sendmax(XRP(10)),
558 domain(domainID));
559 env.close();
560
561 env(offer(bob, XRP(10), USD(10)), domain(domainID));
562 env.close();
563
564 // domain owner can send
565 env(pay(domainOwner, alice, USD(10)),
566 path(~USD),
567 sendmax(XRP(10)),
568 domain(domainID));
569 env.close();
570 }
571 }
572
573 void
575 {
576 testcase("Book step");
577
578 // test domain cross currency payment consuming one offer
579 {
580 Env env(*this, features);
581 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
582 PermissionedDEX(env);
583
584 // create a regular offer without domain
585 auto const regularOfferSeq{env.seq(bob)};
586 env(offer(bob, XRP(10), USD(10)));
587 env.close();
588 BEAST_EXPECT(
589 checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10)));
590
591 auto const regularDirKey =
592 getDefaultOfferDirKey(env, bob, regularOfferSeq);
593 BEAST_EXPECT(regularDirKey);
594 BEAST_EXPECT(checkDirectorySize(env, *regularDirKey, 1));
595
596 // a domain payment cannot consume regular offers
597 env(pay(alice, carol, USD(10)),
598 path(~USD),
599 sendmax(XRP(10)),
600 domain(domainID),
602 env.close();
603
604 // create a domain offer
605 auto const domainOfferSeq{env.seq(bob)};
606 env(offer(bob, XRP(10), USD(10)), domain(domainID));
607 env.close();
608
609 BEAST_EXPECT(checkOffer(
610 env, bob, domainOfferSeq, XRP(10), USD(10), 0, true));
611
612 auto const domainDirKey =
613 getDefaultOfferDirKey(env, bob, domainOfferSeq);
614 BEAST_EXPECT(domainDirKey);
615 BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 1));
616
617 // cross-currency permissioned payment consumed
618 // domain offer instead of regular offer
619 env(pay(alice, carol, USD(10)),
620 path(~USD),
621 sendmax(XRP(10)),
622 domain(domainID));
623 env.close();
624 BEAST_EXPECT(!offerExists(env, bob, domainOfferSeq));
625 BEAST_EXPECT(
626 checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10)));
627
628 // domain directory is empty
629 BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 0));
630 BEAST_EXPECT(checkDirectorySize(env, *regularDirKey, 1));
631 }
632
633 // test domain payment consuming two offers in the path
634 {
635 Env env(*this, features);
636 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
637 PermissionedDEX(env);
638
639 auto const EUR = gw["EUR"];
640 env.trust(EUR(1000), alice);
641 env.close();
642 env.trust(EUR(1000), bob);
643 env.close();
644 env.trust(EUR(1000), carol);
645 env.close();
646 env(pay(gw, bob, EUR(100)));
647 env.close();
648
649 // create XRP/USD domain offer
650 auto const usdOfferSeq{env.seq(bob)};
651 env(offer(bob, XRP(10), USD(10)), domain(domainID));
652 env.close();
653
654 BEAST_EXPECT(
655 checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true));
656
657 // payment fail because there isn't eur offer
658 env(pay(alice, carol, EUR(10)),
659 path(~USD, ~EUR),
660 sendmax(XRP(10)),
661 domain(domainID),
663 env.close();
664 BEAST_EXPECT(
665 checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true));
666
667 // bob creates a regular USD/EUR offer
668 auto const regularOfferSeq{env.seq(bob)};
669 env(offer(bob, USD(10), EUR(10)));
670 env.close();
671 BEAST_EXPECT(
672 checkOffer(env, bob, regularOfferSeq, USD(10), EUR(10)));
673
674 // alice tries to pay again, but still fails because the regular
675 // offer cannot be consumed
676 env(pay(alice, carol, EUR(10)),
677 path(~USD, ~EUR),
678 sendmax(XRP(10)),
679 domain(domainID),
681 env.close();
682
683 // bob creates a domain USD/EUR offer
684 auto const eurOfferSeq{env.seq(bob)};
685 env(offer(bob, USD(10), EUR(10)), domain(domainID));
686 env.close();
687 BEAST_EXPECT(
688 checkOffer(env, bob, eurOfferSeq, USD(10), EUR(10), 0, true));
689
690 // alice successfully consume two domain offers: xrp/usd and usd/eur
691 env(pay(alice, carol, EUR(5)),
692 sendmax(XRP(5)),
693 domain(domainID),
694 path(~USD, ~EUR));
695 env.close();
696
697 BEAST_EXPECT(
698 checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true));
699 BEAST_EXPECT(
700 checkOffer(env, bob, eurOfferSeq, USD(5), EUR(5), 0, true));
701
702 // alice successfully consume two domain offers and deletes them
703 // we compute path this time using `paths`
704 env(pay(alice, carol, EUR(5)),
705 sendmax(XRP(5)),
706 domain(domainID),
707 paths(XRP));
708 env.close();
709
710 BEAST_EXPECT(!offerExists(env, bob, usdOfferSeq));
711 BEAST_EXPECT(!offerExists(env, bob, eurOfferSeq));
712
713 // regular offer is not consumed
714 BEAST_EXPECT(
715 checkOffer(env, bob, regularOfferSeq, USD(10), EUR(10)));
716 }
717
718 // domain payment cannot consume offer from another domain
719 {
720 Env env(*this, features);
721 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
722 PermissionedDEX(env);
723
724 // Fund devin and create USD trustline
725 Account badDomainOwner("badDomainOwner");
726 Account devin("devin");
727 env.fund(XRP(1000), badDomainOwner, devin);
728 env.close();
729 env.trust(USD(1000), devin);
730 env.close();
731 env(pay(gw, devin, USD(100)));
732 env.close();
733
734 auto const badCredType = "badCred";
735 pdomain::Credentials credentials{{badDomainOwner, badCredType}};
736 env(pdomain::setTx(badDomainOwner, credentials));
737
738 auto objects = pdomain::getObjects(badDomainOwner, env);
739 auto const badDomainID = objects.begin()->first;
740
741 env(credentials::create(devin, badDomainOwner, badCredType));
742 env.close();
743 env(credentials::accept(devin, badDomainOwner, badCredType));
744
745 // devin creates a domain offer in another domain
746 env(offer(devin, XRP(10), USD(10)), domain(badDomainID));
747 env.close();
748
749 // domain payment can't consume an offer from another domain
750 env(pay(alice, carol, USD(10)),
751 path(~USD),
752 sendmax(XRP(10)),
753 domain(domainID),
755 env.close();
756
757 // bob creates an offer under the right domain
758 auto const bobOfferSeq{env.seq(bob)};
759 env(offer(bob, XRP(10), USD(10)), domain(domainID));
760 env.close();
761 BEAST_EXPECT(
762 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
763
764 // domain payment now consumes from the right domain
765 env(pay(alice, carol, USD(10)),
766 path(~USD),
767 sendmax(XRP(10)),
768 domain(domainID));
769 env.close();
770
771 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
772 }
773
774 // sanity check: devin, who is part of the domain but doesn't have a
775 // trustline with USD issuer, can successfully make a payment using
776 // offer
777 {
778 Env env(*this, features);
779 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
780 PermissionedDEX(env);
781
782 env(offer(bob, XRP(10), USD(10)), domain(domainID));
783 env.close();
784
785 // fund devin but don't create a USD trustline with gateway
786 Account devin("devin");
787 env.fund(XRP(1000), devin);
788 env.close();
789
790 // domain owner also issues a credential for devin
791 env(credentials::create(devin, domainOwner, credType));
792 env.close();
793
794 env(credentials::accept(devin, domainOwner, credType));
795 env.close();
796
797 // successful payment because offer is consumed
798 env(pay(devin, alice, USD(10)), sendmax(XRP(10)), domain(domainID));
799 env.close();
800 }
801
802 // offer becomes unfunded when offer owner's cred expires
803 {
804 Env env(*this, features);
805 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
806 PermissionedDEX(env);
807
808 // create devin account who is not part of the domain
809 Account devin("devin");
810 env.fund(XRP(1000), devin);
811 env.close();
812 env.trust(USD(1000), devin);
813 env.close();
814 env(pay(gw, devin, USD(100)));
815 env.close();
816
817 auto jv = credentials::create(devin, domainOwner, credType);
818 uint32_t const t = env.current()
819 ->info()
820 .parentCloseTime.time_since_epoch()
821 .count();
822 jv[sfExpiration.jsonName] = t + 20;
823 env(jv);
824
825 env(credentials::accept(devin, domainOwner, credType));
826 env.close();
827
828 // devin can still create offer while his cred is not expired
829 auto const offerSeq{env.seq(devin)};
830 env(offer(devin, XRP(10), USD(10)), domain(domainID));
831 env.close();
832
833 // devin's offer can still be consumed while his cred isn't expired
834 env(pay(alice, carol, USD(5)),
835 path(~USD),
836 sendmax(XRP(5)),
837 domain(domainID));
838 env.close();
839 BEAST_EXPECT(
840 checkOffer(env, devin, offerSeq, XRP(5), USD(5), 0, true));
841
842 // advance time
844
845 // devin's offer is unfunded now due to expired cred
846 env(pay(alice, carol, USD(5)),
847 path(~USD),
848 sendmax(XRP(5)),
849 domain(domainID),
851 env.close();
852 BEAST_EXPECT(
853 checkOffer(env, devin, offerSeq, XRP(5), USD(5), 0, true));
854 }
855
856 // offer becomes unfunded when offer owner's cred is removed
857 {
858 Env env(*this, features);
859 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
860 PermissionedDEX(env);
861
862 auto const offerSeq{env.seq(bob)};
863 env(offer(bob, XRP(10), USD(10)), domain(domainID));
864 env.close();
865
866 // bob's offer can still be consumed while his cred exists
867 env(pay(alice, carol, USD(5)),
868 path(~USD),
869 sendmax(XRP(5)),
870 domain(domainID));
871 env.close();
872 BEAST_EXPECT(
873 checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true));
874
875 // remove bob's cred
877 domainOwner, bob, domainOwner, credType));
878 env.close();
879
880 // bob's offer is unfunded now due to expired cred
881 env(pay(alice, carol, USD(5)),
882 path(~USD),
883 sendmax(XRP(5)),
884 domain(domainID),
886 env.close();
887 BEAST_EXPECT(
888 checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true));
889 }
890 }
891
892 void
894 {
895 testcase("Rippling");
896
897 // test a non-domain account can still be part of rippling in a domain
898 // payment. If the domain wishes to control who is allowed to ripple
899 // through, they should set the rippling individually
900 Env env(*this, features);
901 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
902 PermissionedDEX(env);
903
904 auto const EURA = alice["EUR"];
905 auto const EURB = bob["EUR"];
906
907 env.trust(EURA(100), bob);
908 env.trust(EURB(100), carol);
909 env.close();
910
911 // remove bob from domain
912 env(credentials::deleteCred(domainOwner, bob, domainOwner, credType));
913 env.close();
914
915 // alice can still ripple through bob even though he's not part
916 // of the domain, this is intentional
917 env(pay(alice, carol, EURB(10)), paths(EURA), domain(domainID));
918 env.close();
919 env.require(balance(bob, EURA(10)), balance(carol, EURB(10)));
920
921 // carol sets no ripple on bob
922 env(trust(carol, bob["EUR"](0), bob, tfSetNoRipple));
923 env.close();
924
925 // payment no longer works because carol has no ripple on bob
926 env(pay(alice, carol, EURB(5)),
927 paths(EURA),
928 domain(domainID),
930 env.close();
931 env.require(balance(bob, EURA(10)), balance(carol, EURB(10)));
932 }
933
934 void
936 {
937 testcase("Offer token issuer in domain");
938
939 // whether the issuer is in the domain should NOT affect whether an
940 // offer can be consumed in domain payment
941 Env env(*this, features);
942 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
943 PermissionedDEX(env);
944
945 // create an xrp/usd offer with usd as takergets
946 auto const bobOffer1Seq{env.seq(bob)};
947 env(offer(bob, XRP(10), USD(10)), domain(domainID));
948 env.close();
949
950 // create an usd/xrp offer with usd as takerpays
951 auto const bobOffer2Seq{env.seq(bob)};
952 env(offer(bob, USD(10), XRP(10)), domain(domainID), txflags(tfPassive));
953 env.close();
954
955 BEAST_EXPECT(
956 checkOffer(env, bob, bobOffer1Seq, XRP(10), USD(10), 0, true));
957 BEAST_EXPECT(checkOffer(
958 env, bob, bobOffer2Seq, USD(10), XRP(10), lsfPassive, true));
959
960 // remove gateway from domain
961 env(credentials::deleteCred(domainOwner, gw, domainOwner, credType));
962 env.close();
963
964 // payment succeeds even if issuer is not in domain
965 // xrp/usd offer is consumed
966 env(pay(alice, carol, USD(10)),
967 path(~USD),
968 sendmax(XRP(10)),
969 domain(domainID));
970 env.close();
971 BEAST_EXPECT(!offerExists(env, bob, bobOffer1Seq));
972
973 // payment succeeds even if issuer is not in domain
974 // usd/xrp offer is consumed
975 env(pay(alice, carol, XRP(10)),
976 path(~XRP),
977 sendmax(USD(10)),
978 domain(domainID));
979 env.close();
980 BEAST_EXPECT(!offerExists(env, bob, bobOffer2Seq));
981 }
982
983 void
985 {
986 testcase("Remove unfunded offer");
987
988 // checking that an unfunded offer will be implictly removed by a
989 // successfuly payment tx
990 Env env(*this, features);
991 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
992 PermissionedDEX(env);
993
994 auto const aliceOfferSeq{env.seq(alice)};
995 env(offer(alice, XRP(100), USD(100)), domain(domainID));
996 env.close();
997
998 auto const bobOfferSeq{env.seq(bob)};
999 env(offer(bob, XRP(20), USD(20)), domain(domainID));
1000 env.close();
1001
1002 BEAST_EXPECT(
1003 checkOffer(env, bob, bobOfferSeq, XRP(20), USD(20), 0, true));
1004 BEAST_EXPECT(
1005 checkOffer(env, alice, aliceOfferSeq, XRP(100), USD(100), 0, true));
1006
1007 auto const domainDirKey = getDefaultOfferDirKey(env, bob, bobOfferSeq);
1008 BEAST_EXPECT(domainDirKey);
1009 BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 2));
1010
1011 // remove alice from domain and thus alice's offer becomes unfunded
1012 env(credentials::deleteCred(domainOwner, alice, domainOwner, credType));
1013 env.close();
1014
1015 env(pay(gw, carol, USD(10)),
1016 path(~USD),
1017 sendmax(XRP(10)),
1018 domain(domainID));
1019 env.close();
1020
1021 BEAST_EXPECT(
1022 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
1023
1024 // alice's unfunded offer is removed implicitly
1025 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
1026 BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 1));
1027 }
1028
1029 void
1031 {
1032 testcase("AMM not used");
1033
1034 Env env(*this, features);
1035 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1036 PermissionedDEX(env);
1037 AMM amm(env, alice, XRP(10), USD(50));
1038
1039 // a domain payment isn't able to consume AMM
1040 env(pay(bob, carol, USD(5)),
1041 path(~USD),
1042 sendmax(XRP(5)),
1043 domain(domainID),
1045 env.close();
1046
1047 // a non domain payment can use AMM
1048 env(pay(bob, carol, USD(5)), path(~USD), sendmax(XRP(5)));
1049 env.close();
1050
1051 // USD amount in AMM is changed
1052 auto [xrp, usd, lpt] = amm.balances(XRP, USD);
1053 BEAST_EXPECT(usd == USD(45));
1054 }
1055
1056 void
1058 {
1059 testcase("Hybrid offer create");
1060
1061 // test preflight - invalid hybrid flag
1062 {
1063 Env env(*this, features - featurePermissionedDEX);
1064 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1065 PermissionedDEX(env);
1066
1067 env(offer(bob, XRP(10), USD(10)),
1068 domain(domainID),
1070 ter(temDISABLED));
1071 env.close();
1072
1073 env(offer(bob, XRP(10), USD(10)),
1076 env.close();
1077
1078 env.enableFeature(featurePermissionedDEX);
1079 env.close();
1080
1081 // hybrid offer must have domainID
1082 env(offer(bob, XRP(10), USD(10)),
1085 env.close();
1086
1087 // hybrid offer must have domainID
1088 auto const offerSeq{env.seq(bob)};
1089 env(offer(bob, XRP(10), USD(10)),
1091 domain(domainID));
1092 env.close();
1093 BEAST_EXPECT(checkOffer(
1094 env, bob, offerSeq, XRP(10), USD(10), lsfHybrid, true));
1095 }
1096
1097 // apply - domain offer can cross with hybrid
1098 {
1099 Env env(*this, features);
1100 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1101 PermissionedDEX(env);
1102
1103 auto const bobOfferSeq{env.seq(bob)};
1104 env(offer(bob, XRP(10), USD(10)),
1106 domain(domainID));
1107 env.close();
1108
1109 BEAST_EXPECT(checkOffer(
1110 env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true));
1111 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1112 BEAST_EXPECT(ownerCount(env, bob) == 3);
1113
1114 auto const aliceOfferSeq{env.seq(alice)};
1115 env(offer(alice, USD(10), XRP(10)), domain(domainID));
1116 env.close();
1117
1118 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
1119 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
1120 BEAST_EXPECT(ownerCount(env, alice) == 2);
1121 }
1122
1123 // apply - open offer can cross with hybrid
1124 {
1125 Env env(*this, features);
1126 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1127 PermissionedDEX(env);
1128
1129 auto const bobOfferSeq{env.seq(bob)};
1130 env(offer(bob, XRP(10), USD(10)),
1132 domain(domainID));
1133 env.close();
1134
1135 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1136 BEAST_EXPECT(ownerCount(env, bob) == 3);
1137 BEAST_EXPECT(checkOffer(
1138 env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true));
1139
1140 auto const aliceOfferSeq{env.seq(alice)};
1141 env(offer(alice, USD(10), XRP(10)));
1142 env.close();
1143
1144 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
1145 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
1146 BEAST_EXPECT(ownerCount(env, alice) == 2);
1147 }
1148
1149 // apply - by default, hybrid offer tries to cross with offers in the
1150 // domain book
1151 {
1152 Env env(*this, features);
1153 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1154 PermissionedDEX(env);
1155
1156 auto const bobOfferSeq{env.seq(bob)};
1157 env(offer(bob, XRP(10), USD(10)), domain(domainID));
1158 env.close();
1159
1160 BEAST_EXPECT(
1161 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
1162 BEAST_EXPECT(ownerCount(env, bob) == 3);
1163
1164 // hybrid offer auto crosses with domain offer
1165 auto const aliceOfferSeq{env.seq(alice)};
1166 env(offer(alice, USD(10), XRP(10)),
1167 domain(domainID),
1168 txflags(tfHybrid));
1169 env.close();
1170
1171 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
1172 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
1173 BEAST_EXPECT(ownerCount(env, alice) == 2);
1174 }
1175
1176 // apply - hybrid offer does not automatically cross with open offers
1177 // because by default, it only tries to cross domain offers
1178 {
1179 Env env(*this, features);
1180 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1181 PermissionedDEX(env);
1182
1183 auto const bobOfferSeq{env.seq(bob)};
1184 env(offer(bob, XRP(10), USD(10)));
1185 env.close();
1186
1187 BEAST_EXPECT(
1188 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false));
1189 BEAST_EXPECT(ownerCount(env, bob) == 3);
1190
1191 // hybrid offer auto crosses with domain offer
1192 auto const aliceOfferSeq{env.seq(alice)};
1193 env(offer(alice, USD(10), XRP(10)),
1194 domain(domainID),
1195 txflags(tfHybrid));
1196 env.close();
1197
1198 BEAST_EXPECT(offerExists(env, alice, aliceOfferSeq));
1199 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1200 BEAST_EXPECT(
1201 checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false));
1202 BEAST_EXPECT(checkOffer(
1203 env, alice, aliceOfferSeq, USD(10), XRP(10), lsfHybrid, true));
1204 BEAST_EXPECT(ownerCount(env, alice) == 3);
1205 }
1206 }
1207
1208 void
1210 {
1211 testcase("Hybrid invalid offer");
1212
1213 // bob has a hybrid offer and then he is removed from domain.
1214 // in this case, the hybrid offer will be considered as unfunded even in
1215 // a regular payment
1216 Env env(*this, features);
1217 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1218 PermissionedDEX(env);
1219
1220 auto const hybridOfferSeq{env.seq(bob)};
1221 env(offer(bob, XRP(50), USD(50)), txflags(tfHybrid), domain(domainID));
1222 env.close();
1223
1224 // remove bob from domain
1225 env(credentials::deleteCred(domainOwner, bob, domainOwner, credType));
1226 env.close();
1227
1228 // bob's hybrid offer is unfunded and can not be consumed in a domain
1229 // payment
1230 env(pay(alice, carol, USD(5)),
1231 path(~USD),
1232 sendmax(XRP(5)),
1233 domain(domainID),
1235 env.close();
1236 BEAST_EXPECT(checkOffer(
1237 env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true));
1238
1239 // bob's unfunded hybrid offer can't be consumed even with a regular
1240 // payment
1241 env(pay(alice, carol, USD(5)),
1242 path(~USD),
1243 sendmax(XRP(5)),
1245 env.close();
1246 BEAST_EXPECT(checkOffer(
1247 env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true));
1248
1249 // create a regular offer
1250 auto const regularOfferSeq{env.seq(bob)};
1251 env(offer(bob, XRP(10), USD(10)));
1252 env.close();
1253 BEAST_EXPECT(offerExists(env, bob, regularOfferSeq));
1254 BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10)));
1255
1256 auto const sleHybridOffer =
1257 env.le(keylet::offer(bob.id(), hybridOfferSeq));
1258 BEAST_EXPECT(sleHybridOffer);
1259 auto const openDir =
1260 sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(
1261 sfBookDirectory);
1262 BEAST_EXPECT(checkDirectorySize(env, openDir, 2));
1263
1264 // this normal payment should consume the regular offer and remove the
1265 // unfunded hybrid offer
1266 env(pay(alice, carol, USD(5)), path(~USD), sendmax(XRP(5)));
1267 env.close();
1268
1269 BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq));
1270 BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(5), USD(5)));
1271 BEAST_EXPECT(checkDirectorySize(env, openDir, 1));
1272 }
1273
1274 void
1276 {
1277 testcase("Hybrid book step");
1278
1279 // both non domain and domain payments can consume hybrid offer
1280 {
1281 Env env(*this, features);
1282 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1283 PermissionedDEX(env);
1284
1285 auto const hybridOfferSeq{env.seq(bob)};
1286 env(offer(bob, XRP(10), USD(10)),
1288 domain(domainID));
1289 env.close();
1290
1291 env(pay(alice, carol, USD(5)),
1292 path(~USD),
1293 sendmax(XRP(5)),
1294 domain(domainID));
1295 env.close();
1296 BEAST_EXPECT(checkOffer(
1297 env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true));
1298
1299 // hybrid offer can't be consumed since bob is not in domain anymore
1300 env(pay(alice, carol, USD(5)), path(~USD), sendmax(XRP(5)));
1301 env.close();
1302
1303 BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq));
1304 }
1305
1306 // someone from another domain can't cross hybrid if they specified
1307 // wrong domainID
1308 {
1309 Env env(*this, features);
1310 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1311 PermissionedDEX(env);
1312
1313 // Fund accounts
1314 Account badDomainOwner("badDomainOwner");
1315 Account devin("devin");
1316 env.fund(XRP(1000), badDomainOwner, devin);
1317 env.close();
1318
1319 auto const badCredType = "badCred";
1320 pdomain::Credentials credentials{{badDomainOwner, badCredType}};
1321 env(pdomain::setTx(badDomainOwner, credentials));
1322
1323 auto objects = pdomain::getObjects(badDomainOwner, env);
1324 auto const badDomainID = objects.begin()->first;
1325
1326 env(credentials::create(devin, badDomainOwner, badCredType));
1327 env.close();
1328 env(credentials::accept(devin, badDomainOwner, badCredType));
1329 env.close();
1330
1331 auto const hybridOfferSeq{env.seq(bob)};
1332 env(offer(bob, XRP(10), USD(10)),
1334 domain(domainID));
1335 env.close();
1336
1337 // other domains can't consume the offer
1338 env(pay(devin, badDomainOwner, USD(5)),
1339 path(~USD),
1340 sendmax(XRP(5)),
1341 domain(badDomainID),
1342 ter(tecPATH_DRY));
1343 env.close();
1344 BEAST_EXPECT(checkOffer(
1345 env, bob, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true));
1346
1347 env(pay(alice, carol, USD(5)),
1348 path(~USD),
1349 sendmax(XRP(5)),
1350 domain(domainID));
1351 env.close();
1352 BEAST_EXPECT(checkOffer(
1353 env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true));
1354
1355 // hybrid offer can't be consumed since bob is not in domain anymore
1356 env(pay(alice, carol, USD(5)), path(~USD), sendmax(XRP(5)));
1357 env.close();
1358
1359 BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq));
1360 }
1361
1362 // test domain payment consuming two offers w/ hybrid offer
1363 {
1364 Env env(*this, features);
1365 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1366 PermissionedDEX(env);
1367
1368 auto const EUR = gw["EUR"];
1369 env.trust(EUR(1000), alice);
1370 env.close();
1371 env.trust(EUR(1000), bob);
1372 env.close();
1373 env.trust(EUR(1000), carol);
1374 env.close();
1375 env(pay(gw, bob, EUR(100)));
1376 env.close();
1377
1378 auto const usdOfferSeq{env.seq(bob)};
1379 env(offer(bob, XRP(10), USD(10)), domain(domainID));
1380 env.close();
1381
1382 BEAST_EXPECT(
1383 checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true));
1384
1385 // payment fail because there isn't eur offer
1386 env(pay(alice, carol, EUR(5)),
1387 path(~USD, ~EUR),
1388 sendmax(XRP(5)),
1389 domain(domainID),
1391 env.close();
1392 BEAST_EXPECT(
1393 checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true));
1394
1395 // bob creates a hybrid eur offer
1396 auto const eurOfferSeq{env.seq(bob)};
1397 env(offer(bob, USD(10), EUR(10)),
1398 domain(domainID),
1399 txflags(tfHybrid));
1400 env.close();
1401 BEAST_EXPECT(checkOffer(
1402 env, bob, eurOfferSeq, USD(10), EUR(10), lsfHybrid, true));
1403
1404 // alice successfully consume two domain offers: xrp/usd and usd/eur
1405 env(pay(alice, carol, EUR(5)),
1406 path(~USD, ~EUR),
1407 sendmax(XRP(5)),
1408 domain(domainID));
1409 env.close();
1410
1411 BEAST_EXPECT(
1412 checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true));
1413 BEAST_EXPECT(checkOffer(
1414 env, bob, eurOfferSeq, USD(5), EUR(5), lsfHybrid, true));
1415 }
1416
1417 // test regular payment using a regular offer and a hybrid offer
1418 {
1419 Env env(*this, features);
1420 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1421 PermissionedDEX(env);
1422
1423 auto const EUR = gw["EUR"];
1424 env.trust(EUR(1000), alice);
1425 env.close();
1426 env.trust(EUR(1000), bob);
1427 env.close();
1428 env.trust(EUR(1000), carol);
1429 env.close();
1430 env(pay(gw, bob, EUR(100)));
1431 env.close();
1432
1433 // bob creates a regular usd offer
1434 auto const usdOfferSeq{env.seq(bob)};
1435 env(offer(bob, XRP(10), USD(10)));
1436 env.close();
1437
1438 BEAST_EXPECT(
1439 checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, false));
1440
1441 // bob creates a hybrid eur offer
1442 auto const eurOfferSeq{env.seq(bob)};
1443 env(offer(bob, USD(10), EUR(10)),
1444 domain(domainID),
1445 txflags(tfHybrid));
1446 env.close();
1447 BEAST_EXPECT(checkOffer(
1448 env, bob, eurOfferSeq, USD(10), EUR(10), lsfHybrid, true));
1449
1450 // alice successfully consume two offers: xrp/usd and usd/eur
1451 env(pay(alice, carol, EUR(5)), path(~USD, ~EUR), sendmax(XRP(5)));
1452 env.close();
1453
1454 BEAST_EXPECT(
1455 checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, false));
1456 BEAST_EXPECT(checkOffer(
1457 env, bob, eurOfferSeq, USD(5), EUR(5), lsfHybrid, true));
1458 }
1459 }
1460
1461 void
1463 {
1464 Env env(*this, features);
1465 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1466 PermissionedDEX(env);
1467
1469 offerSeqs.reserve(100);
1470
1471 Book domainBook{Issue(XRP), Issue(USD), domainID};
1472 Book openBook{Issue(XRP), Issue(USD), std::nullopt};
1473
1474 auto const domainDir = getBookDirKey(domainBook, XRP(10), USD(10));
1475 auto const openDir = getBookDirKey(openBook, XRP(10), USD(10));
1476
1477 size_t dirCnt = 100;
1478
1479 for (size_t i = 1; i <= dirCnt; i++)
1480 {
1481 auto const bobOfferSeq{env.seq(bob)};
1482 offerSeqs.emplace_back(bobOfferSeq);
1483 env(offer(bob, XRP(10), USD(10)),
1485 domain(domainID));
1486 env.close();
1487
1488 auto const sleOffer = env.le(keylet::offer(bob.id(), bobOfferSeq));
1489 BEAST_EXPECT(sleOffer);
1490 BEAST_EXPECT(sleOffer->getFieldH256(sfBookDirectory) == domainDir);
1491 BEAST_EXPECT(
1492 sleOffer->getFieldArray(sfAdditionalBooks).size() == 1);
1493 BEAST_EXPECT(
1494 sleOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(
1495 sfBookDirectory) == openDir);
1496
1497 BEAST_EXPECT(checkOffer(
1498 env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true));
1499 BEAST_EXPECT(checkDirectorySize(env, domainDir, i));
1500 BEAST_EXPECT(checkDirectorySize(env, openDir, i));
1501 }
1502
1503 for (auto const offerSeq : offerSeqs)
1504 {
1505 env(offer_cancel(bob, offerSeq));
1506 env.close();
1507 dirCnt--;
1508 BEAST_EXPECT(!offerExists(env, bob, offerSeq));
1509 BEAST_EXPECT(checkDirectorySize(env, domainDir, dirCnt));
1510 BEAST_EXPECT(checkDirectorySize(env, openDir, dirCnt));
1511 }
1512 }
1513
1514 void
1516 {
1517 testcase("Auto bridge");
1518
1519 Env env(*this, features);
1520 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1521 PermissionedDEX(env);
1522 auto const EUR = gw["EUR"];
1523
1524 for (auto const& account : {alice, bob, carol})
1525 {
1526 env(trust(account, EUR(10000)));
1527 env.close();
1528 }
1529
1530 env(pay(gw, carol, EUR(1)));
1531 env.close();
1532
1533 auto const aliceOfferSeq{env.seq(alice)};
1534 auto const bobOfferSeq{env.seq(bob)};
1535 env(offer(alice, XRP(100), USD(1)), domain(domainID));
1536 env(offer(bob, EUR(1), XRP(100)), domain(domainID));
1537 env.close();
1538
1539 // carol's offer should cross bob and alice's offers due to auto
1540 // bridging
1541 auto const carolOfferSeq{env.seq(carol)};
1542 env(offer(carol, USD(1), EUR(1)), domain(domainID));
1543 env.close();
1544
1545 BEAST_EXPECT(!offerExists(env, bob, aliceOfferSeq));
1546 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
1547 BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq));
1548 }
1549
1550public:
1551 void
1552 run() override
1553 {
1555
1556 // Test domain offer (w/o hyrbid)
1565
1566 // Test hybrid offers
1571 }
1572};
1573
1574BEAST_DEFINE_TESTSUITE(PermissionedDEX, app, ripple);
1575
1576} // namespace test
1577} // 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 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:268
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:547
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:121
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:320
void enableFeature(uint256 const feature)
Definition Env.cpp:660
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:289
std::shared_ptr< SLE const > le(Account const &account) const
Return an account root.
Definition Env.cpp:277
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)
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: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)
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
FeatureBitset testable_amendments()
Definition Env.h:74
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:111
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