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