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