xrpld
Loading...
Searching...
No Matches
Path_test.cpp
1#include <test/jtx/AMM.h>
2#include <test/jtx/AMMTest.h>
3#include <test/jtx/Account.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/TestHelpers.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/balance.h>
8#include <test/jtx/domain.h>
9#include <test/jtx/envconfig.h>
10#include <test/jtx/jtx_json.h>
11#include <test/jtx/offer.h>
12#include <test/jtx/owners.h> // IWYU pragma: keep
13#include <test/jtx/paths.h>
14#include <test/jtx/pay.h>
15#include <test/jtx/permissioned_dex.h>
16#include <test/jtx/rate.h>
17#include <test/jtx/sendmax.h>
18#include <test/jtx/ter.h>
19#include <test/jtx/trust.h>
20#include <test/jtx/txflags.h>
21
22#include <xrpld/core/Config.h>
23#include <xrpld/rpc/RPCHandler.h>
24#include <xrpld/rpc/Role.h>
25#include <xrpld/rpc/detail/Tuning.h>
26
27#include <xrpl/basics/base_uint.h>
28#include <xrpl/basics/hardened_hash.h>
29#include <xrpl/beast/unit_test/suite.h>
30#include <xrpl/core/Job.h>
31#include <xrpl/core/JobQueue.h>
32#include <xrpl/json/json_reader.h>
33#include <xrpl/json/json_value.h>
34#include <xrpl/protocol/AccountID.h>
35#include <xrpl/protocol/ApiVersion.h>
36#include <xrpl/protocol/Indexes.h>
37#include <xrpl/protocol/Issue.h>
38#include <xrpl/protocol/SField.h>
39#include <xrpl/protocol/STAmount.h>
40#include <xrpl/protocol/STParsedJSON.h>
41#include <xrpl/protocol/STPathSet.h>
42#include <xrpl/protocol/TER.h>
43#include <xrpl/protocol/TxFlags.h>
44#include <xrpl/protocol/UintTypes.h>
45#include <xrpl/protocol/jss.h>
46#include <xrpl/resource/Charge.h>
47#include <xrpl/resource/Consumer.h>
48#include <xrpl/resource/Fees.h>
49
50#include <algorithm>
51#include <chrono>
52#include <condition_variable>
53#include <cstddef>
54#include <cstdint>
55#include <memory>
56#include <mutex>
57#include <optional>
58#include <set>
59#include <string>
60#include <string_view>
61#include <tuple>
62#include <utility>
63#include <vector>
64
65namespace xrpl::test {
66
67//------------------------------------------------------------------------------
68
69json::Value
70rpf(jtx::Account const& src, jtx::Account const& dst, std::uint32_t numSrc)
71{
73 jv[jss::command] = "ripple_path_find";
74 jv[jss::source_account] = toBase58(src);
75
76 if (numSrc > 0)
77 {
78 auto& sc = (jv[jss::source_currencies] = json::ValueType::Array);
80 while ((numSrc--) != 0u)
81 {
82 j[jss::currency] = std::to_string(numSrc + 100);
83 sc.append(j);
84 }
85 }
86
87 auto const d = toBase58(dst);
88 jv[jss::destination_account] = d;
89
90 json::Value& j = (jv[jss::destination_amount] = json::ValueType::Object);
91 j[jss::currency] = "USD";
92 j[jss::value] = "0.01";
93 j[jss::issuer] = d;
94
95 return jv;
96}
97
98//------------------------------------------------------------------------------
99
101{
104 {
105 // These tests were originally written with search parameters that are
106 // different from the current defaults. This function creates an env
107 // with the search parameters that the tests were written for.
108 using namespace jtx;
109 return Env(*this, envconfig([](std::unique_ptr<Config> cfg) {
110 cfg->pathSearchOld = 7;
111 cfg->pathSearch = 7;
112 cfg->pathSearchMax = 10;
113 return cfg;
114 }));
115 }
116
117public:
118 class Gate
119 {
120 private:
123 bool signaled_ = false;
124
125 public:
126 // Thread safe, blocks until signaled or period expires.
127 // Returns `true` if signaled.
128 template <class Rep, class Period>
129 bool
131 {
133 auto b = cv_.wait_for(lk, relTime, [this] { return signaled_; });
134 signaled_ = false;
135 return b;
136 }
137
138 void
140 {
141 std::scoped_lock const lk(mutex_);
142 signaled_ = true;
143 cv_.notify_all();
144 }
145 };
146
147 auto
149 jtx::Env& env,
150 jtx::Account const& src,
151 jtx::Account const& dst,
152 STAmount const& saDstAmount,
153 std::optional<STAmount> const& saSendMax = std::nullopt,
154 std::optional<Currency> const& saSrcCurrency = std::nullopt,
155 std::optional<UInt256> const& domain = std::nullopt,
156 std::optional<AccountID> const& saSrcIssuer = std::nullopt)
157 {
158 using namespace jtx;
159
160 auto& app = env.app();
163
164 rpc::JsonContext context{
165 {.j = env.journal,
166 .app = app,
167 .loadType = loadType,
168 .netOps = app.getOPs(),
169 .ledgerMaster = app.getLedgerMaster(),
170 .consumer = c,
171 .role = Role::USER,
172 .coro = {},
173 .infoSub = {},
174 .apiVersion = rpc::kApiVersionIfUnspecified},
175 {},
176 {}};
177
179 params[jss::command] = "ripple_path_find";
180 params[jss::source_account] = toBase58(src);
181 params[jss::destination_account] = toBase58(dst);
182 params[jss::destination_amount] = saDstAmount.getJson(JsonOptions::Values::None);
183 if (saSendMax)
184 params[jss::send_max] = saSendMax->getJson(JsonOptions::Values::None);
185 if (saSrcCurrency)
186 {
187 auto& sc = params[jss::source_currencies] = json::ValueType::Array;
189 j[jss::currency] = to_string(saSrcCurrency.value());
190 // Optional issuer for tests that need to exercise
191 // source_currencies entries more precisely than currency alone.
192 if (saSrcIssuer)
193 j[jss::issuer] = toBase58(*saSrcIssuer);
194 sc.append(j);
195 }
196 if (domain)
197 params[jss::domain] = to_string(*domain);
198
199 json::Value result;
200 Gate g;
201 app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
202 context.params = std::move(params);
203 context.coro = coro;
204 rpc::doCommand(context, result);
205 g.signal();
206 });
207
208 using namespace std::chrono_literals;
209 BEAST_EXPECT(g.waitFor(5s));
210 BEAST_EXPECT(!result.isMember(jss::error));
211 return result;
212 }
213
216 jtx::Env& env,
217 jtx::Account const& src,
218 jtx::Account const& dst,
219 STAmount const& saDstAmount,
220 std::optional<STAmount> const& saSendMax = std::nullopt,
221 std::optional<Currency> const& saSrcCurrency = std::nullopt,
222 std::optional<UInt256> const& domain = std::nullopt,
223 std::optional<AccountID> const& saSrcIssuer = std::nullopt)
224 {
226 env, src, dst, saDstAmount, saSendMax, saSrcCurrency, domain, saSrcIssuer);
227 BEAST_EXPECT(!result.isMember(jss::error));
228
229 STAmount da;
230 if (result.isMember(jss::destination_amount))
231 da = amountFromJson(sfGeneric, result[jss::destination_amount]);
232
233 STAmount sa;
234 STPathSet paths;
235 if (result.isMember(jss::alternatives))
236 {
237 auto const& alts = result[jss::alternatives];
238 if (alts.size() > 0)
239 {
240 auto const& path = alts[0u];
241
242 if (path.isMember(jss::source_amount))
243 sa = amountFromJson(sfGeneric, path[jss::source_amount]);
244
245 if (path.isMember(jss::destination_amount))
246 da = amountFromJson(sfGeneric, path[jss::destination_amount]);
247
248 if (path.isMember(jss::paths_computed))
249 {
250 json::Value p;
251 p["Paths"] = path[jss::paths_computed];
252 STParsedJSONObject po("generic", p);
253
254 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
255 paths = po.object->getFieldPathSet(sfPaths);
256 }
257 }
258 }
259
260 return std::make_tuple(std::move(paths), std::move(sa), std::move(da));
261 }
262
263 void
265 {
266 testcase("source currency limits");
267 using namespace std::chrono_literals;
268 using namespace jtx;
269 Env env = pathTestEnv();
270 auto const gw = Account("gateway");
271 env.fund(XRP(10000), "alice", "bob", gw);
272 env.close();
273 env.trust(gw["USD"](100), "alice", "bob");
274 env.close();
275
276 auto& app = env.app();
279
280 rpc::JsonContext context{
281 {.j = env.journal,
282 .app = app,
283 .loadType = loadType,
284 .netOps = app.getOPs(),
285 .ledgerMaster = app.getLedgerMaster(),
286 .consumer = c,
287 .role = Role::USER,
288 .coro = {},
289 .infoSub = {},
290 .apiVersion = rpc::kApiVersionIfUnspecified},
291 {},
292 {}};
293 json::Value result;
294 Gate g;
295 // Test rpc::tuning::max_src_cur source currencies.
296 app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
297 context.params = rpf(Account("alice"), Account("bob"), rpc::tuning::kMaxSrcCur);
298 context.coro = coro;
299 rpc::doCommand(context, result);
300 g.signal();
301 });
302 BEAST_EXPECT(g.waitFor(5s));
303 BEAST_EXPECT(!result.isMember(jss::error));
304
305 // Test more than rpc::tuning::max_src_cur source currencies.
306 app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
307 context.params = rpf(Account("alice"), Account("bob"), rpc::tuning::kMaxSrcCur + 1);
308 context.coro = coro;
309 rpc::doCommand(context, result);
310 g.signal();
311 });
312 BEAST_EXPECT(g.waitFor(5s));
313 BEAST_EXPECT(result.isMember(jss::error));
314
315 // Test rpc::tuning::max_auto_src_cur source currencies.
316 for (auto i = 0; i < (rpc::tuning::kMaxAutoSrcCur - 1); ++i)
317 env.trust(Account("alice")[std::to_string(i + 100)](100), "bob");
318 app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
319 context.params = rpf(Account("alice"), Account("bob"), 0);
320 context.coro = coro;
321 rpc::doCommand(context, result);
322 g.signal();
323 });
324 BEAST_EXPECT(g.waitFor(5s));
325 BEAST_EXPECT(!result.isMember(jss::error));
326
327 // Test more than rpc::tuning::max_auto_src_cur source currencies.
328 env.trust(Account("alice")["AUD"](100), "bob");
329 app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
330 context.params = rpf(Account("alice"), Account("bob"), 0);
331 context.coro = coro;
332 rpc::doCommand(context, result);
333 g.signal();
334 });
335 BEAST_EXPECT(g.waitFor(5s));
336 BEAST_EXPECT(result.isMember(jss::error));
337 }
338
339 void
341 {
342 testcase("source currency issuer selection");
343 using namespace jtx;
344
345 Env env = pathTestEnv();
346 auto const alice = Account("alice");
347 auto const bob = Account("bob");
348 auto const gateway = Account("gateway");
349
350 env.fund(XRP(10000), alice, bob, gateway);
351 env.close();
352
353 auto const usd = gateway["USD"];
354 env.trust(usd(600), alice);
355 env.trust(usd(700), bob);
356 env.trust(alice["USD"](700), bob);
357 env(pay(gateway, alice, usd(70)));
358 env(pay(gateway, bob, usd(50)));
359 env.close();
360
361 // Ask for USD from an explicit source issuer while send_max is
362 // Alice-issued USD. The parser should choose gateway-issued USD
363 // because gateway is the issuer in source_currencies.
364 //
365 // The Alice/Bob trust line is intentional: if Alice-issued USD is also
366 // considered as a source asset, pathfinding can produce an additional
367 // alternative. The single expected alternative below verifies that only
368 // the explicit issuer is selected.
369 auto const result = findPathsRequest(
370 env,
371 alice,
372 bob,
373 bob["USD"](-1),
374 alice["USD"](100).value(),
375 usd.currency,
376 std::nullopt,
377 gateway.id());
378 auto const& alternatives = result[jss::alternatives];
379 BEAST_EXPECT(alternatives.size() == 1);
380 auto const sa = amountFromJson(sfGeneric, alternatives[0u][jss::source_amount]);
381 auto const da = amountFromJson(sfGeneric, alternatives[0u][jss::destination_amount]);
382 BEAST_EXPECTS(equal(sa, usd(100)), sa.getFullText());
383 BEAST_EXPECTS(equal(da, bob["USD"](100)), da.getFullText());
384 }
385
386 void
388 {
389 testcase("no direct path no intermediary no alternatives");
390 using namespace jtx;
391 Env env = pathTestEnv();
392 env.fund(XRP(10000), "alice", "bob");
393 env.close();
394
395 auto const result = findPaths(env, "alice", "bob", Account("bob")["USD"](5));
396 BEAST_EXPECT(std::get<0>(result).empty());
397 }
398
399 void
401 {
402 testcase("direct path no intermediary");
403 using namespace jtx;
404 Env env = pathTestEnv();
405 env.fund(XRP(10000), "alice", "bob");
406 env.close();
407 env.trust(Account("alice")["USD"](700), "bob");
408
409 STPathSet st;
410 STAmount sa;
411 std::tie(st, sa, std::ignore) = findPaths(env, "alice", "bob", Account("bob")["USD"](5));
412 BEAST_EXPECT(st.empty());
413 BEAST_EXPECT(equal(sa, Account("alice")["USD"](5)));
414 }
415
416 void
418 {
419 testcase("payment auto path find");
420 using namespace jtx;
421 Env env = pathTestEnv();
422 auto const gw = Account("gateway");
423 auto const usd = gw["USD"];
424 env.fund(XRP(10000), "alice", "bob", gw);
425 env.close();
426 env.trust(usd(600), "alice");
427 env.trust(usd(700), "bob");
428 env(pay(gw, "alice", usd(70)));
429 env(pay("alice", "bob", usd(24)));
430 env.require(Balance("alice", usd(46)));
431 env.require(Balance(gw, Account("alice")["USD"](-46)));
432 env.require(Balance("bob", usd(24)));
433 env.require(Balance(gw, Account("bob")["USD"](-24)));
434 }
435
436 void
437 pathFind(bool const domainEnabled)
438 {
439 testcase(std::string("path find") + (domainEnabled ? " w/ " : " w/o ") + "domain");
440 using namespace jtx;
441 Env env = pathTestEnv();
442 auto const gw = Account("gateway");
443 auto const usd = gw["USD"];
444 env.fund(XRP(10000), "alice", "bob", gw);
445 env.close();
446 env.trust(usd(600), "alice");
447 env.trust(usd(700), "bob");
448 env(pay(gw, "alice", usd(70)));
449 env(pay(gw, "bob", usd(50)));
450
451 std::optional<UInt256> domainID;
452 if (domainEnabled)
453 domainID = setupDomain(env, {"alice", "bob", gw});
454
455 STPathSet st;
456 STAmount sa;
457 std::tie(st, sa, std::ignore) = findPaths(
458 env, "alice", "bob", Account("bob")["USD"](5), std::nullopt, std::nullopt, domainID);
459 BEAST_EXPECT(same(st, stpath("gateway")));
460 BEAST_EXPECT(equal(sa, Account("alice")["USD"](5)));
461 }
462
463 void
464 xrpToXrp(bool const domainEnabled)
465 {
466 using namespace jtx;
467 testcase(std::string("XRP to XRP") + (domainEnabled ? " w/ " : " w/o ") + "domain");
468 Env env = pathTestEnv();
469 env.fund(XRP(10000), "alice", "bob");
470 env.close();
471
472 std::optional<UInt256> domainID;
473 if (domainEnabled)
474 domainID = setupDomain(env, {"alice", "bob"});
475
476 auto const result =
477 findPaths(env, "alice", "bob", XRP(5), std::nullopt, std::nullopt, domainID);
478 BEAST_EXPECT(std::get<0>(result).empty());
479 }
480
481 void
482 pathFindConsumeAll(bool const domainEnabled)
483 {
484 testcase(
485 std::string("path find consume all") + (domainEnabled ? " w/ " : " w/o ") + "domain");
486 using namespace jtx;
487
488 {
489 Env env = pathTestEnv();
490 env.fund(XRP(10000), "alice", "bob", "carol", "dan", "edward");
491 env.close();
492 env.trust(Account("alice")["USD"](10), "bob");
493 env.trust(Account("bob")["USD"](10), "carol");
494 env.trust(Account("carol")["USD"](10), "edward");
495 env.trust(Account("alice")["USD"](100), "dan");
496 env.trust(Account("dan")["USD"](100), "edward");
497
498 std::optional<UInt256> domainID;
499 if (domainEnabled)
500 domainID = setupDomain(env, {"alice", "bob", "carol", "dan", "edward"});
501
502 STPathSet st;
503 STAmount sa;
504 STAmount da;
505 std::tie(st, sa, da) = findPaths(
506 env,
507 "alice",
508 "edward",
509 Account("edward")["USD"](-1),
510 std::nullopt,
511 std::nullopt,
512 domainID);
513 BEAST_EXPECT(same(st, stpath("dan"), stpath("bob", "carol")));
514 BEAST_EXPECT(equal(sa, Account("alice")["USD"](110)));
515 BEAST_EXPECT(equal(da, Account("edward")["USD"](110)));
516 }
517
518 {
519 Env env = pathTestEnv();
520 auto const gw = Account("gateway");
521 auto const usd = gw["USD"];
522 env.fund(XRP(10000), "alice", "bob", "carol", gw);
523 env.close();
524 env.trust(usd(100), "bob", "carol");
525 env.close();
526 env(pay(gw, "carol", usd(100)));
527 env.close();
528
529 std::optional<UInt256> domainID;
530 if (domainEnabled)
531 {
532 domainID = setupDomain(env, {"alice", "bob", "carol", "gateway"});
533 env(offer("carol", XRP(100), usd(100)), Domain(*domainID));
534 }
535 else
536 {
537 env(offer("carol", XRP(100), usd(100)));
538 }
539 env.close();
540
541 STPathSet st;
542 STAmount sa;
543 STAmount da;
544 std::tie(st, sa, da) = findPaths(
545 env,
546 "alice",
547 "bob",
548 Account("bob")["AUD"](-1),
550 std::nullopt,
551 domainID);
552 BEAST_EXPECT(st.empty());
553 std::tie(st, sa, da) = findPaths(
554 env,
555 "alice",
556 "bob",
557 Account("bob")["USD"](-1),
559 std::nullopt,
560 domainID);
561 BEAST_EXPECT(sa == XRP(100));
562 BEAST_EXPECT(equal(da, Account("bob")["USD"](100)));
563
564 // if domain is used, finding path in the open offerbook will return
565 // empty result
566 if (domainEnabled)
567 {
568 std::tie(st, sa, da) = findPaths(
569 env,
570 "alice",
571 "bob",
572 Account("bob")["USD"](-1),
574 std::nullopt,
575 std::nullopt); // not specifying a domain
576 BEAST_EXPECT(st.empty());
577 }
578 }
579 }
580
581 void
582 alternativePathConsumeBoth(bool const domainEnabled)
583 {
584 testcase(
585 std::string("alternative path consume both") + (domainEnabled ? " w/ " : " w/o ") +
586 "domain");
587 using namespace jtx;
588 Env env = pathTestEnv();
589 auto const gw = Account("gateway");
590 auto const usd = gw["USD"];
591 auto const gw2 = Account("gateway2");
592 auto const gw2Usd = gw2["USD"];
593 env.fund(XRP(10000), "alice", "bob", gw, gw2);
594 env.close();
595 env.trust(usd(600), "alice");
596 env.trust(gw2Usd(800), "alice");
597 env.trust(usd(700), "bob");
598 env.trust(gw2Usd(900), "bob");
599
600 std::optional<UInt256> domainID;
601 if (domainEnabled)
602 {
603 domainID = setupDomain(env, {"alice", "bob", "gateway", "gateway2"});
604 env(pay(gw, "alice", usd(70)), Domain(*domainID));
605 env(pay(gw2, "alice", gw2Usd(70)), Domain(*domainID));
606 env(pay("alice", "bob", Account("bob")["USD"](140)),
607 Paths(Account("alice")["USD"]),
608 Domain(*domainID));
609 }
610 else
611 {
612 env(pay(gw, "alice", usd(70)));
613 env(pay(gw2, "alice", gw2Usd(70)));
614 env(pay("alice", "bob", Account("bob")["USD"](140)), Paths(Account("alice")["USD"]));
615 }
616
617 env.require(Balance("alice", usd(0)));
618 env.require(Balance("alice", gw2Usd(0)));
619 env.require(Balance("bob", usd(70)));
620 env.require(Balance("bob", gw2Usd(70)));
621 env.require(Balance(gw, Account("alice")["USD"](0)));
622 env.require(Balance(gw, Account("bob")["USD"](-70)));
623 env.require(Balance(gw2, Account("alice")["USD"](0)));
624 env.require(Balance(gw2, Account("bob")["USD"](-70)));
625 }
626
627 void
628 alternativePathsConsumeBestTransfer(bool const domainEnabled)
629 {
630 testcase(
631 std::string("alternative paths consume best transfer") +
632 (domainEnabled ? " w/ " : " w/o ") + "domain");
633 using namespace jtx;
634 Env env = pathTestEnv();
635 auto const gw = Account("gateway");
636 auto const usd = gw["USD"];
637 auto const gw2 = Account("gateway2");
638 auto const gw2Usd = gw2["USD"];
639 env.fund(XRP(10000), "alice", "bob", gw, gw2);
640 env.close();
641 env(rate(gw2, 1.1));
642 env.trust(usd(600), "alice");
643 env.trust(gw2Usd(800), "alice");
644 env.trust(usd(700), "bob");
645 env.trust(gw2Usd(900), "bob");
646
647 std::optional<UInt256> domainID;
648 if (domainEnabled)
649 {
650 domainID = setupDomain(env, {"alice", "bob", "gateway", "gateway2"});
651 env(pay(gw, "alice", usd(70)), Domain(*domainID));
652 env(pay(gw2, "alice", gw2Usd(70)), Domain(*domainID));
653 env(pay("alice", "bob", usd(70)), Domain(*domainID));
654 }
655 else
656 {
657 env(pay(gw, "alice", usd(70)));
658 env(pay(gw2, "alice", gw2Usd(70)));
659 env(pay("alice", "bob", usd(70)));
660 }
661 env.require(Balance("alice", usd(0)));
662 env.require(Balance("alice", gw2Usd(70)));
663 env.require(Balance("bob", usd(70)));
664 env.require(Balance("bob", gw2Usd(0)));
665 env.require(Balance(gw, Account("alice")["USD"](0)));
666 env.require(Balance(gw, Account("bob")["USD"](-70)));
667 env.require(Balance(gw2, Account("alice")["USD"](-70)));
668 env.require(Balance(gw2, Account("bob")["USD"](0)));
669 }
670
671 void
673 {
674 testcase("alternative paths - consume best transfer first");
675 using namespace jtx;
676 Env env = pathTestEnv();
677 auto const gw = Account("gateway");
678 auto const usd = gw["USD"];
679 auto const gw2 = Account("gateway2");
680 auto const gw2Usd = gw2["USD"];
681 env.fund(XRP(10000), "alice", "bob", gw, gw2);
682 env.close();
683 env(rate(gw2, 1.1));
684 env.trust(usd(600), "alice");
685 env.trust(gw2Usd(800), "alice");
686 env.trust(usd(700), "bob");
687 env.trust(gw2Usd(900), "bob");
688 env(pay(gw, "alice", usd(70)));
689 env(pay(gw2, "alice", gw2Usd(70)));
690 env(pay("alice", "bob", Account("bob")["USD"](77)),
691 Sendmax(Account("alice")["USD"](100)),
692 Paths(Account("alice")["USD"]));
693 env.require(Balance("alice", usd(0)));
694 env.require(Balance("alice", gw2Usd(62.3)));
695 env.require(Balance("bob", usd(70)));
696 env.require(Balance("bob", gw2Usd(7)));
697 env.require(Balance(gw, Account("alice")["USD"](0)));
698 env.require(Balance(gw, Account("bob")["USD"](-70)));
699 env.require(Balance(gw2, Account("alice")["USD"](-62.3)));
700 env.require(Balance(gw2, Account("bob")["USD"](-7)));
701 }
702
703 void
705 {
706 testcase(
707 std::string("alternative paths - limit returned paths to best quality") +
708 (domainEnabled ? " w/ " : " w/o ") + "domain");
709 using namespace jtx;
710 Env env = pathTestEnv();
711 auto const gw = Account("gateway");
712 auto const usd = gw["USD"];
713 auto const gw2 = Account("gateway2");
714 auto const gw2Usd = gw2["USD"];
715 env.fund(XRP(10000), "alice", "bob", "carol", "dan", gw, gw2);
716 env.close();
717 env(rate("carol", 1.1));
718 env.trust(Account("carol")["USD"](800), "alice", "bob");
719 env.trust(Account("dan")["USD"](800), "alice", "bob");
720 env.trust(usd(800), "alice", "bob");
721 env.trust(gw2Usd(800), "alice", "bob");
722 env.trust(Account("alice")["USD"](800), "dan");
723 env.trust(Account("bob")["USD"](800), "dan");
724 env.close();
725 env(pay(gw2, "alice", gw2Usd(100)));
726 env.close();
727 env(pay("carol", "alice", Account("carol")["USD"](100)));
728 env.close();
729 env(pay(gw, "alice", usd(100)));
730 env.close();
731
732 std::optional<UInt256> domainID;
733 if (domainEnabled)
734 {
735 domainID = setupDomain(env, {"alice", "bob", "carol", "dan", gw, gw2});
736 }
737
738 STPathSet st;
739 STAmount sa;
740 std::tie(st, sa, std::ignore) = findPaths(
741 env, "alice", "bob", Account("bob")["USD"](5), std::nullopt, std::nullopt, domainID);
742 BEAST_EXPECT(
743 same(st, stpath("gateway"), stpath("gateway2"), stpath("dan"), stpath("carol")));
744 BEAST_EXPECT(equal(sa, Account("alice")["USD"](5)));
745 }
746
747 void
748 issuesPathNegativeIssue(bool const domainEnabled)
749 {
750 testcase(
751 std::string("path negative: Issue #5") + (domainEnabled ? " w/ " : " w/o ") + "domain");
752 using namespace jtx;
753 Env env = pathTestEnv();
754 env.fund(XRP(10000), "alice", "bob", "carol", "dan");
755 env.close();
756 env.trust(Account("bob")["USD"](100), "alice", "carol", "dan");
757 env.trust(Account("alice")["USD"](100), "dan");
758 env.trust(Account("carol")["USD"](100), "dan");
759 env(pay("bob", "carol", Account("bob")["USD"](75)));
760 env.require(Balance("bob", Account("carol")["USD"](-75)));
761 env.require(Balance("carol", Account("bob")["USD"](75)));
762 env.close();
763
764 std::optional<UInt256> domainID;
765 if (domainEnabled)
766 {
767 domainID = setupDomain(env, {"alice", "bob", "carol", "dan"});
768 }
769
770 auto result = findPaths(
771 env, "alice", "bob", Account("bob")["USD"](25), std::nullopt, std::nullopt, domainID);
772 BEAST_EXPECT(std::get<0>(result).empty());
773
774 env(pay("alice", "bob", Account("alice")["USD"](25)), Ter(tecPATH_DRY));
775 env.close();
776
777 result = findPaths(
778 env, "alice", "bob", Account("alice")["USD"](25), std::nullopt, std::nullopt, domainID);
779 BEAST_EXPECT(std::get<0>(result).empty());
780
781 env.require(Balance("alice", Account("bob")["USD"](0)));
782 env.require(Balance("alice", Account("dan")["USD"](0)));
783 env.require(Balance("bob", Account("alice")["USD"](0)));
784 env.require(Balance("bob", Account("carol")["USD"](-75)));
785 env.require(Balance("bob", Account("dan")["USD"](0)));
786 env.require(Balance("carol", Account("bob")["USD"](75)));
787 env.require(Balance("carol", Account("dan")["USD"](0)));
788 env.require(Balance("dan", Account("alice")["USD"](0)));
789 env.require(Balance("dan", Account("bob")["USD"](0)));
790 env.require(Balance("dan", Account("carol")["USD"](0)));
791 }
792
793 // alice_ -- limit 40 --> bob_
794 // alice_ --> carol_ --> dan --> bob_
795 // Balance of 100 USD Bob - Balance of 37 USD -> Rod
796 void
798 {
799 testcase("path negative: ripple-client issue #23: smaller");
800 using namespace jtx;
801 Env env = pathTestEnv();
802 env.fund(XRP(10000), "alice", "bob", "carol", "dan");
803 env.close();
804 env.trust(Account("alice")["USD"](40), "bob");
805 env.trust(Account("dan")["USD"](20), "bob");
806 env.trust(Account("alice")["USD"](20), "carol");
807 env.trust(Account("carol")["USD"](20), "dan");
808 env(pay("alice", "bob", Account("bob")["USD"](55)), Paths(Account("alice")["USD"]));
809 env.require(Balance("bob", Account("alice")["USD"](40)));
810 env.require(Balance("bob", Account("dan")["USD"](15)));
811 }
812
813 // alice_ -120 USD-> edward -25 USD-> bob_
814 // alice_ -25 USD-> carol_ -75 USD -> dan -100 USD-> bob_
815 void
817 {
818 testcase("path negative: ripple-client issue #23: larger");
819 using namespace jtx;
820 Env env = pathTestEnv();
821 env.fund(XRP(10000), "alice", "bob", "carol", "dan", "edward");
822 env.close();
823 env.trust(Account("alice")["USD"](120), "edward");
824 env.trust(Account("edward")["USD"](25), "bob");
825 env.trust(Account("dan")["USD"](100), "bob");
826 env.trust(Account("alice")["USD"](25), "carol");
827 env.trust(Account("carol")["USD"](75), "dan");
828 env(pay("alice", "bob", Account("bob")["USD"](50)), Paths(Account("alice")["USD"]));
829 env.require(Balance("alice", Account("edward")["USD"](-25)));
830 env.require(Balance("alice", Account("carol")["USD"](-25)));
831 env.require(Balance("bob", Account("edward")["USD"](25)));
832 env.require(Balance("bob", Account("dan")["USD"](25)));
833 env.require(Balance("carol", Account("alice")["USD"](25)));
834 env.require(Balance("carol", Account("dan")["USD"](-25)));
835 env.require(Balance("dan", Account("carol")["USD"](25)));
836 env.require(Balance("dan", Account("bob")["USD"](-25)));
837 }
838
839 // carol_ holds gateway AUD, sells gateway AUD for XRP
840 // bob_ will hold gateway AUD
841 // alice_ pays bob_ gateway AUD using XRP
842 void
843 viaOffersViaGateway(bool const domainEnabled)
844 {
845 testcase(std::string("via gateway") + (domainEnabled ? " w/ " : " w/o ") + "domain");
846 using namespace jtx;
847 Env env = pathTestEnv();
848 auto const gw = Account("gateway");
849 auto const aud = gw["AUD"];
850 env.fund(XRP(10000), "alice", "bob", "carol", gw);
851 env.close();
852 env(rate(gw, 1.1));
853 env.close();
854 env.trust(aud(100), "bob", "carol");
855 env.close();
856 env(pay(gw, "carol", aud(50)));
857 env.close();
858
859 std::optional<UInt256> domainID;
860 if (domainEnabled)
861 {
862 domainID = setupDomain(env, {"alice", "bob", "carol", gw});
863 env(offer("carol", XRP(50), aud(50)), Domain(*domainID));
864 env.close();
865 env(pay("alice", "bob", aud(10)), Sendmax(XRP(100)), Paths(XRP), Domain(*domainID));
866 env.close();
867 }
868 else
869 {
870 env(offer("carol", XRP(50), aud(50)));
871 env.close();
872 env(pay("alice", "bob", aud(10)), Sendmax(XRP(100)), Paths(XRP));
873 env.close();
874 }
875
876 env.require(Balance("bob", aud(10)));
877 env.require(Balance("carol", aud(39)));
878
879 auto const result = findPaths(
880 env, "alice", "bob", Account("bob")["USD"](25), std::nullopt, std::nullopt, domainID);
881 BEAST_EXPECT(std::get<0>(result).empty());
882 }
883
884 void
886 {
887 testcase("path find");
888 using namespace jtx;
889 Env env = pathTestEnv();
890 env.fund(XRP(10000), "alice", "bob", "carol");
891 env.close();
892 env.trust(Account("alice")["USD"](1000), "bob");
893 env.trust(Account("bob")["USD"](1000), "carol");
894
895 STPathSet st;
896 STAmount sa;
897 std::tie(st, sa, std::ignore) =
898 findPaths(env, "alice", "carol", Account("carol")["USD"](5));
899 BEAST_EXPECT(same(st, stpath("bob")));
900 BEAST_EXPECT(equal(sa, Account("alice")["USD"](5)));
901 }
902
903 void
905 {
906 testcase("quality set and test");
907 using namespace jtx;
908 Env env = pathTestEnv();
909 env.fund(XRP(10000), "alice", "bob");
910 env.close();
911 env(trust("bob", Account("alice")["USD"](1000)),
912 Json("{\"" + sfQualityIn.fieldName + "\": 2000}"),
913 Json("{\"" + sfQualityOut.fieldName + "\": 1400000000}"));
914
915 json::Value jv;
917 R"({
918 "Balance" : {
919 "currency" : "USD",
920 "issuer" : "rrrrrrrrrrrrrrrrrrrrBZbvji",
921 "value" : "0"
922 },
923 "Flags" : 131072,
924 "HighLimit" : {
925 "currency" : "USD",
926 "issuer" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
927 "value" : "1000"
928 },
929 "HighNode" : "0",
930 "HighQualityIn" : 2000,
931 "HighQualityOut" : 1400000000,
932 "LedgerEntryType" : "RippleState",
933 "LowLimit" : {
934 "currency" : "USD",
935 "issuer" : "rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn",
936 "value" : "0"
937 },
938 "LowNode" : "0"
939 })",
940 jv);
941
942 auto const jvL = env.le(keylet::trustLine(Account("bob").id(), Account("alice")["USD"]))
943 ->getJson(JsonOptions::Values::None);
944 for (auto it = jv.begin(); it != jv.end(); ++it)
945 BEAST_EXPECT(*it == jvL[it.memberName()]);
946 }
947
948 void
950 {
951 testcase("trust normal clear");
952 using namespace jtx;
953 Env env = pathTestEnv();
954 env.fund(XRP(10000), "alice", "bob");
955 env.close();
956 env.trust(Account("bob")["USD"](1000), "alice");
957 env.trust(Account("alice")["USD"](1000), "bob");
958
959 json::Value jv;
961 R"({
962 "Balance" : {
963 "currency" : "USD",
964 "issuer" : "rrrrrrrrrrrrrrrrrrrrBZbvji",
965 "value" : "0"
966 },
967 "Flags" : 196608,
968 "HighLimit" : {
969 "currency" : "USD",
970 "issuer" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
971 "value" : "1000"
972 },
973 "HighNode" : "0",
974 "LedgerEntryType" : "RippleState",
975 "LowLimit" : {
976 "currency" : "USD",
977 "issuer" : "rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn",
978 "value" : "1000"
979 },
980 "LowNode" : "0"
981 })",
982 jv);
983
984 auto const jvL = env.le(keylet::trustLine(Account("bob").id(), Account("alice")["USD"]))
985 ->getJson(JsonOptions::Values::None);
986 for (auto it = jv.begin(); it != jv.end(); ++it)
987 BEAST_EXPECT(*it == jvL[it.memberName()]);
988
989 env.trust(Account("bob")["USD"](0), "alice");
990 env.trust(Account("alice")["USD"](0), "bob");
991 BEAST_EXPECT(
992 env.le(keylet::trustLine(Account("bob").id(), Account("alice")["USD"])) == nullptr);
993 }
994
995 void
997 {
998 testcase("trust auto clear");
999 using namespace jtx;
1000 Env env = pathTestEnv();
1001 env.fund(XRP(10000), "alice", "bob");
1002 env.close();
1003 env.trust(Account("bob")["USD"](1000), "alice");
1004 env(pay("bob", "alice", Account("bob")["USD"](50)));
1005 env.trust(Account("bob")["USD"](0), "alice");
1006
1007 json::Value jv;
1009 R"({
1010 "Balance" :
1011 {
1012 "currency" : "USD",
1013 "issuer" : "rrrrrrrrrrrrrrrrrrrrBZbvji",
1014 "value" : "50"
1015 },
1016 "Flags" : 65536,
1017 "HighLimit" :
1018 {
1019 "currency" : "USD",
1020 "issuer" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
1021 "value" : "0"
1022 },
1023 "HighNode" : "0",
1024 "LedgerEntryType" : "RippleState",
1025 "LowLimit" :
1026 {
1027 "currency" : "USD",
1028 "issuer" : "rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn",
1029 "value" : "0"
1030 },
1031 "LowNode" : "0"
1032 })",
1033 jv);
1034
1035 auto const jvL = env.le(keylet::trustLine(Account("alice").id(), Account("bob")["USD"]))
1036 ->getJson(JsonOptions::Values::None);
1037 for (auto it = jv.begin(); it != jv.end(); ++it)
1038 BEAST_EXPECT(*it == jvL[it.memberName()]);
1039
1040 env(pay("alice", "bob", Account("alice")["USD"](50)));
1041 BEAST_EXPECT(
1042 env.le(keylet::trustLine(Account("alice").id(), Account("bob")["USD"])) == nullptr);
1043 }
1044
1045 void
1046 pathFind01(bool const domainEnabled)
1047 {
1048 testcase(
1049 std::string("Path Find: XRP -> XRP and XRP -> IOU") +
1050 (domainEnabled ? " w/ " : " w/o ") + "domain");
1051 using namespace jtx;
1052 Env env = pathTestEnv();
1053 Account const a1{"A1"};
1054 Account const a2{"A2"};
1055 Account const a3{"A3"};
1056 Account const g1{"G1"};
1057 Account const g2{"G2"};
1058 Account const g3{"G3"};
1059 Account const m1{"M1"};
1060
1061 env.fund(XRP(100000), a1);
1062 env.fund(XRP(10000), a2);
1063 env.fund(XRP(1000), a3, g1, g2, g3, m1);
1064 env.close();
1065
1066 env.trust(g1["XYZ"](5000), a1);
1067 env.trust(g3["ABC"](5000), a1);
1068 env.trust(g2["XYZ"](5000), a2);
1069 env.trust(g3["ABC"](5000), a2);
1070 env.trust(a2["ABC"](1000), a3);
1071 env.trust(g1["XYZ"](100000), m1);
1072 env.trust(g2["XYZ"](100000), m1);
1073 env.trust(g3["ABC"](100000), m1);
1074 env.close();
1075
1076 env(pay(g1, a1, g1["XYZ"](3500)));
1077 env(pay(g3, a1, g3["ABC"](1200)));
1078 env(pay(g2, m1, g2["XYZ"](25000)));
1079 env(pay(g3, m1, g3["ABC"](25000)));
1080 env.close();
1081
1083 if (domainEnabled)
1084 {
1085 domainID = setupDomain(env, {a1, a2, a3, g1, g2, g3, m1});
1086 env(offer(m1, g1["XYZ"](1000), g2["XYZ"](1000)), Domain(*domainID));
1087 env(offer(m1, XRP(10000), g3["ABC"](1000)), Domain(*domainID));
1088 env.close();
1089 }
1090 else
1091 {
1092 env(offer(m1, g1["XYZ"](1000), g2["XYZ"](1000)));
1093 env(offer(m1, XRP(10000), g3["ABC"](1000)));
1094 env.close();
1095 }
1096
1097 STPathSet st;
1098 STAmount sa, da;
1099
1100 {
1101 auto const& sendAmt = XRP(10);
1102 std::tie(st, sa, da) =
1103 findPaths(env, a1, a2, sendAmt, std::nullopt, xrpCurrency(), domainID);
1104 BEAST_EXPECT(equal(da, sendAmt));
1105 BEAST_EXPECT(st.empty());
1106 }
1107
1108 {
1109 // no path should exist for this since dest account
1110 // does not exist.
1111 auto const& sendAmt = XRP(200);
1112 std::tie(st, sa, da) =
1113 findPaths(env, a1, Account{"A0"}, sendAmt, std::nullopt, xrpCurrency(), domainID);
1114 BEAST_EXPECT(equal(da, sendAmt));
1115 BEAST_EXPECT(st.empty());
1116 }
1117
1118 {
1119 auto const& sendAmt = g3["ABC"](10);
1120 std::tie(st, sa, da) =
1121 findPaths(env, a2, g3, sendAmt, std::nullopt, xrpCurrency(), domainID);
1122 BEAST_EXPECT(equal(da, sendAmt));
1123 BEAST_EXPECT(equal(sa, XRP(100)));
1124 BEAST_EXPECT(same(st, stpath(ipe(g3["ABC"]))));
1125 }
1126
1127 {
1128 auto const& sendAmt = a2["ABC"](1);
1129 std::tie(st, sa, da) =
1130 findPaths(env, a1, a2, sendAmt, std::nullopt, xrpCurrency(), domainID);
1131 BEAST_EXPECT(equal(da, sendAmt));
1132 BEAST_EXPECT(equal(sa, XRP(10)));
1133 BEAST_EXPECT(same(st, stpath(ipe(g3["ABC"]), g3)));
1134 }
1135
1136 {
1137 auto const& sendAmt = a3["ABC"](1);
1138 std::tie(st, sa, da) =
1139 findPaths(env, a1, a3, sendAmt, std::nullopt, xrpCurrency(), domainID);
1140 BEAST_EXPECT(equal(da, sendAmt));
1141 BEAST_EXPECT(equal(sa, XRP(10)));
1142 BEAST_EXPECT(same(st, stpath(ipe(g3["ABC"]), g3, a2)));
1143 }
1144 }
1145
1146 void
1147 pathFind02(bool const domainEnabled)
1148 {
1149 testcase(
1150 std::string("Path Find: non-XRP -> XRP") + (domainEnabled ? " w/ " : " w/o ") +
1151 "domain");
1152 using namespace jtx;
1153 Env env = pathTestEnv();
1154 Account const a1{"A1"};
1155 Account const a2{"A2"};
1156 Account const g3{"G3"};
1157 Account const m1{"M1"};
1158
1159 env.fund(XRP(1000), a1, a2, g3);
1160 env.fund(XRP(11000), m1);
1161 env.close();
1162
1163 env.trust(g3["ABC"](1000), a1, a2);
1164 env.trust(g3["ABC"](100000), m1);
1165 env.close();
1166
1167 env(pay(g3, a1, g3["ABC"](1000)));
1168 env(pay(g3, a2, g3["ABC"](1000)));
1169 env(pay(g3, m1, g3["ABC"](1200)));
1170 env.close();
1171
1172 std::optional<UInt256> domainID;
1173 if (domainEnabled)
1174 {
1175 domainID = setupDomain(env, {a1, a2, g3, m1});
1176 env(offer(m1, g3["ABC"](1000), XRP(10000)), Domain(*domainID));
1177 }
1178 else
1179 {
1180 env(offer(m1, g3["ABC"](1000), XRP(10000)));
1181 }
1182
1183 STPathSet st;
1184 STAmount sa, da;
1185 auto const& sendAmt = XRP(10);
1186
1187 {
1188 std::tie(st, sa, da) =
1189 findPaths(env, a1, a2, sendAmt, std::nullopt, a2["ABC"].currency, domainID);
1190 BEAST_EXPECT(equal(da, sendAmt));
1191 BEAST_EXPECT(equal(sa, a1["ABC"](1)));
1192 BEAST_EXPECT(same(st, stpath(g3, ipe(xrpIssue()))));
1193 }
1194
1195 // domain offer will not be considered in pathfinding for non-domain
1196 // paths
1197 if (domainEnabled)
1198 {
1199 std::tie(st, sa, da) =
1200 findPaths(env, a1, a2, sendAmt, std::nullopt, a2["ABC"].currency);
1201 BEAST_EXPECT(equal(da, sendAmt));
1202 BEAST_EXPECT(st.empty());
1203 }
1204 }
1205
1206 void
1207 pathFind04(bool const domainEnabled)
1208 {
1209 testcase(
1210 std::string("Path Find: Bitstamp and SnapSwap, liquidity with no offers") +
1211 (domainEnabled ? " w/ " : " w/o ") + "domain");
1212 using namespace jtx;
1213 Env env = pathTestEnv();
1214 Account const a1{"A1"};
1215 Account const a2{"A2"};
1216 Account const g1Bs{"G1BS"};
1217 Account const g2Sw{"G2SW"};
1218 Account const m1{"M1"};
1219
1220 env.fund(XRP(1000), g1Bs, g2Sw, a1, a2);
1221 env.fund(XRP(11000), m1);
1222 env.close();
1223
1224 env.trust(g1Bs["HKD"](2000), a1);
1225 env.trust(g2Sw["HKD"](2000), a2);
1226 env.trust(g1Bs["HKD"](100000), m1);
1227 env.trust(g2Sw["HKD"](100000), m1);
1228 env.close();
1229
1230 env(pay(g1Bs, a1, g1Bs["HKD"](1000)));
1231 env(pay(g2Sw, a2, g2Sw["HKD"](1000)));
1232 // SnapSwap wants to be able to set trust line quality settings so they
1233 // can charge a fee when transactions ripple across. Liquidity
1234 // provider, via trusting/holding both accounts
1235 env(pay(g1Bs, m1, g1Bs["HKD"](1200)));
1236 env(pay(g2Sw, m1, g2Sw["HKD"](5000)));
1237 env.close();
1238
1239 std::optional<UInt256> domainID;
1240 if (domainEnabled)
1241 domainID = setupDomain(env, {a1, a2, g1Bs, g2Sw, m1});
1242
1243 STPathSet st;
1244 STAmount sa, da;
1245
1246 {
1247 auto const& sendAmt = a2["HKD"](10);
1248 std::tie(st, sa, da) =
1249 findPaths(env, a1, a2, sendAmt, std::nullopt, a2["HKD"].currency, domainID);
1250 BEAST_EXPECT(equal(da, sendAmt));
1251 BEAST_EXPECT(equal(sa, a1["HKD"](10)));
1252 BEAST_EXPECT(same(st, stpath(g1Bs, m1, g2Sw)));
1253 }
1254
1255 {
1256 auto const& sendAmt = a1["HKD"](10);
1257 std::tie(st, sa, da) =
1258 findPaths(env, a2, a1, sendAmt, std::nullopt, a1["HKD"].currency, domainID);
1259 BEAST_EXPECT(equal(da, sendAmt));
1260 BEAST_EXPECT(equal(sa, a2["HKD"](10)));
1261 BEAST_EXPECT(same(st, stpath(g2Sw, m1, g1Bs)));
1262 }
1263
1264 {
1265 auto const& sendAmt = a2["HKD"](10);
1266 std::tie(st, sa, da) =
1267 findPaths(env, g1Bs, a2, sendAmt, std::nullopt, a1["HKD"].currency, domainID);
1268 BEAST_EXPECT(equal(da, sendAmt));
1269 BEAST_EXPECT(equal(sa, g1Bs["HKD"](10)));
1270 BEAST_EXPECT(same(st, stpath(m1, g2Sw)));
1271 }
1272
1273 {
1274 auto const& sendAmt = m1["HKD"](10);
1275 std::tie(st, sa, da) =
1276 findPaths(env, m1, g1Bs, sendAmt, std::nullopt, a1["HKD"].currency, domainID);
1277 BEAST_EXPECT(equal(da, sendAmt));
1278 BEAST_EXPECT(equal(sa, m1["HKD"](10)));
1279 BEAST_EXPECT(st.empty());
1280 }
1281
1282 {
1283 auto const& sendAmt = a1["HKD"](10);
1284 std::tie(st, sa, da) =
1285 findPaths(env, g2Sw, a1, sendAmt, std::nullopt, a1["HKD"].currency, domainID);
1286 BEAST_EXPECT(equal(da, sendAmt));
1287 BEAST_EXPECT(equal(sa, g2Sw["HKD"](10)));
1288 BEAST_EXPECT(same(st, stpath(m1, g1Bs)));
1289 }
1290 }
1291
1292 void
1293 pathFind05(bool const domainEnabled)
1294 {
1295 testcase(
1296 std::string("Path Find: non-XRP -> non-XRP, same currency") +
1297 (domainEnabled ? " w/ " : " w/o ") + "domain");
1298 using namespace jtx;
1299 Env env = pathTestEnv();
1300 Account const a1{"A1"};
1301 Account const a2{"A2"};
1302 Account const a3{"A3"};
1303 Account const a4{"A4"};
1304 Account const g1{"G1"};
1305 Account const g2{"G2"};
1306 Account const g3{"G3"};
1307 Account const g4{"G4"};
1308 Account const m1{"M1"};
1309 Account const m2{"M2"};
1310
1311 env.fund(XRP(1000), a1, a2, a3, g1, g2, g3, g4);
1312 env.fund(XRP(10000), a4);
1313 env.fund(XRP(11000), m1, m2);
1314 env.close();
1315
1316 env.trust(g1["HKD"](2000), a1);
1317 env.trust(g2["HKD"](2000), a2);
1318 env.trust(g1["HKD"](2000), a3);
1319 env.trust(g1["HKD"](100000), m1);
1320 env.trust(g2["HKD"](100000), m1);
1321 env.trust(g1["HKD"](100000), m2);
1322 env.trust(g2["HKD"](100000), m2);
1323 env.close();
1324
1325 env(pay(g1, a1, g1["HKD"](1000)));
1326 env(pay(g2, a2, g2["HKD"](1000)));
1327 env(pay(g1, a3, g1["HKD"](1000)));
1328 env(pay(g1, m1, g1["HKD"](1200)));
1329 env(pay(g2, m1, g2["HKD"](5000)));
1330 env(pay(g1, m2, g1["HKD"](1200)));
1331 env(pay(g2, m2, g2["HKD"](5000)));
1332 env.close();
1333
1334 std::optional<UInt256> domainID;
1335 if (domainEnabled)
1336 {
1337 domainID = setupDomain(env, {a1, a2, a3, a4, g1, g2, g3, g4, m1, m2});
1338 env(offer(m1, g1["HKD"](1000), g2["HKD"](1000)), Domain(*domainID));
1339 env(offer(m2, XRP(10000), g2["HKD"](1000)), Domain(*domainID));
1340 env(offer(m2, g1["HKD"](1000), XRP(10000)), Domain(*domainID));
1341 }
1342 else
1343 {
1344 env(offer(m1, g1["HKD"](1000), g2["HKD"](1000)));
1345 env(offer(m2, XRP(10000), g2["HKD"](1000)));
1346 env(offer(m2, g1["HKD"](1000), XRP(10000)));
1347 }
1348
1349 STPathSet st;
1350 STAmount sa, da;
1351
1352 {
1353 // A) Borrow or repay --
1354 // Source -> Destination (repay source issuer)
1355 auto const& sendAmt = g1["HKD"](10);
1356 std::tie(st, sa, da) =
1357 findPaths(env, a1, g1, sendAmt, std::nullopt, g1["HKD"].currency, domainID);
1358 BEAST_EXPECT(st.empty());
1359 BEAST_EXPECT(equal(da, sendAmt));
1360 BEAST_EXPECT(equal(sa, a1["HKD"](10)));
1361 }
1362
1363 {
1364 // A2) Borrow or repay --
1365 // Source -> Destination (repay destination issuer)
1366 auto const& sendAmt = a1["HKD"](10);
1367 std::tie(st, sa, da) =
1368 findPaths(env, a1, g1, sendAmt, std::nullopt, g1["HKD"].currency, domainID);
1369 BEAST_EXPECT(st.empty());
1370 BEAST_EXPECT(equal(da, sendAmt));
1371 BEAST_EXPECT(equal(sa, a1["HKD"](10)));
1372 }
1373
1374 {
1375 // B) Common gateway --
1376 // Source -> AC -> Destination
1377 auto const& sendAmt = a3["HKD"](10);
1378 std::tie(st, sa, da) =
1379 findPaths(env, a1, a3, sendAmt, std::nullopt, g1["HKD"].currency, domainID);
1380 BEAST_EXPECT(equal(da, sendAmt));
1381 BEAST_EXPECT(equal(sa, a1["HKD"](10)));
1382 BEAST_EXPECT(same(st, stpath(g1)));
1383 }
1384
1385 {
1386 // C) Gateway to gateway --
1387 // Source -> OB -> Destination
1388 auto const& sendAmt = g2["HKD"](10);
1389 std::tie(st, sa, da) =
1390 findPaths(env, g1, g2, sendAmt, std::nullopt, g1["HKD"].currency, domainID);
1391 BEAST_EXPECT(equal(da, sendAmt));
1392 BEAST_EXPECT(equal(sa, g1["HKD"](10)));
1393 BEAST_EXPECT(same(
1394 st,
1395 stpath(ipe(g2["HKD"])),
1396 stpath(m1),
1397 stpath(m2),
1398 stpath(ipe(xrpIssue()), ipe(g2["HKD"]))));
1399 }
1400
1401 {
1402 // D) User to unlinked gateway via order book --
1403 // Source -> AC -> OB -> Destination
1404 auto const& sendAmt = g2["HKD"](10);
1405 std::tie(st, sa, da) =
1406 findPaths(env, a1, g2, sendAmt, std::nullopt, g1["HKD"].currency, domainID);
1407 BEAST_EXPECT(equal(da, sendAmt));
1408 BEAST_EXPECT(equal(sa, a1["HKD"](10)));
1409 BEAST_EXPECT(same(
1410 st,
1411 stpath(g1, m1),
1412 stpath(g1, m2),
1413 stpath(g1, ipe(g2["HKD"])),
1414 stpath(g1, ipe(xrpIssue()), ipe(g2["HKD"]))));
1415 }
1416
1417 {
1418 // I4) XRP bridge" --
1419 // Source -> AC -> OB to XRP -> OB from XRP -> AC ->
1420 // Destination
1421 auto const& sendAmt = a2["HKD"](10);
1422 std::tie(st, sa, da) =
1423 findPaths(env, a1, a2, sendAmt, std::nullopt, g1["HKD"].currency, domainID);
1424 BEAST_EXPECT(equal(da, sendAmt));
1425 BEAST_EXPECT(equal(sa, a1["HKD"](10)));
1426 BEAST_EXPECT(same(
1427 st,
1428 stpath(g1, m1, g2),
1429 stpath(g1, m2, g2),
1430 stpath(g1, ipe(g2["HKD"]), g2),
1431 stpath(g1, ipe(xrpIssue()), ipe(g2["HKD"]), g2)));
1432 }
1433 }
1434
1435 void
1436 pathFind06(bool const domainEnabled)
1437 {
1438 testcase(
1439 std::string("Path Find: non-XRP -> non-XRP, same currency)") +
1440 (domainEnabled ? " w/ " : " w/o ") + "domain");
1441 using namespace jtx;
1442 Env env = pathTestEnv();
1443 Account const a1{"A1"};
1444 Account const a2{"A2"};
1445 Account const a3{"A3"};
1446 Account const g1{"G1"};
1447 Account const g2{"G2"};
1448 Account const m1{"M1"};
1449
1450 env.fund(XRP(11000), m1);
1451 env.fund(XRP(1000), a1, a2, a3, g1, g2);
1452 env.close();
1453
1454 env.trust(g1["HKD"](2000), a1);
1455 env.trust(g2["HKD"](2000), a2);
1456 env.trust(a2["HKD"](2000), a3);
1457 env.trust(g1["HKD"](100000), m1);
1458 env.trust(g2["HKD"](100000), m1);
1459 env.close();
1460
1461 env(pay(g1, a1, g1["HKD"](1000)));
1462 env(pay(g2, a2, g2["HKD"](1000)));
1463 env(pay(g1, m1, g1["HKD"](5000)));
1464 env(pay(g2, m1, g2["HKD"](5000)));
1465 env.close();
1466
1467 std::optional<UInt256> domainID;
1468 if (domainEnabled)
1469 {
1470 domainID = setupDomain(env, {a1, a2, a3, g1, g2, m1});
1471 env(offer(m1, g1["HKD"](1000), g2["HKD"](1000)), Domain(*domainID));
1472 }
1473 else
1474 {
1475 env(offer(m1, g1["HKD"](1000), g2["HKD"](1000)));
1476 }
1477
1478 // E) Gateway to user
1479 // Source -> OB -> AC -> Destination
1480 auto const& sendAmt = a2["HKD"](10);
1481 STPathSet st;
1482 STAmount sa, da;
1483 std::tie(st, sa, da) =
1484 findPaths(env, g1, a2, sendAmt, std::nullopt, g1["HKD"].currency, domainID);
1485 BEAST_EXPECT(equal(da, sendAmt));
1486 BEAST_EXPECT(equal(sa, g1["HKD"](10)));
1487 BEAST_EXPECT(same(st, stpath(m1, g2), stpath(ipe(g2["HKD"]), g2)));
1488 }
1489
1490 void
1491 receiveMax(bool const domainEnabled)
1492 {
1493 testcase(std::string("Receive max") + (domainEnabled ? " w/ " : " w/o ") + "domain");
1494
1495 using namespace jtx;
1496 auto const alice = Account("alice");
1497 auto const bob = Account("bob");
1498 auto const charlie = Account("charlie");
1499 auto const gw = Account("gw");
1500 auto const usd = gw["USD"];
1501 {
1502 // XRP -> IOU receive max
1503 Env env = pathTestEnv();
1504 env.fund(XRP(10000), alice, bob, charlie, gw);
1505 env.close();
1506 env.trust(usd(100), alice, bob, charlie);
1507 env.close();
1508 env(pay(gw, charlie, usd(10)));
1509 env.close();
1510
1511 std::optional<UInt256> domainID;
1512 if (domainEnabled)
1513 {
1514 domainID = setupDomain(env, {alice, bob, charlie, gw});
1515 env(offer(charlie, XRP(10), usd(10)), Domain(*domainID));
1516 env.close();
1517 }
1518 else
1519 {
1520 env(offer(charlie, XRP(10), usd(10)));
1521 env.close();
1522 }
1523
1524 auto [st, sa, da] =
1525 findPaths(env, alice, bob, usd(-1), XRP(100).value(), std::nullopt, domainID);
1526 BEAST_EXPECT(sa == XRP(10));
1527 BEAST_EXPECT(equal(da, usd(10)));
1528 if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1))
1529 {
1530 auto const& pathElem = st[0][0];
1531 BEAST_EXPECT(
1532 pathElem.isOffer() && pathElem.getIssuerID() == gw.id() &&
1533 pathElem.getCurrency() == usd.currency);
1534 }
1535 }
1536 {
1537 // IOU -> XRP receive max
1538 Env env = pathTestEnv();
1539 env.fund(XRP(10000), alice, bob, charlie, gw);
1540 env.close();
1541 env.trust(usd(100), alice, bob, charlie);
1542 env.close();
1543 env(pay(gw, alice, usd(10)));
1544 env.close();
1545
1546 std::optional<UInt256> domainID;
1547 if (domainEnabled)
1548 {
1549 domainID = setupDomain(env, {alice, bob, charlie, gw});
1550 env(offer(charlie, usd(10), XRP(10)), Domain(*domainID));
1551 env.close();
1552 }
1553 else
1554 {
1555 env(offer(charlie, usd(10), XRP(10)));
1556 env.close();
1557 }
1559 auto [st, sa, da] =
1560 findPaths(env, alice, bob, drops(-1), usd(100).value(), std::nullopt, domainID);
1561 BEAST_EXPECT(sa == usd(10));
1562 BEAST_EXPECT(equal(da, XRP(10)));
1563 if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1))
1564 {
1565 auto const& pathElem = st[0][0];
1566 BEAST_EXPECT(
1567 pathElem.isOffer() && pathElem.getIssuerID() == xrpAccount() &&
1568 pathElem.getCurrency() == xrpCurrency());
1569 }
1570 }
1571 }
1572
1573 void
1575 {
1576 using namespace jtx;
1577 // This test will create trust lines with various values of the noRipple
1578 // flag. alice_ <-> george <-> bob_ george will sort of act like a
1579 // gateway, but use a different name to avoid the usual assumptions
1580 // about gateways.
1581 auto const alice = Account("alice");
1582 auto const bob = Account("bob");
1583 auto const george = Account("george");
1584 auto const usd = george["USD"];
1585 auto test = [&](std::string casename, bool aliceRipple, bool bobRipple, bool expectPath) {
1586 testcase(casename);
1587
1588 Env env = pathTestEnv();
1589 env.fund(XRP(10000), noripple(alice, bob, george));
1590 env.close();
1591 // Set the same flags at both ends of the trustline, even though
1592 // only george's matter.
1593 env(trust(alice, usd(100), aliceRipple ? tfClearNoRipple : tfSetNoRipple));
1594 env(trust(george, alice["USD"](100), aliceRipple ? tfClearNoRipple : tfSetNoRipple));
1595 env(trust(bob, usd(100), bobRipple ? tfClearNoRipple : tfSetNoRipple));
1596 env(trust(george, bob["USD"](100), bobRipple ? tfClearNoRipple : tfSetNoRipple));
1597 env.close();
1598 env(pay(george, alice, usd(70)));
1599 env.close();
1600
1601 auto [st, sa, da] = findPaths(env, "alice", "bob", Account("bob")["USD"](5));
1602 BEAST_EXPECT(equal(da, bob["USD"](5)));
1603
1604 if (expectPath)
1605 {
1606 BEAST_EXPECT(st.size() == 1);
1607 BEAST_EXPECT(same(st, stpath("george")));
1608 BEAST_EXPECT(equal(sa, alice["USD"](5)));
1609 }
1610 else
1611 {
1612 BEAST_EXPECT(st.empty());
1613 BEAST_EXPECT(equal(sa, XRP(0)));
1614 }
1615 };
1616 test("ripple -> ripple", true, true, true);
1617 test("ripple -> no ripple", true, false, true);
1618 test("no ripple -> ripple", false, true, true);
1619 test("no ripple -> no ripple", false, false, false);
1620 }
1621
1622 void
1624 {
1625 testcase("Hybrid offer path");
1626 using namespace jtx;
1627
1628 // test cases copied from path_find_05 and ensures path results for
1629 // different combinations of open/domain/hybrid offers. `func` is a
1630 // lambda param that creates different types of offers
1631 auto testPathfind = [&](auto func, bool const domainEnabled = false) {
1632 Env env = pathTestEnv();
1633 Account const a1{"A1"};
1634 Account const a2{"A2"};
1635 Account const a3{"A3"};
1636 Account const a4{"A4"};
1637 Account const g1{"G1"};
1638 Account const g2{"G2"};
1639 Account const g3{"G3"};
1640 Account const g4{"G4"};
1641 Account const m1{"M1"};
1642 Account const m2{"M2"};
1643
1644 env.fund(XRP(1000), a1, a2, a3, g1, g2, g3, g4);
1645 env.fund(XRP(10000), a4);
1646 env.fund(XRP(11000), m1, m2);
1647 env.close();
1648
1649 env.trust(g1["HKD"](2000), a1);
1650 env.trust(g2["HKD"](2000), a2);
1651 env.trust(g1["HKD"](2000), a3);
1652 env.trust(g1["HKD"](100000), m1);
1653 env.trust(g2["HKD"](100000), m1);
1654 env.trust(g1["HKD"](100000), m2);
1655 env.trust(g2["HKD"](100000), m2);
1656 env.close();
1657
1658 env(pay(g1, a1, g1["HKD"](1000)));
1659 env(pay(g2, a2, g2["HKD"](1000)));
1660 env(pay(g1, a3, g1["HKD"](1000)));
1661 env(pay(g1, m1, g1["HKD"](1200)));
1662 env(pay(g2, m1, g2["HKD"](5000)));
1663 env(pay(g1, m2, g1["HKD"](1200)));
1664 env(pay(g2, m2, g2["HKD"](5000)));
1665 env.close();
1666
1667 std::optional<UInt256> domainID =
1668 setupDomain(env, {a1, a2, a3, a4, g1, g2, g3, g4, m1, m2});
1669 BEAST_EXPECT(domainID);
1670
1671 func(env, m1, m2, g1, g2, *domainID);
1672
1673 STPathSet st;
1674 STAmount sa, da;
1675
1676 {
1677 // A) Borrow or repay --
1678 // Source -> Destination (repay source issuer)
1679 auto const& sendAmt = g1["HKD"](10);
1680 std::tie(st, sa, da) = findPaths(
1681 env,
1682 a1,
1683 g1,
1684 sendAmt,
1685 std::nullopt,
1686 g1["HKD"].currency,
1687 domainEnabled ? domainID : std::nullopt);
1688 BEAST_EXPECT(st.empty());
1689 BEAST_EXPECT(equal(da, sendAmt));
1690 BEAST_EXPECT(equal(sa, a1["HKD"](10)));
1691 }
1692
1693 {
1694 // A2) Borrow or repay --
1695 // Source -> Destination (repay destination issuer)
1696 auto const& sendAmt = a1["HKD"](10);
1697 std::tie(st, sa, da) = findPaths(
1698 env,
1699 a1,
1700 g1,
1701 sendAmt,
1702 std::nullopt,
1703 g1["HKD"].currency,
1704 domainEnabled ? domainID : std::nullopt);
1705 BEAST_EXPECT(st.empty());
1706 BEAST_EXPECT(equal(da, sendAmt));
1707 BEAST_EXPECT(equal(sa, a1["HKD"](10)));
1708 }
1709
1710 {
1711 // B) Common gateway --
1712 // Source -> AC -> Destination
1713 auto const& sendAmt = a3["HKD"](10);
1714 std::tie(st, sa, da) = findPaths(
1715 env,
1716 a1,
1717 a3,
1718 sendAmt,
1719 std::nullopt,
1720 g1["HKD"].currency,
1721 domainEnabled ? domainID : std::nullopt);
1722 BEAST_EXPECT(equal(da, sendAmt));
1723 BEAST_EXPECT(equal(sa, a1["HKD"](10)));
1724 BEAST_EXPECT(same(st, stpath(g1)));
1725 }
1726
1727 {
1728 // C) Gateway to gateway --
1729 // Source -> OB -> Destination
1730 auto const& sendAmt = g2["HKD"](10);
1731 std::tie(st, sa, da) = findPaths(
1732 env,
1733 g1,
1734 g2,
1735 sendAmt,
1736 std::nullopt,
1737 g1["HKD"].currency,
1738 domainEnabled ? domainID : std::nullopt);
1739 BEAST_EXPECT(equal(da, sendAmt));
1740 BEAST_EXPECT(equal(sa, g1["HKD"](10)));
1741 BEAST_EXPECT(same(
1742 st,
1743 stpath(ipe(g2["HKD"])),
1744 stpath(m1),
1745 stpath(m2),
1746 stpath(ipe(xrpIssue()), ipe(g2["HKD"]))));
1747 }
1748
1749 {
1750 // D) User to unlinked gateway via order book --
1751 // Source -> AC -> OB -> Destination
1752 auto const& sendAmt = g2["HKD"](10);
1753 std::tie(st, sa, da) = findPaths(
1754 env,
1755 a1,
1756 g2,
1757 sendAmt,
1758 std::nullopt,
1759 g1["HKD"].currency,
1760 domainEnabled ? domainID : std::nullopt);
1761 BEAST_EXPECT(equal(da, sendAmt));
1762 BEAST_EXPECT(equal(sa, a1["HKD"](10)));
1763 BEAST_EXPECT(same(
1764 st,
1765 stpath(g1, m1),
1766 stpath(g1, m2),
1767 stpath(g1, ipe(g2["HKD"])),
1768 stpath(g1, ipe(xrpIssue()), ipe(g2["HKD"]))));
1769 }
1770
1771 {
1772 // I4) XRP bridge" --
1773 // Source -> AC -> OB to XRP -> OB from XRP -> AC ->
1774 // Destination
1775 auto const& sendAmt = a2["HKD"](10);
1776 std::tie(st, sa, da) = findPaths(
1777 env,
1778 a1,
1779 a2,
1780 sendAmt,
1781 std::nullopt,
1782 g1["HKD"].currency,
1783 domainEnabled ? domainID : std::nullopt);
1784 BEAST_EXPECT(equal(da, sendAmt));
1785 BEAST_EXPECT(equal(sa, a1["HKD"](10)));
1786 BEAST_EXPECT(same(
1787 st,
1788 stpath(g1, m1, g2),
1789 stpath(g1, m2, g2),
1790 stpath(g1, ipe(g2["HKD"]), g2),
1791 stpath(g1, ipe(xrpIssue()), ipe(g2["HKD"]), g2)));
1792 }
1793 };
1794
1795 // the following tests exercise different combinations of open/hybrid
1796 // offers to make sure that hybrid offers work in pathfinding for open
1797 // order book
1798 {
1799 testPathfind(
1800 [](Env& env, Account m1, Account m2, Account g1, Account g2, UInt256 domainID) {
1801 env(offer(m1, g1["HKD"](1000), g2["HKD"](1000)),
1802 Domain(domainID),
1803 Txflags(tfHybrid));
1804 env(offer(m2, XRP(10000), g2["HKD"](1000)));
1805 env(offer(m2, g1["HKD"](1000), XRP(10000)));
1806 });
1807
1808 testPathfind(
1809 [](Env& env, Account m1, Account m2, Account g1, Account g2, UInt256 domainID) {
1810 env(offer(m1, g1["HKD"](1000), g2["HKD"](1000)),
1811 Domain(domainID),
1812 Txflags(tfHybrid));
1813 env(offer(m2, XRP(10000), g2["HKD"](1000)),
1814 Domain(domainID),
1815 Txflags(tfHybrid));
1816 env(offer(m2, g1["HKD"](1000), XRP(10000)));
1817 });
1818
1819 testPathfind(
1820 [](Env& env, Account m1, Account m2, Account g1, Account g2, UInt256 domainID) {
1821 env(offer(m1, g1["HKD"](1000), g2["HKD"](1000)),
1822 Domain(domainID),
1823 Txflags(tfHybrid));
1824 env(offer(m2, XRP(10000), g2["HKD"](1000)),
1825 Domain(domainID),
1826 Txflags(tfHybrid));
1827 env(offer(m2, g1["HKD"](1000), XRP(10000)),
1828 Domain(domainID),
1829 Txflags(tfHybrid));
1830 });
1831
1832 testPathfind(
1833 [](Env& env, Account m1, Account m2, Account g1, Account g2, UInt256 domainID) {
1834 env(offer(m1, g1["HKD"](1000), g2["HKD"](1000)));
1835 env(offer(m2, XRP(10000), g2["HKD"](1000)));
1836 env(offer(m2, g1["HKD"](1000), XRP(10000)),
1837 Domain(domainID),
1838 Txflags(tfHybrid));
1839 });
1840
1841 testPathfind(
1842 [](Env& env, Account m1, Account m2, Account g1, Account g2, UInt256 domainID) {
1843 env(offer(m1, g1["HKD"](1000), g2["HKD"](1000)));
1844 env(offer(m2, XRP(10000), g2["HKD"](1000)),
1845 Domain(domainID),
1846 Txflags(tfHybrid));
1847 env(offer(m2, g1["HKD"](1000), XRP(10000)),
1848 Domain(domainID),
1849 Txflags(tfHybrid));
1850 });
1851 }
1852
1853 // the following tests exercise different combinations of domain/hybrid
1854 // offers to make sure that hybrid offers work in pathfinding for domain
1855 // order book
1856 {
1857 testPathfind(
1858 [](Env& env, Account m1, Account m2, Account g1, Account g2, UInt256 domainID) {
1859 env(offer(m1, g1["HKD"](1000), g2["HKD"](1000)),
1860 Domain(domainID),
1861 Txflags(tfHybrid));
1862 env(offer(m2, XRP(10000), g2["HKD"](1000)), Domain(domainID));
1863 env(offer(m2, g1["HKD"](1000), XRP(10000)), Domain(domainID));
1865 true);
1866
1867 testPathfind(
1868 [](Env& env, Account m1, Account m2, Account g1, Account g2, UInt256 domainID) {
1869 env(offer(m1, g1["HKD"](1000), g2["HKD"](1000)),
1870 Domain(domainID),
1871 Txflags(tfHybrid));
1872 env(offer(m2, XRP(10000), g2["HKD"](1000)),
1873 Domain(domainID),
1874 Txflags(tfHybrid));
1875 env(offer(m2, g1["HKD"](1000), XRP(10000)), Domain(domainID));
1876 },
1877 true);
1878
1879 testPathfind(
1880 [](Env& env, Account m1, Account m2, Account g1, Account g2, UInt256 domainID) {
1881 env(offer(m1, g1["HKD"](1000), g2["HKD"](1000)), Domain(domainID));
1882 env(offer(m2, XRP(10000), g2["HKD"](1000)), Domain(domainID));
1883 env(offer(m2, g1["HKD"](1000), XRP(10000)),
1884 Domain(domainID),
1885 Txflags(tfHybrid));
1886 },
1887 true);
1888
1889 testPathfind(
1890 [](Env& env, Account m1, Account m2, Account g1, Account g2, UInt256 domainID) {
1891 env(offer(m1, g1["HKD"](1000), g2["HKD"](1000)), Domain(domainID));
1892 env(offer(m2, XRP(10000), g2["HKD"](1000)),
1893 Domain(domainID),
1894 Txflags(tfHybrid));
1895 env(offer(m2, g1["HKD"](1000), XRP(10000)),
1896 Domain(domainID),
1897 Txflags(tfHybrid));
1898 },
1899 true);
1900 }
1901 }
1902
1903 void
1905 {
1906 testcase("AMM not used in domain path");
1907 using namespace jtx;
1908 Env env = pathTestEnv();
1909 PermissionedDEX const permDex(env);
1910 auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = permDex;
1911 AMM const amm(env, alice_, XRP(10), USD(50));
1912
1913 STPathSet st;
1914 STAmount sa, da;
1915
1916 auto const& sendAmt = XRP(1);
1917
1918 // doing pathfind with domain won't include amm
1919 std::tie(st, sa, da) =
1920 findPaths(env, bob_, carol_, sendAmt, std::nullopt, USD.currency, domainID);
1921 BEAST_EXPECT(st.empty());
1922
1923 // a non-domain pathfind returns amm in the path
1924 std::tie(st, sa, da) = findPaths(env, bob_, carol_, sendAmt, std::nullopt, USD.currency);
1925 BEAST_EXPECT(same(st, stpath(gw_, ipe(xrpIssue()))));
1926 }
1927
1928 void
1930 {
1931 testcase("STPathSet::assembleAdd deduplication — O(N^2) regression");
1932
1933 static constexpr std::string_view kAccount1 = "A3F19C7B2E5D08146FB93A7C0E2D5184BC6F3A09";
1934 static constexpr std::string_view kAccount2 = "1D7E4B90C2A6F3851E0B9D47A2C5F8136E0A4B7D";
1935 static constexpr std::string_view kAccount3 = "F08C36A1D95E27B40CA1F63E8D204B7950E1C3A6";
1936 static constexpr std::string_view kAccount4 = "4B6209E7F1A3C85D0E94B27Af3D6018C5A7E92B4";
1937 static constexpr std::string_view kAccount5 = "9E2D7041BCA3F6589D013E7B2A4C6F80159D3E7A";
1938 static constexpr std::string_view kAccount6 = "7C5A91E384F2D06BA19C4E73D820F516B3A9C0E4";
1939 static constexpr std::string_view kAccount7 = "2F8B043C6A1E9D75B0C38E14F6A2D509731BC4E8";
1940 static constexpr std::string_view kAccount8 = "E61D9A30F47C285BA0D31E96C7B4F802513A8D6F";
1941
1942 static constexpr AccountID kAccountID1{kAccount1};
1943 static constexpr AccountID kAccountID2{kAccount2};
1944 static constexpr AccountID kAccountID3{kAccount3};
1945 static constexpr AccountID kAccountID4{kAccount4};
1946 static constexpr AccountID kAccountID5{kAccount5};
1947 static constexpr AccountID kAccountID6{kAccount6};
1948 static constexpr AccountID kAccountID7{kAccount7};
1949 static constexpr AccountID kAccountID8{kAccount8};
1950
1951 auto ps = STPathSet{STPathSet::DeduplicationTag{}};
1952
1953 auto createPathElements = [](auto const& account1, auto const& account2) {
1954 auto base = STPath{};
1955 base.pushBack(
1956 STPathElement{STPathElement::TypeAccount, account1, xrpCurrency(), account1});
1957 auto tail =
1958 STPathElement{STPathElement::TypeAccount, account2, xrpCurrency(), account2};
1959 return std::make_pair(base, tail);
1960 };
1962 {
1963 auto [base, tail] = createPathElements(kAccountID1, kAccountID2);
1964
1965 for (auto i = 0uz; i < 10000; ++i)
1966 {
1967 ps.assembleAdd(base, tail);
1968 }
1969
1970 BEAST_EXPECT(ps.size() == 1);
1971 }
1972
1973 {
1974 auto [base, tail] = createPathElements(kAccountID3, kAccountID4);
1975 ps.assembleAdd(base, tail);
1976 }
1977
1978 {
1979 auto [base, tail] = createPathElements(kAccountID5, kAccountID6);
1980 ps.assembleAdd(base, tail);
1981 }
1982
1983 {
1984 auto [base, tail] = createPathElements(kAccountID7, kAccountID8);
1985
1986 auto before = ps.size();
1987
1988 for (auto i = 0uz; i < 10000; ++i)
1989 {
1990 ps.assembleAdd(base, tail);
1991 }
1992
1993 BEAST_EXPECT(ps.size() - before == 1);
1994 }
1995
1996 {
1997 auto [base, tail] = createPathElements(kAccountID1, kAccountID3);
1998 auto copy = base;
1999 copy.pushBack(tail);
2000
2001 auto before = ps.size();
2002
2003 ps.pushBack(copy);
2004 ps.assembleAdd(base, tail);
2005
2006 BEAST_EXPECT(ps.size() - before == 1);
2007 }
2008
2009 {
2010 auto [base, tail] = createPathElements(kAccountID2, kAccountID4);
2011 auto copy = base;
2012 copy.pushBack(tail);
2013
2014 auto before = ps.size();
2015
2016 ps.emplaceBack(copy);
2017 ps.assembleAdd(base, tail);
2018
2019 BEAST_EXPECT(ps.size() - before == 1);
2020 }
2021
2022 BEAST_EXPECT(ps.size() == 6);
2023 }
2024
2025 void
2027 {
2028 testcase("STPathSet::pushBack/emplaceBack deduplication");
2029
2030 // pushBack and emplaceBack reject duplicates on a set built with the
2031 // DeduplicationTag, and append unconditionally without it. Both
2032 // report which happened. The unconditional case is the one the wire
2033 // and JSON paths rely on: collapsing duplicates there would change the
2034 // signed content of a transaction.
2035
2036 static constexpr AccountID kAccountID1{"A3F19C7B2E5D08146FB93A7C0E2D5184BC6F3A09"};
2037 static constexpr AccountID kAccountID2{"1D7E4B90C2A6F3851E0B9D47A2C5F8136E0A4B7D"};
2038 static constexpr AccountID kAccountID3{"F08C36A1D95E27B40CA1F63E8D204B7950E1C3A6"};
2039
2040 auto makePath = [](AccountID const& account) {
2041 auto p = STPath{};
2042 p.pushBack(STPathElement{STPathElement::TypeAccount, account, xrpCurrency(), account});
2043 return p;
2044 };
2045
2046 auto const first = makePath(kAccountID1);
2047 auto const second = makePath(kAccountID2);
2048 auto const third = makePath(kAccountID3);
2049
2050 // Deduplicating set: the second insert of a path is rejected, and the
2051 // rejection is reported rather than silently swallowed.
2052 {
2053 auto ps = STPathSet{STPathSet::DeduplicationTag{}};
2054
2055 BEAST_EXPECT(ps.pushBack(first));
2056 BEAST_EXPECT(ps.size() == 1);
2057
2058 BEAST_EXPECT(!ps.pushBack(first));
2059 BEAST_EXPECT(ps.size() == 1);
2060
2061 // emplaceBack sees paths registered by pushBack...
2062 BEAST_EXPECT(!ps.emplaceBack(first));
2063 BEAST_EXPECT(ps.size() == 1);
2064
2065 BEAST_EXPECT(ps.emplaceBack(second));
2066 BEAST_EXPECT(ps.size() == 2);
2067
2068 // ...and pushBack sees paths registered by emplaceBack.
2069 BEAST_EXPECT(!ps.pushBack(second));
2070 BEAST_EXPECT(ps.size() == 2);
2071
2072 // emplaceBack's forwarding form registers the same way.
2073 BEAST_EXPECT(ps.emplaceBack(std::vector<STPathElement>{third.front()}));
2074 BEAST_EXPECT(ps.size() == 3);
2075 BEAST_EXPECT(!ps.pushBack(third));
2076 BEAST_EXPECT(ps.size() == 3);
2077
2078 // A rejected duplicate must not disturb what is already stored.
2079 BEAST_EXPECT(ps[0] == first);
2080 BEAST_EXPECT(ps[1] == second);
2081 BEAST_EXPECT(ps[2] == third);
2082 }
2083
2084 // Without the tag there is no index, so duplicates are appended and
2085 // both methods report success every time.
2086 {
2087 auto plain = STPathSet{};
2088 BEAST_EXPECT(plain.pushBack(first));
2089 BEAST_EXPECT(plain.pushBack(first));
2090 BEAST_EXPECT(plain.emplaceBack(first));
2091 BEAST_EXPECT(plain.size() == 3);
2092
2093 auto named = STPathSet{sfPaths};
2094 BEAST_EXPECT(named.pushBack(first));
2095 BEAST_EXPECT(named.pushBack(first));
2096 BEAST_EXPECT(named.size() == 2);
2097 }
2098 }
2099
2100 void
2102 {
2103 testcase("STPathElement hash injectivity");
2104
2105 auto const zeroCurrency =
2106 STPathElement{AccountID{}, PathAsset{Currency{}}, AccountID{}, true};
2107 auto const zeroMPT = STPathElement{AccountID{}, PathAsset{MPTID{}}, AccountID{}, true};
2108
2109 BEAST_EXPECT(!(zeroCurrency == zeroMPT));
2110
2111 auto path = [](std::vector<STPathElement> const& elements) {
2112 auto p = STPath{};
2113 for (auto const& element : elements)
2114 p.pushBack(element);
2115 return p;
2116 };
2117
2118 auto const currencyFirst = path({zeroCurrency, zeroMPT});
2119 auto const mptFirst = path({zeroMPT, zeroCurrency});
2120
2121 BEAST_EXPECT(!(currencyFirst == mptFirst));
2122
2123 auto const hasher = HardenedHash<>{};
2124 BEAST_EXPECT(hasher(currencyFirst) != hasher(mptFirst));
2125
2126 auto mask = std::vector<int>{0, 0, 1, 1};
2127 auto hashes = std::set<std::size_t>{};
2128 auto orderings = 0uz;
2129 do
2130 {
2131 auto elements = std::vector<STPathElement>{};
2132 for (auto const isMPT : mask)
2133 {
2134 elements.push_back(isMPT != 0 ? zeroMPT : zeroCurrency);
2135 }
2136 hashes.insert(hasher(path(elements)));
2137 ++orderings;
2138 } while (std::ranges::next_permutation(mask).found);
2139
2140 BEAST_EXPECT(orderings == 6);
2141 BEAST_EXPECT(hashes.size() == orderings);
2142
2143 auto seen = HardenedHashSet<STPath>{};
2144 for (auto const& p : {currencyFirst, mptFirst})
2145 {
2146 seen.emplace(p);
2147 }
2148 BEAST_EXPECT(seen.size() == 2);
2149
2150 // The other half of the invariant: equal elements must hash equally.
2151 // STPathElement::operator== masks type_ down to the TypeAccount bit, so
2152 // elements whose remaining type bits differ still compare equal --
2153 // hashing the full type_ would give them distinct hashes and silently
2154 // defeat deduplication.
2155 static constexpr AccountID kAccount{"A3F19C7B2E5D08146FB93A7C0E2D5184BC6F3A09"};
2156 static constexpr AccountID kIssuer{"1D7E4B90C2A6F3851E0B9D47A2C5F8136E0A4B7D"};
2157
2158 auto const equivalent = std::vector<std::pair<STPathElement, STPathElement>>{
2159 // forceAsset toggles TypeCurrency on an XRP asset.
2160 {STPathElement{kAccount, PathAsset{xrpCurrency()}, kIssuer, true},
2161 STPathElement{kAccount, PathAsset{xrpCurrency()}, kIssuer, false}},
2162 // An explicit type mask vs. one derived from the populated fields.
2163 {STPathElement{STPathElement::TypeAccount, kAccount, xrpCurrency(), kIssuer},
2164 STPathElement{kAccount, PathAsset{xrpCurrency()}, kIssuer, false}},
2165 };
2166
2167 for (auto const& [lhs, rhs] : equivalent)
2168 {
2169 BEAST_EXPECT(lhs.getNodeType() != rhs.getNodeType());
2170 BEAST_EXPECT(lhs == rhs);
2171
2172 auto const lhsPath = path({lhs});
2173 auto const rhsPath = path({rhs});
2174 BEAST_EXPECT(hasher(lhsPath) == hasher(rhsPath));
2175
2177 equal.emplace(lhsPath);
2178 equal.emplace(rhsPath);
2179 BEAST_EXPECT(equal.size() == 1);
2180 }
2181 }
2182
2183 void
2185 {
2186 testcase("STPathSet deserialization preserves duplicate paths");
2187
2188 // The `Paths` field of a signed transaction must round-trip byte for
2189 // byte. The deduplication index exists solely for pathfinding, so the
2190 // deserializing constructor must never engage it: collapsing duplicates
2191 // on parse would silently change the signed content of a transaction.
2192
2193 static constexpr AccountID kAccountID1{"A3F19C7B2E5D08146FB93A7C0E2D5184BC6F3A09"};
2194 static constexpr AccountID kAccountID2{"1D7E4B90C2A6F3851E0B9D47A2C5F8136E0A4B7D"};
2195
2196 auto const element =
2197 STPathElement{kAccountID1, PathAsset{xrpCurrency()}, kAccountID2, true};
2198
2199 auto path = STPath{};
2200 path.pushBack(element);
2201
2202 static constexpr auto kDuplicates = 64uz;
2203
2204 auto original = STPathSet{sfPaths};
2205 for (auto i = 0uz; i < kDuplicates; ++i)
2206 {
2207 original.pushBack(path);
2208 }
2209
2210 // No index was requested, so nothing is deduplicated on the way in.
2211 BEAST_EXPECT(original.size() == kDuplicates);
2212
2213 auto s = Serializer{};
2214 original.add(s);
2215
2216 auto sit = SerialIter{s.slice()};
2217 auto const parsed = STPathSet{sit, sfPaths};
2218
2219 // The duplicates survive the round trip...
2220 BEAST_EXPECT(parsed.size() == kDuplicates);
2221 BEAST_EXPECT(parsed.isEquivalent(original));
2222
2223 // ...and re-serializing reproduces the original bytes exactly.
2224 auto serialized = Serializer{};
2225 parsed.add(serialized);
2226 BEAST_EXPECT(serialized.getData() == s.getData());
2227
2228 // A parsed set holds no index, so appending to it stays append-only.
2229 auto appended = parsed;
2230 appended.pushBack(path);
2231 BEAST_EXPECT(appended.size() == kDuplicates + 1);
2232 }
2233
2234 void
2235 run() override
2236 {
2254
2255 for (bool const domainEnabled : {false, true})
2256 {
2257 pathFind(domainEnabled);
2258 pathFindConsumeAll(domainEnabled);
2259 alternativePathConsumeBoth(domainEnabled);
2262 issuesPathNegativeIssue(domainEnabled);
2263 viaOffersViaGateway(domainEnabled);
2264 xrpToXrp(domainEnabled);
2265 receiveMax(domainEnabled);
2266
2267 // The following path_find_NN tests are data driven tests
2268 // that were originally implemented in js/coffee and migrated
2269 // here. The quantities and currencies used are taken directly from
2270 // those legacy tests, which in some cases probably represented
2271 // customer use cases.
2272
2273 pathFind01(domainEnabled);
2274 pathFind02(domainEnabled);
2275 pathFind04(domainEnabled);
2276 pathFind05(domainEnabled);
2277 pathFind06(domainEnabled);
2278 }
2279
2281 ammDomainPath();
2282 }
2283};
2284
2286
2287} // namespace xrpl::test
A testsuite class.
Definition suite.h:52
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Unserialize a JSON document into a Value.
Definition json_reader.h:20
bool parse(std::string const &document, Value &root)
Read a Value from a JSON document.
Represents a JSON value.
Definition json_value.h:117
const_iterator begin() const
const_iterator end() const
bool isMember(char const *key) const
Return true if the object has a member named key.
Seed functor once per construction.
json::Value getJson(JsonOptions=JsonOptions::Values::None) const override
Definition STAmount.cpp:735
Holds the serialized result of parsing an input JSON object.
std::optional< STObject > object
The STObject if the parse was successful.
bool empty() const
Definition STPathSet.h:630
A consumption charge.
Definition Charge.h:13
An endpoint that consumes resources.
Definition Consumer.h:20
std::condition_variable cv_
bool waitFor(std::chrono::duration< Rep, Period > const &relTime)
void pathFind06(bool const domainEnabled)
void issuesPathNegativeRippleClientIssue23Larger()
void testAssembleAddDeduplication()
void run() override
Runs the suite.
void issuesPathNegativeIssue(bool const domainEnabled)
void alternativePathsConsumeBestTransferFirst()
void xrpToXrp(bool const domainEnabled)
void pathFind01(bool const domainEnabled)
void pathFind04(bool const domainEnabled)
auto findPathsRequest(jtx::Env &env, jtx::Account const &src, jtx::Account const &dst, STAmount const &saDstAmount, std::optional< STAmount > const &saSendMax=std::nullopt, std::optional< Currency > const &saSrcCurrency=std::nullopt, std::optional< UInt256 > const &domain=std::nullopt, std::optional< AccountID > const &saSrcIssuer=std::nullopt)
void receiveMax(bool const domainEnabled)
void alternativePathsLimitReturnedPathsToBestQuality(bool const domainEnabled)
void pathFind05(bool const domainEnabled)
void viaOffersViaGateway(bool const domainEnabled)
void trustAutoClearTrustNormalClear()
void alternativePathConsumeBoth(bool const domainEnabled)
void issuesPathNegativeRippleClientIssue23Smaller()
std::tuple< STPathSet, STAmount, STAmount > findPaths(jtx::Env &env, jtx::Account const &src, jtx::Account const &dst, STAmount const &saDstAmount, std::optional< STAmount > const &saSendMax=std::nullopt, std::optional< Currency > const &saSrcCurrency=std::nullopt, std::optional< UInt256 > const &domain=std::nullopt, std::optional< AccountID > const &saSrcIssuer=std::nullopt)
void testDeserializationPreservesDuplicates()
void pathFind02(bool const domainEnabled)
void noDirectPathNoIntermediaryNoAlternatives()
void sourceCurrencyIssuerSelection()
void qualityPathsQualitySetAndTest()
void pathFind(bool const domainEnabled)
void pathFindConsumeAll(bool const domainEnabled)
void trustAutoClearTrustAutoClear()
void alternativePathsConsumeBestTransfer(bool const domainEnabled)
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
A transaction testing environment.
Definition Env.h:161
Application & app()
Definition Env.h:300
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:354
beast::Journal const journal
Definition Env.h:204
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:766
Inject raw JSON.
Definition jtx_json.h:16
Add a path.
Definition paths.h:47
Set Paths, SendMax on a JTx.
Definition paths.h:23
Sets the SendMax on a JTx.
Definition sendmax.h:16
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
Set the flags on a JTx.
Definition txflags.h:14
T copy(T... args)
T make_pair(T... args)
T make_tuple(T... args)
@ Array
array value (ordered list)
Definition json_value.h:28
@ Object
object value (collection of name/value pairs).
Definition json_value.h:29
Keylet amm(Asset const &issue1, Asset const &issue2) noexcept
AMM entry.
Definition Indexes.cpp:471
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:220
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:275
Charge const kFeeReferenceRpc
static constexpr int kMaxSrcCur
Maximum number of source currencies allowed in a path find request.
static constexpr int kMaxAutoSrcCur
Maximum number of auto source currencies in a path find request.
static constexpr auto kApiVersionIfUnspecified
Definition ApiVersion.h:44
Status doCommand(rpc::JsonContext &context, json::Value &result)
Execute an RPC command and store the results in a json::Value.
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
bool same(STPathSet const &st1, Args const &... args)
STPathElement ipe(Asset const &asset)
std::array< Account, 1+sizeof...(Args)> noripple(Account const &account, Args const &... args)
Designate accounts as no-ripple in Env::fund.
Definition Env.h:86
bool equal(STAmount const &sa1, STAmount const &sa2)
json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:14
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:38
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
UInt256 setupDomain(jtx::Env &env, std::vector< jtx::Account > const &accounts, jtx::Account const &domainOwner, std::string const &credType)
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
STPath stpath(Args const &... args)
json::Value rate(Account const &account, double multiplier)
Set a transfer rate.
Definition rate.cpp:15
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
json::Value rpf(jtx::Account const &src, jtx::Account const &dst, std::uint32_t numSrc)
Definition Path_test.cpp:70
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
std::unordered_set< Value, Hash, Pred, Allocator > HardenedHashSet
BaseUInt< 256 > Domain
Domain is a 256-bit hash representing a specific domain.
Definition UintTypes.h:59
SField const sfGeneric
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:95
BaseUInt< 160, detail::CurrencyTag > Currency
Currency is a hash representing a specific currency.
Definition UintTypes.h:42
Currency const & xrpCurrency()
XRP currency.
Definition UintTypes.cpp:99
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
@ USER
Definition Role.h:27
BaseUInt< 256 > UInt256
Definition base_uint.h:580
@ JtClient
Definition Job.h:31
BaseUInt< 192 > MPTID
MPTID is a 192-bit value representing MPT Issuance ID, which is a concatenation of a 32-bit sequence ...
Definition UintTypes.h:54
STAmount amountFromJson(SField const &name, json::Value const &v)
Definition STAmount.cpp:917
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
AccountID const & xrpAccount()
Compute AccountID from public key.
@ tecPATH_DRY
Definition TER.h:302
T next_permutation(T... args)
std::shared_ptr< JobQueue::Coro > coro
Definition Context.h:35
json::Value params
Definition Context.h:51
T tie(T... args)
T to_string(T... args)