rippled
Loading...
Searching...
No Matches
Book_test.cpp
1#include <test/jtx.h>
2#include <test/jtx/WSClient.h>
3
4#include <xrpld/rpc/detail/Tuning.h>
5
6#include <xrpl/beast/unit_test.h>
7#include <xrpl/protocol/Indexes.h>
8#include <xrpl/protocol/LedgerFormats.h>
9#include <xrpl/protocol/TxFlags.h>
10#include <xrpl/protocol/jss.h>
11
12namespace ripple {
13namespace test {
14
16{
19 jtx::Env& env,
20 Issue const& in,
21 Issue const& out,
23 {
24 std::string dir;
25 auto uBookBase = getBookBase({in, out, domain});
26 auto uBookEnd = getQualityNext(uBookBase);
27 auto view = env.closed();
28 auto key = view->succ(uBookBase, uBookEnd);
29 if (key)
30 {
31 auto sleOfferDir = view->read(keylet::page(key.value()));
32 uint256 offerIndex;
33 unsigned int bookEntry;
35 *view, sleOfferDir->key(), sleOfferDir, bookEntry, offerIndex);
36 auto sleOffer = view->read(keylet::offer(offerIndex));
37 dir = to_string(sleOffer->getFieldH256(sfBookDirectory));
38 }
39 return dir;
40 }
41
42public:
43 void
45 {
46 testcase("One Side Empty Book");
47 using namespace std::chrono_literals;
48 using namespace jtx;
49 Env env(*this);
50 env.fund(XRP(10000), "alice");
51 auto USD = Account("alice")["USD"];
52 auto wsc = makeWSClient(env.app().config());
53 Json::Value books;
54
55 {
56 // RPC subscribe to books stream
57 books[jss::books] = Json::arrayValue;
58 {
59 auto& j = books[jss::books].append(Json::objectValue);
60 j[jss::snapshot] = true;
61 j[jss::taker_gets][jss::currency] = "XRP";
62 j[jss::taker_pays][jss::currency] = "USD";
63 j[jss::taker_pays][jss::issuer] = Account("alice").human();
64 }
65
66 auto jv = wsc->invoke("subscribe", books);
67 if (wsc->version() == 2)
68 {
69 BEAST_EXPECT(
70 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
71 BEAST_EXPECT(
72 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
73 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
74 }
75 if (!BEAST_EXPECT(jv[jss::status] == "success"))
76 return;
77 BEAST_EXPECT(
78 jv[jss::result].isMember(jss::offers) &&
79 jv[jss::result][jss::offers].size() == 0);
80 BEAST_EXPECT(!jv[jss::result].isMember(jss::asks));
81 BEAST_EXPECT(!jv[jss::result].isMember(jss::bids));
82 }
83
84 {
85 // Create an ask: TakerPays 700, TakerGets 100/USD
86 env(offer("alice", XRP(700), USD(100)),
87 require(owners("alice", 1)));
88 env.close();
89
90 // Check stream update
91 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
92 auto const& t = jv[jss::transaction];
93 return t[jss::TransactionType] == jss::OfferCreate &&
94 t[jss::TakerGets] ==
95 USD(100).value().getJson(JsonOptions::none) &&
96 t[jss::TakerPays] ==
97 XRP(700).value().getJson(JsonOptions::none);
98 }));
99 }
100
101 {
102 // Create a bid: TakerPays 100/USD, TakerGets 75
103 env(offer("alice", USD(100), XRP(75)), require(owners("alice", 2)));
104 env.close();
105 BEAST_EXPECT(!wsc->getMsg(10ms));
106 }
107
108 // RPC unsubscribe
109 auto jv = wsc->invoke("unsubscribe", books);
110 BEAST_EXPECT(jv[jss::status] == "success");
111 if (wsc->version() == 2)
112 {
113 BEAST_EXPECT(
114 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
115 BEAST_EXPECT(
116 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
117 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
118 }
119 }
120
121 void
123 {
124 testcase("One Side Offers In Book");
125 using namespace std::chrono_literals;
126 using namespace jtx;
127 Env env(*this);
128 env.fund(XRP(10000), "alice");
129 auto USD = Account("alice")["USD"];
130 auto wsc = makeWSClient(env.app().config());
131 Json::Value books;
132
133 // Create an ask: TakerPays 500, TakerGets 100/USD
134 env(offer("alice", XRP(500), USD(100)), require(owners("alice", 1)));
135
136 // Create a bid: TakerPays 100/USD, TakerGets 200
137 env(offer("alice", USD(100), XRP(200)), require(owners("alice", 2)));
138 env.close();
139
140 {
141 // RPC subscribe to books stream
142 books[jss::books] = Json::arrayValue;
143 {
144 auto& j = books[jss::books].append(Json::objectValue);
145 j[jss::snapshot] = true;
146 j[jss::taker_gets][jss::currency] = "XRP";
147 j[jss::taker_pays][jss::currency] = "USD";
148 j[jss::taker_pays][jss::issuer] = Account("alice").human();
149 }
150
151 auto jv = wsc->invoke("subscribe", books);
152 if (wsc->version() == 2)
153 {
154 BEAST_EXPECT(
155 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
156 BEAST_EXPECT(
157 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
158 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
159 }
160 if (!BEAST_EXPECT(jv[jss::status] == "success"))
161 return;
162 BEAST_EXPECT(
163 jv[jss::result].isMember(jss::offers) &&
164 jv[jss::result][jss::offers].size() == 1);
165 BEAST_EXPECT(
166 jv[jss::result][jss::offers][0u][jss::TakerGets] ==
167 XRP(200).value().getJson(JsonOptions::none));
168 BEAST_EXPECT(
169 jv[jss::result][jss::offers][0u][jss::TakerPays] ==
170 USD(100).value().getJson(JsonOptions::none));
171 BEAST_EXPECT(!jv[jss::result].isMember(jss::asks));
172 BEAST_EXPECT(!jv[jss::result].isMember(jss::bids));
173 }
174
175 {
176 // Create an ask: TakerPays 700, TakerGets 100/USD
177 env(offer("alice", XRP(700), USD(100)),
178 require(owners("alice", 3)));
179 env.close();
180
181 // Check stream update
182 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
183 auto const& t = jv[jss::transaction];
184 return t[jss::TransactionType] == jss::OfferCreate &&
185 t[jss::TakerGets] ==
186 USD(100).value().getJson(JsonOptions::none) &&
187 t[jss::TakerPays] ==
188 XRP(700).value().getJson(JsonOptions::none);
189 }));
190 }
191
192 {
193 // Create a bid: TakerPays 100/USD, TakerGets 75
194 env(offer("alice", USD(100), XRP(75)), require(owners("alice", 4)));
195 env.close();
196 BEAST_EXPECT(!wsc->getMsg(10ms));
197 }
198
199 // RPC unsubscribe
200 auto jv = wsc->invoke("unsubscribe", books);
201 BEAST_EXPECT(jv[jss::status] == "success");
202 if (wsc->version() == 2)
203 {
204 BEAST_EXPECT(
205 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
206 BEAST_EXPECT(
207 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
208 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
209 }
210 }
211
212 void
214 {
215 testcase("Both Sides Empty Book");
216 using namespace std::chrono_literals;
217 using namespace jtx;
218 Env env(*this);
219 env.fund(XRP(10000), "alice");
220 auto USD = Account("alice")["USD"];
221 auto wsc = makeWSClient(env.app().config());
222 Json::Value books;
223
224 {
225 // RPC subscribe to books stream
226 books[jss::books] = Json::arrayValue;
227 {
228 auto& j = books[jss::books].append(Json::objectValue);
229 j[jss::snapshot] = true;
230 j[jss::both] = true;
231 j[jss::taker_gets][jss::currency] = "XRP";
232 j[jss::taker_pays][jss::currency] = "USD";
233 j[jss::taker_pays][jss::issuer] = Account("alice").human();
234 }
235
236 auto jv = wsc->invoke("subscribe", books);
237 if (wsc->version() == 2)
238 {
239 BEAST_EXPECT(
240 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
241 BEAST_EXPECT(
242 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
243 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
244 }
245 if (!BEAST_EXPECT(jv[jss::status] == "success"))
246 return;
247 BEAST_EXPECT(
248 jv[jss::result].isMember(jss::asks) &&
249 jv[jss::result][jss::asks].size() == 0);
250 BEAST_EXPECT(
251 jv[jss::result].isMember(jss::bids) &&
252 jv[jss::result][jss::bids].size() == 0);
253 BEAST_EXPECT(!jv[jss::result].isMember(jss::offers));
254 }
255
256 {
257 // Create an ask: TakerPays 700, TakerGets 100/USD
258 env(offer("alice", XRP(700), USD(100)),
259 require(owners("alice", 1)));
260 env.close();
261
262 // Check stream update
263 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
264 auto const& t = jv[jss::transaction];
265 return t[jss::TransactionType] == jss::OfferCreate &&
266 t[jss::TakerGets] ==
267 USD(100).value().getJson(JsonOptions::none) &&
268 t[jss::TakerPays] ==
269 XRP(700).value().getJson(JsonOptions::none);
270 }));
271 }
272
273 {
274 // Create a bid: TakerPays 100/USD, TakerGets 75
275 env(offer("alice", USD(100), XRP(75)), require(owners("alice", 2)));
276 env.close();
277
278 // Check stream update
279 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
280 auto const& t = jv[jss::transaction];
281 return t[jss::TransactionType] == jss::OfferCreate &&
282 t[jss::TakerGets] ==
283 XRP(75).value().getJson(JsonOptions::none) &&
284 t[jss::TakerPays] ==
285 USD(100).value().getJson(JsonOptions::none);
286 }));
287 }
288
289 // RPC unsubscribe
290 auto jv = wsc->invoke("unsubscribe", books);
291 BEAST_EXPECT(jv[jss::status] == "success");
292 if (wsc->version() == 2)
293 {
294 BEAST_EXPECT(
295 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
296 BEAST_EXPECT(
297 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
298 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
299 }
300 }
301
302 void
304 {
305 testcase("Both Sides Offers In Book");
306 using namespace std::chrono_literals;
307 using namespace jtx;
308 Env env(*this);
309 env.fund(XRP(10000), "alice");
310 auto USD = Account("alice")["USD"];
311 auto wsc = makeWSClient(env.app().config());
312 Json::Value books;
313
314 // Create an ask: TakerPays 500, TakerGets 100/USD
315 env(offer("alice", XRP(500), USD(100)), require(owners("alice", 1)));
316
317 // Create a bid: TakerPays 100/USD, TakerGets 200
318 env(offer("alice", USD(100), XRP(200)), require(owners("alice", 2)));
319 env.close();
320
321 {
322 // RPC subscribe to books stream
323 books[jss::books] = Json::arrayValue;
324 {
325 auto& j = books[jss::books].append(Json::objectValue);
326 j[jss::snapshot] = true;
327 j[jss::both] = true;
328 j[jss::taker_gets][jss::currency] = "XRP";
329 j[jss::taker_pays][jss::currency] = "USD";
330 j[jss::taker_pays][jss::issuer] = Account("alice").human();
331 }
332
333 auto jv = wsc->invoke("subscribe", books);
334 if (wsc->version() == 2)
335 {
336 BEAST_EXPECT(
337 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
338 BEAST_EXPECT(
339 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
340 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
341 }
342 if (!BEAST_EXPECT(jv[jss::status] == "success"))
343 return;
344 BEAST_EXPECT(
345 jv[jss::result].isMember(jss::asks) &&
346 jv[jss::result][jss::asks].size() == 1);
347 BEAST_EXPECT(
348 jv[jss::result].isMember(jss::bids) &&
349 jv[jss::result][jss::bids].size() == 1);
350 BEAST_EXPECT(
351 jv[jss::result][jss::asks][0u][jss::TakerGets] ==
352 USD(100).value().getJson(JsonOptions::none));
353 BEAST_EXPECT(
354 jv[jss::result][jss::asks][0u][jss::TakerPays] ==
355 XRP(500).value().getJson(JsonOptions::none));
356 BEAST_EXPECT(
357 jv[jss::result][jss::bids][0u][jss::TakerGets] ==
358 XRP(200).value().getJson(JsonOptions::none));
359 BEAST_EXPECT(
360 jv[jss::result][jss::bids][0u][jss::TakerPays] ==
361 USD(100).value().getJson(JsonOptions::none));
362 BEAST_EXPECT(!jv[jss::result].isMember(jss::offers));
363 }
364
365 {
366 // Create an ask: TakerPays 700, TakerGets 100/USD
367 env(offer("alice", XRP(700), USD(100)),
368 require(owners("alice", 3)));
369 env.close();
370
371 // Check stream update
372 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
373 auto const& t = jv[jss::transaction];
374 return t[jss::TransactionType] == jss::OfferCreate &&
375 t[jss::TakerGets] ==
376 USD(100).value().getJson(JsonOptions::none) &&
377 t[jss::TakerPays] ==
378 XRP(700).value().getJson(JsonOptions::none);
379 }));
380 }
381
382 {
383 // Create a bid: TakerPays 100/USD, TakerGets 75
384 env(offer("alice", USD(100), XRP(75)), require(owners("alice", 4)));
385 env.close();
386
387 // Check stream update
388 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
389 auto const& t = jv[jss::transaction];
390 return t[jss::TransactionType] == jss::OfferCreate &&
391 t[jss::TakerGets] ==
392 XRP(75).value().getJson(JsonOptions::none) &&
393 t[jss::TakerPays] ==
394 USD(100).value().getJson(JsonOptions::none);
395 }));
396 }
397
398 // RPC unsubscribe
399 auto jv = wsc->invoke("unsubscribe", books);
400 BEAST_EXPECT(jv[jss::status] == "success");
401 if (wsc->version() == 2)
402 {
403 BEAST_EXPECT(
404 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
405 BEAST_EXPECT(
406 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
407 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
408 }
409 }
410
411 void
413 {
414 testcase("Multiple Books, One Side Empty");
415 using namespace std::chrono_literals;
416 using namespace jtx;
417 Env env(*this);
418 env.fund(XRP(10000), "alice");
419 auto USD = Account("alice")["USD"];
420 auto CNY = Account("alice")["CNY"];
421 auto JPY = Account("alice")["JPY"];
422 auto wsc = makeWSClient(env.app().config());
423 Json::Value books;
424
425 {
426 // RPC subscribe to books stream
427 books[jss::books] = Json::arrayValue;
428 {
429 auto& j = books[jss::books].append(Json::objectValue);
430 j[jss::snapshot] = true;
431 j[jss::taker_gets][jss::currency] = "XRP";
432 j[jss::taker_pays][jss::currency] = "USD";
433 j[jss::taker_pays][jss::issuer] = Account("alice").human();
434 }
435 {
436 auto& j = books[jss::books].append(Json::objectValue);
437 j[jss::snapshot] = true;
438 j[jss::taker_gets][jss::currency] = "CNY";
439 j[jss::taker_gets][jss::issuer] = Account("alice").human();
440 j[jss::taker_pays][jss::currency] = "JPY";
441 j[jss::taker_pays][jss::issuer] = Account("alice").human();
442 }
443
444 auto jv = wsc->invoke("subscribe", books);
445 if (wsc->version() == 2)
446 {
447 BEAST_EXPECT(
448 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
449 BEAST_EXPECT(
450 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
451 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
452 }
453 if (!BEAST_EXPECT(jv[jss::status] == "success"))
454 return;
455 BEAST_EXPECT(
456 jv[jss::result].isMember(jss::offers) &&
457 jv[jss::result][jss::offers].size() == 0);
458 BEAST_EXPECT(!jv[jss::result].isMember(jss::asks));
459 BEAST_EXPECT(!jv[jss::result].isMember(jss::bids));
460 }
461
462 {
463 // Create an ask: TakerPays 700, TakerGets 100/USD
464 env(offer("alice", XRP(700), USD(100)),
465 require(owners("alice", 1)));
466 env.close();
467
468 // Check stream update
469 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
470 auto const& t = jv[jss::transaction];
471 return t[jss::TransactionType] == jss::OfferCreate &&
472 t[jss::TakerGets] ==
473 USD(100).value().getJson(JsonOptions::none) &&
474 t[jss::TakerPays] ==
475 XRP(700).value().getJson(JsonOptions::none);
476 }));
477 }
478
479 {
480 // Create a bid: TakerPays 100/USD, TakerGets 75
481 env(offer("alice", USD(100), XRP(75)), require(owners("alice", 2)));
482 env.close();
483 BEAST_EXPECT(!wsc->getMsg(10ms));
484 }
485
486 {
487 // Create an ask: TakerPays 700/CNY, TakerGets 100/JPY
488 env(offer("alice", CNY(700), JPY(100)),
489 require(owners("alice", 3)));
490 env.close();
491
492 // Check stream update
493 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
494 auto const& t = jv[jss::transaction];
495 return t[jss::TransactionType] == jss::OfferCreate &&
496 t[jss::TakerGets] ==
497 JPY(100).value().getJson(JsonOptions::none) &&
498 t[jss::TakerPays] ==
499 CNY(700).value().getJson(JsonOptions::none);
500 }));
501 }
502
503 {
504 // Create a bid: TakerPays 100/JPY, TakerGets 75/CNY
505 env(offer("alice", JPY(100), CNY(75)), require(owners("alice", 4)));
506 env.close();
507 BEAST_EXPECT(!wsc->getMsg(10ms));
508 }
509
510 // RPC unsubscribe
511 auto jv = wsc->invoke("unsubscribe", books);
512 BEAST_EXPECT(jv[jss::status] == "success");
513 if (wsc->version() == 2)
514 {
515 BEAST_EXPECT(
516 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
517 BEAST_EXPECT(
518 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
519 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
520 }
521 }
522
523 void
525 {
526 testcase("Multiple Books, One Side Offers In Book");
527 using namespace std::chrono_literals;
528 using namespace jtx;
529 Env env(*this);
530 env.fund(XRP(10000), "alice");
531 auto USD = Account("alice")["USD"];
532 auto CNY = Account("alice")["CNY"];
533 auto JPY = Account("alice")["JPY"];
534 auto wsc = makeWSClient(env.app().config());
535 Json::Value books;
536
537 // Create an ask: TakerPays 500, TakerGets 100/USD
538 env(offer("alice", XRP(500), USD(100)), require(owners("alice", 1)));
539
540 // Create an ask: TakerPays 500/CNY, TakerGets 100/JPY
541 env(offer("alice", CNY(500), JPY(100)), require(owners("alice", 2)));
542
543 // Create a bid: TakerPays 100/USD, TakerGets 200
544 env(offer("alice", USD(100), XRP(200)), require(owners("alice", 3)));
545
546 // Create a bid: TakerPays 100/JPY, TakerGets 200/CNY
547 env(offer("alice", JPY(100), CNY(200)), require(owners("alice", 4)));
548 env.close();
549
550 {
551 // RPC subscribe to books stream
552 books[jss::books] = Json::arrayValue;
553 {
554 auto& j = books[jss::books].append(Json::objectValue);
555 j[jss::snapshot] = true;
556 j[jss::taker_gets][jss::currency] = "XRP";
557 j[jss::taker_pays][jss::currency] = "USD";
558 j[jss::taker_pays][jss::issuer] = Account("alice").human();
559 }
560 {
561 auto& j = books[jss::books].append(Json::objectValue);
562 j[jss::snapshot] = true;
563 j[jss::taker_gets][jss::currency] = "CNY";
564 j[jss::taker_gets][jss::issuer] = Account("alice").human();
565 j[jss::taker_pays][jss::currency] = "JPY";
566 j[jss::taker_pays][jss::issuer] = Account("alice").human();
567 }
568
569 auto jv = wsc->invoke("subscribe", books);
570 if (wsc->version() == 2)
571 {
572 BEAST_EXPECT(
573 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
574 BEAST_EXPECT(
575 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
576 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
577 }
578 if (!BEAST_EXPECT(jv[jss::status] == "success"))
579 return;
580 BEAST_EXPECT(
581 jv[jss::result].isMember(jss::offers) &&
582 jv[jss::result][jss::offers].size() == 2);
583 BEAST_EXPECT(
584 jv[jss::result][jss::offers][0u][jss::TakerGets] ==
585 XRP(200).value().getJson(JsonOptions::none));
586 BEAST_EXPECT(
587 jv[jss::result][jss::offers][0u][jss::TakerPays] ==
588 USD(100).value().getJson(JsonOptions::none));
589 BEAST_EXPECT(
590 jv[jss::result][jss::offers][1u][jss::TakerGets] ==
591 CNY(200).value().getJson(JsonOptions::none));
592 BEAST_EXPECT(
593 jv[jss::result][jss::offers][1u][jss::TakerPays] ==
594 JPY(100).value().getJson(JsonOptions::none));
595 BEAST_EXPECT(!jv[jss::result].isMember(jss::asks));
596 BEAST_EXPECT(!jv[jss::result].isMember(jss::bids));
597 }
598
599 {
600 // Create an ask: TakerPays 700, TakerGets 100/USD
601 env(offer("alice", XRP(700), USD(100)),
602 require(owners("alice", 5)));
603 env.close();
604
605 // Check stream update
606 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
607 auto const& t = jv[jss::transaction];
608 return t[jss::TransactionType] == jss::OfferCreate &&
609 t[jss::TakerGets] ==
610 USD(100).value().getJson(JsonOptions::none) &&
611 t[jss::TakerPays] ==
612 XRP(700).value().getJson(JsonOptions::none);
613 }));
614 }
615
616 {
617 // Create a bid: TakerPays 100/USD, TakerGets 75
618 env(offer("alice", USD(100), XRP(75)), require(owners("alice", 6)));
619 env.close();
620 BEAST_EXPECT(!wsc->getMsg(10ms));
621 }
622
623 {
624 // Create an ask: TakerPays 700/CNY, TakerGets 100/JPY
625 env(offer("alice", CNY(700), JPY(100)),
626 require(owners("alice", 7)));
627 env.close();
628
629 // Check stream update
630 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
631 auto const& t = jv[jss::transaction];
632 return t[jss::TransactionType] == jss::OfferCreate &&
633 t[jss::TakerGets] ==
634 JPY(100).value().getJson(JsonOptions::none) &&
635 t[jss::TakerPays] ==
636 CNY(700).value().getJson(JsonOptions::none);
637 }));
638 }
639
640 {
641 // Create a bid: TakerPays 100/JPY, TakerGets 75/CNY
642 env(offer("alice", JPY(100), CNY(75)), require(owners("alice", 8)));
643 env.close();
644 BEAST_EXPECT(!wsc->getMsg(10ms));
645 }
646
647 // RPC unsubscribe
648 auto jv = wsc->invoke("unsubscribe", books);
649 BEAST_EXPECT(jv[jss::status] == "success");
650 if (wsc->version() == 2)
651 {
652 BEAST_EXPECT(
653 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
654 BEAST_EXPECT(
655 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
656 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
657 }
658 }
659
660 void
662 {
663 testcase("Multiple Books, Both Sides Empty Book");
664 using namespace std::chrono_literals;
665 using namespace jtx;
666 Env env(*this);
667 env.fund(XRP(10000), "alice");
668 auto USD = Account("alice")["USD"];
669 auto CNY = Account("alice")["CNY"];
670 auto JPY = Account("alice")["JPY"];
671 auto wsc = makeWSClient(env.app().config());
672 Json::Value books;
673
674 {
675 // RPC subscribe to books stream
676 books[jss::books] = Json::arrayValue;
677 {
678 auto& j = books[jss::books].append(Json::objectValue);
679 j[jss::snapshot] = true;
680 j[jss::both] = true;
681 j[jss::taker_gets][jss::currency] = "XRP";
682 j[jss::taker_pays][jss::currency] = "USD";
683 j[jss::taker_pays][jss::issuer] = Account("alice").human();
684 }
685 {
686 auto& j = books[jss::books].append(Json::objectValue);
687 j[jss::snapshot] = true;
688 j[jss::both] = true;
689 j[jss::taker_gets][jss::currency] = "CNY";
690 j[jss::taker_gets][jss::issuer] = Account("alice").human();
691 j[jss::taker_pays][jss::currency] = "JPY";
692 j[jss::taker_pays][jss::issuer] = Account("alice").human();
693 }
694
695 auto jv = wsc->invoke("subscribe", books);
696 if (wsc->version() == 2)
697 {
698 BEAST_EXPECT(
699 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
700 BEAST_EXPECT(
701 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
702 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
703 }
704 if (!BEAST_EXPECT(jv[jss::status] == "success"))
705 return;
706 BEAST_EXPECT(
707 jv[jss::result].isMember(jss::asks) &&
708 jv[jss::result][jss::asks].size() == 0);
709 BEAST_EXPECT(
710 jv[jss::result].isMember(jss::bids) &&
711 jv[jss::result][jss::bids].size() == 0);
712 BEAST_EXPECT(!jv[jss::result].isMember(jss::offers));
713 }
714
715 {
716 // Create an ask: TakerPays 700, TakerGets 100/USD
717 env(offer("alice", XRP(700), USD(100)),
718 require(owners("alice", 1)));
719 env.close();
720
721 // Check stream update
722 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
723 auto const& t = jv[jss::transaction];
724 return t[jss::TransactionType] == jss::OfferCreate &&
725 t[jss::TakerGets] ==
726 USD(100).value().getJson(JsonOptions::none) &&
727 t[jss::TakerPays] ==
728 XRP(700).value().getJson(JsonOptions::none);
729 }));
730 }
731
732 {
733 // Create a bid: TakerPays 100/USD, TakerGets 75
734 env(offer("alice", USD(100), XRP(75)), require(owners("alice", 2)));
735 env.close();
736
737 // Check stream update
738 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
739 auto const& t = jv[jss::transaction];
740 return t[jss::TransactionType] == jss::OfferCreate &&
741 t[jss::TakerGets] ==
742 XRP(75).value().getJson(JsonOptions::none) &&
743 t[jss::TakerPays] ==
744 USD(100).value().getJson(JsonOptions::none);
745 }));
746 }
747
748 {
749 // Create an ask: TakerPays 700/CNY, TakerGets 100/JPY
750 env(offer("alice", CNY(700), JPY(100)),
751 require(owners("alice", 3)));
752 env.close();
753
754 // Check stream update
755 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
756 auto const& t = jv[jss::transaction];
757 return t[jss::TransactionType] == jss::OfferCreate &&
758 t[jss::TakerGets] ==
759 JPY(100).value().getJson(JsonOptions::none) &&
760 t[jss::TakerPays] ==
761 CNY(700).value().getJson(JsonOptions::none);
762 }));
763 }
764
765 {
766 // Create a bid: TakerPays 100/JPY, TakerGets 75/CNY
767 env(offer("alice", JPY(100), CNY(75)), require(owners("alice", 4)));
768 env.close();
769
770 // Check stream update
771 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
772 auto const& t = jv[jss::transaction];
773 return t[jss::TransactionType] == jss::OfferCreate &&
774 t[jss::TakerGets] ==
775 CNY(75).value().getJson(JsonOptions::none) &&
776 t[jss::TakerPays] ==
777 JPY(100).value().getJson(JsonOptions::none);
778 }));
779 }
780
781 // RPC unsubscribe
782 auto jv = wsc->invoke("unsubscribe", books);
783 BEAST_EXPECT(jv[jss::status] == "success");
784 if (wsc->version() == 2)
785 {
786 BEAST_EXPECT(
787 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
788 BEAST_EXPECT(
789 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
790 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
791 }
792 }
793
794 void
796 {
797 testcase("Multiple Books, Both Sides Offers In Book");
798 using namespace std::chrono_literals;
799 using namespace jtx;
800 Env env(*this);
801 env.fund(XRP(10000), "alice");
802 auto USD = Account("alice")["USD"];
803 auto CNY = Account("alice")["CNY"];
804 auto JPY = Account("alice")["JPY"];
805 auto wsc = makeWSClient(env.app().config());
806 Json::Value books;
807
808 // Create an ask: TakerPays 500, TakerGets 100/USD
809 env(offer("alice", XRP(500), USD(100)), require(owners("alice", 1)));
810
811 // Create an ask: TakerPays 500/CNY, TakerGets 100/JPY
812 env(offer("alice", CNY(500), JPY(100)), require(owners("alice", 2)));
813
814 // Create a bid: TakerPays 100/USD, TakerGets 200
815 env(offer("alice", USD(100), XRP(200)), require(owners("alice", 3)));
816
817 // Create a bid: TakerPays 100/JPY, TakerGets 200/CNY
818 env(offer("alice", JPY(100), CNY(200)), require(owners("alice", 4)));
819 env.close();
820
821 {
822 // RPC subscribe to books stream
823 books[jss::books] = Json::arrayValue;
824 {
825 auto& j = books[jss::books].append(Json::objectValue);
826 j[jss::snapshot] = true;
827 j[jss::both] = true;
828 j[jss::taker_gets][jss::currency] = "XRP";
829 j[jss::taker_pays][jss::currency] = "USD";
830 j[jss::taker_pays][jss::issuer] = Account("alice").human();
831 }
832 // RPC subscribe to books stream
833 {
834 auto& j = books[jss::books].append(Json::objectValue);
835 j[jss::snapshot] = true;
836 j[jss::both] = true;
837 j[jss::taker_gets][jss::currency] = "CNY";
838 j[jss::taker_gets][jss::issuer] = Account("alice").human();
839 j[jss::taker_pays][jss::currency] = "JPY";
840 j[jss::taker_pays][jss::issuer] = Account("alice").human();
841 }
842
843 auto jv = wsc->invoke("subscribe", books);
844 if (wsc->version() == 2)
845 {
846 BEAST_EXPECT(
847 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
848 BEAST_EXPECT(
849 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
850 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
851 }
852 if (!BEAST_EXPECT(jv[jss::status] == "success"))
853 return;
854 BEAST_EXPECT(
855 jv[jss::result].isMember(jss::asks) &&
856 jv[jss::result][jss::asks].size() == 2);
857 BEAST_EXPECT(
858 jv[jss::result].isMember(jss::bids) &&
859 jv[jss::result][jss::bids].size() == 2);
860 BEAST_EXPECT(
861 jv[jss::result][jss::asks][0u][jss::TakerGets] ==
862 USD(100).value().getJson(JsonOptions::none));
863 BEAST_EXPECT(
864 jv[jss::result][jss::asks][0u][jss::TakerPays] ==
865 XRP(500).value().getJson(JsonOptions::none));
866 BEAST_EXPECT(
867 jv[jss::result][jss::asks][1u][jss::TakerGets] ==
868 JPY(100).value().getJson(JsonOptions::none));
869 BEAST_EXPECT(
870 jv[jss::result][jss::asks][1u][jss::TakerPays] ==
871 CNY(500).value().getJson(JsonOptions::none));
872 BEAST_EXPECT(
873 jv[jss::result][jss::bids][0u][jss::TakerGets] ==
874 XRP(200).value().getJson(JsonOptions::none));
875 BEAST_EXPECT(
876 jv[jss::result][jss::bids][0u][jss::TakerPays] ==
877 USD(100).value().getJson(JsonOptions::none));
878 BEAST_EXPECT(
879 jv[jss::result][jss::bids][1u][jss::TakerGets] ==
880 CNY(200).value().getJson(JsonOptions::none));
881 BEAST_EXPECT(
882 jv[jss::result][jss::bids][1u][jss::TakerPays] ==
883 JPY(100).value().getJson(JsonOptions::none));
884 BEAST_EXPECT(!jv[jss::result].isMember(jss::offers));
885 }
886
887 {
888 // Create an ask: TakerPays 700, TakerGets 100/USD
889 env(offer("alice", XRP(700), USD(100)),
890 require(owners("alice", 5)));
891 env.close();
892
893 // Check stream update
894 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
895 auto const& t = jv[jss::transaction];
896 return t[jss::TransactionType] == jss::OfferCreate &&
897 t[jss::TakerGets] ==
898 USD(100).value().getJson(JsonOptions::none) &&
899 t[jss::TakerPays] ==
900 XRP(700).value().getJson(JsonOptions::none);
901 }));
902 }
903
904 {
905 // Create a bid: TakerPays 100/USD, TakerGets 75
906 env(offer("alice", USD(100), XRP(75)), require(owners("alice", 6)));
907 env.close();
908
909 // Check stream update
910 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
911 auto const& t = jv[jss::transaction];
912 return t[jss::TransactionType] == jss::OfferCreate &&
913 t[jss::TakerGets] ==
914 XRP(75).value().getJson(JsonOptions::none) &&
915 t[jss::TakerPays] ==
916 USD(100).value().getJson(JsonOptions::none);
917 }));
918 }
919
920 {
921 // Create an ask: TakerPays 700/CNY, TakerGets 100/JPY
922 env(offer("alice", CNY(700), JPY(100)),
923 require(owners("alice", 7)));
924 env.close();
925
926 // Check stream update
927 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
928 auto const& t = jv[jss::transaction];
929 return t[jss::TransactionType] == jss::OfferCreate &&
930 t[jss::TakerGets] ==
931 JPY(100).value().getJson(JsonOptions::none) &&
932 t[jss::TakerPays] ==
933 CNY(700).value().getJson(JsonOptions::none);
934 }));
935 }
936
937 {
938 // Create a bid: TakerPays 100/JPY, TakerGets 75/CNY
939 env(offer("alice", JPY(100), CNY(75)), require(owners("alice", 8)));
940 env.close();
941
942 // Check stream update
943 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
944 auto const& t = jv[jss::transaction];
945 return t[jss::TransactionType] == jss::OfferCreate &&
946 t[jss::TakerGets] ==
947 CNY(75).value().getJson(JsonOptions::none) &&
948 t[jss::TakerPays] ==
949 JPY(100).value().getJson(JsonOptions::none);
950 }));
951 }
952
953 // RPC unsubscribe
954 auto jv = wsc->invoke("unsubscribe", books);
955 BEAST_EXPECT(jv[jss::status] == "success");
956 if (wsc->version() == 2)
957 {
958 BEAST_EXPECT(
959 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
960 BEAST_EXPECT(
961 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
962 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
963 }
964 }
965
966 void
968 {
969 testcase("TrackOffers");
970 using namespace jtx;
971 Env env(*this);
972 Account gw{"gw"};
973 Account alice{"alice"};
974 Account bob{"bob"};
975 auto wsc = makeWSClient(env.app().config());
976 env.fund(XRP(20000), alice, bob, gw);
977 env.close();
978 auto USD = gw["USD"];
979
980 Json::Value books;
981 {
982 books[jss::books] = Json::arrayValue;
983 {
984 auto& j = books[jss::books].append(Json::objectValue);
985 j[jss::snapshot] = true;
986 j[jss::taker_gets][jss::currency] = "XRP";
987 j[jss::taker_pays][jss::currency] = "USD";
988 j[jss::taker_pays][jss::issuer] = gw.human();
989 }
990
991 auto jv = wsc->invoke("subscribe", books);
992 if (wsc->version() == 2)
993 {
994 BEAST_EXPECT(
995 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
996 BEAST_EXPECT(
997 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
998 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
999 }
1000 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1001 return;
1002 BEAST_EXPECT(
1003 jv[jss::result].isMember(jss::offers) &&
1004 jv[jss::result][jss::offers].size() == 0);
1005 BEAST_EXPECT(!jv[jss::result].isMember(jss::asks));
1006 BEAST_EXPECT(!jv[jss::result].isMember(jss::bids));
1007 }
1008
1009 env(rate(gw, 1.1));
1010 env.close();
1011 env.trust(USD(1000), alice);
1012 env.trust(USD(1000), bob);
1013 env(pay(gw, alice, USD(100)));
1014 env(pay(gw, bob, USD(50)));
1015 env(offer(alice, XRP(4000), USD(10)));
1016 env.close();
1017
1018 Json::Value jvParams;
1019 jvParams[jss::taker] = env.master.human();
1020 jvParams[jss::taker_pays][jss::currency] = "XRP";
1021 jvParams[jss::ledger_index] = "validated";
1022 jvParams[jss::taker_gets][jss::currency] = "USD";
1023 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1024
1025 auto jv = wsc->invoke("book_offers", jvParams);
1026 if (wsc->version() == 2)
1027 {
1028 BEAST_EXPECT(
1029 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
1030 BEAST_EXPECT(
1031 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
1032 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
1033 }
1034 auto jrr = jv[jss::result];
1035
1036 BEAST_EXPECT(jrr[jss::offers].isArray());
1037 BEAST_EXPECT(jrr[jss::offers].size() == 1);
1038 auto const jrOffer = jrr[jss::offers][0u];
1039 BEAST_EXPECT(jrOffer[sfAccount.fieldName] == alice.human());
1040 BEAST_EXPECT(
1041 jrOffer[sfBookDirectory.fieldName] ==
1042 getBookDir(env, XRP, USD.issue()));
1043 BEAST_EXPECT(jrOffer[sfBookNode.fieldName] == "0");
1044 BEAST_EXPECT(jrOffer[jss::Flags] == 0);
1045 BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer);
1046 BEAST_EXPECT(jrOffer[sfOwnerNode.fieldName] == "0");
1047 BEAST_EXPECT(jrOffer[sfSequence.fieldName] == 5);
1048 BEAST_EXPECT(
1049 jrOffer[jss::TakerGets] ==
1050 USD(10).value().getJson(JsonOptions::none));
1051 BEAST_EXPECT(
1052 jrOffer[jss::TakerPays] ==
1053 XRP(4000).value().getJson(JsonOptions::none));
1054 BEAST_EXPECT(jrOffer[jss::owner_funds] == "100");
1055 BEAST_EXPECT(jrOffer[jss::quality] == "400000000");
1056
1057 using namespace std::chrono_literals;
1058 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jval) {
1059 auto const& t = jval[jss::transaction];
1060 return t[jss::TransactionType] == jss::OfferCreate &&
1061 t[jss::TakerGets] ==
1062 USD(10).value().getJson(JsonOptions::none) &&
1063 t[jss::owner_funds] == "100" &&
1064 t[jss::TakerPays] ==
1065 XRP(4000).value().getJson(JsonOptions::none);
1066 }));
1067
1068 env(offer(bob, XRP(2000), USD(5)));
1069 env.close();
1070
1071 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jval) {
1072 auto const& t = jval[jss::transaction];
1073 return t[jss::TransactionType] == jss::OfferCreate &&
1074 t[jss::TakerGets] ==
1075 USD(5).value().getJson(JsonOptions::none) &&
1076 t[jss::owner_funds] == "50" &&
1077 t[jss::TakerPays] ==
1078 XRP(2000).value().getJson(JsonOptions::none);
1079 }));
1080
1081 jv = wsc->invoke("book_offers", jvParams);
1082 if (wsc->version() == 2)
1083 {
1084 BEAST_EXPECT(
1085 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
1086 BEAST_EXPECT(
1087 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
1088 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
1089 }
1090 jrr = jv[jss::result];
1091
1092 BEAST_EXPECT(jrr[jss::offers].isArray());
1093 BEAST_EXPECT(jrr[jss::offers].size() == 2);
1094 auto const jrNextOffer = jrr[jss::offers][1u];
1095 BEAST_EXPECT(jrNextOffer[sfAccount.fieldName] == bob.human());
1096 BEAST_EXPECT(
1097 jrNextOffer[sfBookDirectory.fieldName] ==
1098 getBookDir(env, XRP, USD.issue()));
1099 BEAST_EXPECT(jrNextOffer[sfBookNode.fieldName] == "0");
1100 BEAST_EXPECT(jrNextOffer[jss::Flags] == 0);
1101 BEAST_EXPECT(jrNextOffer[sfLedgerEntryType.fieldName] == jss::Offer);
1102 BEAST_EXPECT(jrNextOffer[sfOwnerNode.fieldName] == "0");
1103 BEAST_EXPECT(jrNextOffer[sfSequence.fieldName] == 5);
1104 BEAST_EXPECT(
1105 jrNextOffer[jss::TakerGets] ==
1106 USD(5).value().getJson(JsonOptions::none));
1107 BEAST_EXPECT(
1108 jrNextOffer[jss::TakerPays] ==
1109 XRP(2000).value().getJson(JsonOptions::none));
1110 BEAST_EXPECT(jrNextOffer[jss::owner_funds] == "50");
1111 BEAST_EXPECT(jrNextOffer[jss::quality] == "400000000");
1112
1113 jv = wsc->invoke("unsubscribe", books);
1114 if (wsc->version() == 2)
1115 {
1116 BEAST_EXPECT(
1117 jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
1118 BEAST_EXPECT(
1119 jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
1120 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
1121 }
1122 BEAST_EXPECT(jv[jss::status] == "success");
1123 }
1124
1125 // Check that a stream only sees the given OfferCreate once
1126 static bool
1128 std::unique_ptr<WSClient> const& wsc,
1129 std::chrono::milliseconds const& timeout,
1130 jtx::PrettyAmount const& takerGets,
1131 jtx::PrettyAmount const& takerPays)
1132 {
1133 auto maybeJv = wsc->getMsg(timeout);
1134 // No message
1135 if (!maybeJv)
1136 return false;
1137 // wrong message
1138 if (!(*maybeJv).isMember(jss::transaction))
1139 return false;
1140 auto const& t = (*maybeJv)[jss::transaction];
1141 if (t[jss::TransactionType] != jss::OfferCreate ||
1142 t[jss::TakerGets] != takerGets.value().getJson(JsonOptions::none) ||
1143 t[jss::TakerPays] != takerPays.value().getJson(JsonOptions::none))
1144 return false;
1145 // Make sure no other message is waiting
1146 return wsc->getMsg(timeout) == std::nullopt;
1147 }
1148
1149 void
1151 {
1152 testcase("Crossing single book offer");
1153
1154 // This was added to check that an OfferCreate transaction is only
1155 // published once in a stream, even if it updates multiple offer
1156 // ledger entries
1157
1158 using namespace jtx;
1159 Env env(*this);
1160
1161 // Scenario is:
1162 // - Alice and Bob place identical offers for USD -> XRP
1163 // - Charlie places a crossing order that takes both Alice and Bob's
1164
1165 auto const gw = Account("gateway");
1166 auto const alice = Account("alice");
1167 auto const bob = Account("bob");
1168 auto const charlie = Account("charlie");
1169 auto const USD = gw["USD"];
1170
1171 env.fund(XRP(1000000), gw, alice, bob, charlie);
1172 env.close();
1173
1174 env(trust(alice, USD(500)));
1175 env(trust(bob, USD(500)));
1176 env.close();
1177
1178 env(pay(gw, alice, USD(500)));
1179 env(pay(gw, bob, USD(500)));
1180 env.close();
1181
1182 // Alice and Bob offer $500 for 500 XRP
1183 env(offer(alice, XRP(500), USD(500)));
1184 env(offer(bob, XRP(500), USD(500)));
1185 env.close();
1186
1187 auto wsc = makeWSClient(env.app().config());
1188 Json::Value books;
1189 {
1190 // RPC subscribe to books stream
1191 books[jss::books] = Json::arrayValue;
1192 {
1193 auto& j = books[jss::books].append(Json::objectValue);
1194 j[jss::snapshot] = false;
1195 j[jss::taker_gets][jss::currency] = "XRP";
1196 j[jss::taker_pays][jss::currency] = "USD";
1197 j[jss::taker_pays][jss::issuer] = gw.human();
1198 }
1199
1200 auto jv = wsc->invoke("subscribe", books);
1201 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1202 return;
1203 }
1204
1205 // Charlie places an offer that crosses Alice and Charlie's offers
1206 env(offer(charlie, USD(1000), XRP(1000)));
1207 env.close();
1208 env.require(offers(alice, 0), offers(bob, 0), offers(charlie, 0));
1209 using namespace std::chrono_literals;
1210 BEAST_EXPECT(offerOnlyOnceInStream(wsc, 1s, XRP(1000), USD(1000)));
1211
1212 // RPC unsubscribe
1213 auto jv = wsc->invoke("unsubscribe", books);
1214 BEAST_EXPECT(jv[jss::status] == "success");
1215 }
1216
1217 void
1219 {
1220 testcase("Crossing multi-book offer");
1221
1222 // This was added to check that an OfferCreate transaction is only
1223 // published once in a stream, even if it auto-bridges across several
1224 // books that are under subscription
1225
1226 using namespace jtx;
1227 Env env(*this);
1228
1229 // Scenario is:
1230 // - Alice has 1 USD and wants 100 XRP
1231 // - Bob has 100 XRP and wants 1 EUR
1232 // - Charlie has 1 EUR and wants 1 USD and should auto-bridge through
1233 // Alice and Bob
1234
1235 auto const gw = Account("gateway");
1236 auto const alice = Account("alice");
1237 auto const bob = Account("bob");
1238 auto const charlie = Account("charlie");
1239 auto const USD = gw["USD"];
1240 auto const EUR = gw["EUR"];
1241
1242 env.fund(XRP(1000000), gw, alice, bob, charlie);
1243 env.close();
1244
1245 for (auto const& account : {alice, bob, charlie})
1246 {
1247 for (auto const& iou : {USD, EUR})
1248 {
1249 env(trust(account, iou(1)));
1250 }
1251 }
1252 env.close();
1253
1254 env(pay(gw, alice, USD(1)));
1255 env(pay(gw, charlie, EUR(1)));
1256 env.close();
1257
1258 env(offer(alice, XRP(100), USD(1)));
1259 env(offer(bob, EUR(1), XRP(100)));
1260 env.close();
1261
1262 auto wsc = makeWSClient(env.app().config());
1263 Json::Value books;
1264
1265 {
1266 // RPC subscribe to multiple book streams
1267 books[jss::books] = Json::arrayValue;
1268 {
1269 auto& j = books[jss::books].append(Json::objectValue);
1270 j[jss::snapshot] = false;
1271 j[jss::taker_gets][jss::currency] = "XRP";
1272 j[jss::taker_pays][jss::currency] = "USD";
1273 j[jss::taker_pays][jss::issuer] = gw.human();
1274 }
1275
1276 {
1277 auto& j = books[jss::books].append(Json::objectValue);
1278 j[jss::snapshot] = false;
1279 j[jss::taker_gets][jss::currency] = "EUR";
1280 j[jss::taker_gets][jss::issuer] = gw.human();
1281 j[jss::taker_pays][jss::currency] = "XRP";
1282 }
1283
1284 auto jv = wsc->invoke("subscribe", books);
1285 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1286 return;
1287 }
1288
1289 // Charlies places an on offer for EUR -> USD that should auto-bridge
1290 env(offer(charlie, USD(1), EUR(1)));
1291 env.close();
1292 using namespace std::chrono_literals;
1293 BEAST_EXPECT(offerOnlyOnceInStream(wsc, 1s, EUR(1), USD(1)));
1294
1295 // RPC unsubscribe
1296 auto jv = wsc->invoke("unsubscribe", books);
1297 BEAST_EXPECT(jv[jss::status] == "success");
1298 }
1299
1300 void
1302 {
1303 testcase("BookOffersRPC Errors");
1304 using namespace jtx;
1305 Env env(*this);
1306 Account gw{"gw"};
1307 Account alice{"alice"};
1308 env.fund(XRP(10000), alice, gw);
1309 env.close();
1310 auto USD = gw["USD"];
1311
1312 {
1313 Json::Value jvParams;
1314 jvParams[jss::ledger_index] = 10u;
1315 auto const jrr = env.rpc(
1316 "json", "book_offers", to_string(jvParams))[jss::result];
1317 BEAST_EXPECT(jrr[jss::error] == "lgrNotFound");
1318 BEAST_EXPECT(jrr[jss::error_message] == "ledgerNotFound");
1319 }
1320
1321 {
1322 Json::Value jvParams;
1323 jvParams[jss::ledger_index] = "validated";
1324 auto const jrr = env.rpc(
1325 "json", "book_offers", to_string(jvParams))[jss::result];
1326 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1327 BEAST_EXPECT(
1328 jrr[jss::error_message] == "Missing field 'taker_pays'.");
1329 }
1330
1331 {
1332 Json::Value jvParams;
1333 jvParams[jss::ledger_index] = "validated";
1334 jvParams[jss::taker_pays] = Json::objectValue;
1335 auto const jrr = env.rpc(
1336 "json", "book_offers", to_string(jvParams))[jss::result];
1337 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1338 BEAST_EXPECT(
1339 jrr[jss::error_message] == "Missing field 'taker_gets'.");
1340 }
1341
1342 {
1343 Json::Value jvParams;
1344 jvParams[jss::ledger_index] = "validated";
1345 jvParams[jss::taker_pays] = "not an object";
1346 jvParams[jss::taker_gets] = Json::objectValue;
1347 auto const jrr = env.rpc(
1348 "json", "book_offers", to_string(jvParams))[jss::result];
1349 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1350 BEAST_EXPECT(
1351 jrr[jss::error_message] ==
1352 "Invalid field 'taker_pays', not object.");
1353 }
1354
1355 {
1356 Json::Value jvParams;
1357 jvParams[jss::ledger_index] = "validated";
1358 jvParams[jss::taker_pays] = Json::objectValue;
1359 jvParams[jss::taker_gets] = "not an object";
1360 auto const jrr = env.rpc(
1361 "json", "book_offers", to_string(jvParams))[jss::result];
1362 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1363 BEAST_EXPECT(
1364 jrr[jss::error_message] ==
1365 "Invalid field 'taker_gets', not object.");
1366 }
1367
1368 {
1369 Json::Value jvParams;
1370 jvParams[jss::ledger_index] = "validated";
1371 jvParams[jss::taker_pays] = Json::objectValue;
1372 jvParams[jss::taker_gets] = Json::objectValue;
1373 auto const jrr = env.rpc(
1374 "json", "book_offers", to_string(jvParams))[jss::result];
1375 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1376 BEAST_EXPECT(
1377 jrr[jss::error_message] ==
1378 "Missing field 'taker_pays.currency'.");
1379 }
1380
1381 {
1382 Json::Value jvParams;
1383 jvParams[jss::ledger_index] = "validated";
1384 jvParams[jss::taker_pays][jss::currency] = 1;
1385 jvParams[jss::taker_gets] = Json::objectValue;
1386 auto const jrr = env.rpc(
1387 "json", "book_offers", to_string(jvParams))[jss::result];
1388 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1389 BEAST_EXPECT(
1390 jrr[jss::error_message] ==
1391 "Invalid field 'taker_pays.currency', not string.");
1392 }
1393
1394 {
1395 Json::Value jvParams;
1396 jvParams[jss::ledger_index] = "validated";
1397 jvParams[jss::taker_pays][jss::currency] = "XRP";
1398 jvParams[jss::taker_gets] = Json::objectValue;
1399 auto const jrr = env.rpc(
1400 "json", "book_offers", to_string(jvParams))[jss::result];
1401 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1402 BEAST_EXPECT(
1403 jrr[jss::error_message] ==
1404 "Missing field 'taker_gets.currency'.");
1405 }
1406
1407 {
1408 Json::Value jvParams;
1409 jvParams[jss::ledger_index] = "validated";
1410 jvParams[jss::taker_pays][jss::currency] = "XRP";
1411 jvParams[jss::taker_gets][jss::currency] = 1;
1412 auto const jrr = env.rpc(
1413 "json", "book_offers", to_string(jvParams))[jss::result];
1414 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1415 BEAST_EXPECT(
1416 jrr[jss::error_message] ==
1417 "Invalid field 'taker_gets.currency', not string.");
1418 }
1419
1420 {
1421 Json::Value jvParams;
1422 jvParams[jss::ledger_index] = "validated";
1423 jvParams[jss::taker_pays][jss::currency] = "NOT_VALID";
1424 jvParams[jss::taker_gets][jss::currency] = "XRP";
1425 auto const jrr = env.rpc(
1426 "json", "book_offers", to_string(jvParams))[jss::result];
1427 BEAST_EXPECT(jrr[jss::error] == "srcCurMalformed");
1428 BEAST_EXPECT(
1429 jrr[jss::error_message] ==
1430 "Invalid field 'taker_pays.currency', bad currency.");
1431 }
1432
1433 {
1434 Json::Value jvParams;
1435 jvParams[jss::ledger_index] = "validated";
1436 jvParams[jss::taker_pays][jss::currency] = "XRP";
1437 jvParams[jss::taker_gets][jss::currency] = "NOT_VALID";
1438 auto const jrr = env.rpc(
1439 "json", "book_offers", to_string(jvParams))[jss::result];
1440 BEAST_EXPECT(jrr[jss::error] == "dstAmtMalformed");
1441 BEAST_EXPECT(
1442 jrr[jss::error_message] ==
1443 "Invalid field 'taker_gets.currency', bad currency.");
1444 }
1445
1446 {
1447 Json::Value jvParams;
1448 jvParams[jss::ledger_index] = "validated";
1449 jvParams[jss::taker_pays][jss::currency] = "XRP";
1450 jvParams[jss::taker_gets][jss::currency] = "USD";
1451 jvParams[jss::taker_gets][jss::issuer] = 1;
1452 auto const jrr = env.rpc(
1453 "json", "book_offers", to_string(jvParams))[jss::result];
1454 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1455 BEAST_EXPECT(
1456 jrr[jss::error_message] ==
1457 "Invalid field 'taker_gets.issuer', not string.");
1458 }
1459
1460 {
1461 Json::Value jvParams;
1462 jvParams[jss::ledger_index] = "validated";
1463 jvParams[jss::taker_pays][jss::currency] = "XRP";
1464 jvParams[jss::taker_pays][jss::issuer] = 1;
1465 jvParams[jss::taker_gets][jss::currency] = "USD";
1466 auto const jrr = env.rpc(
1467 "json", "book_offers", to_string(jvParams))[jss::result];
1468 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1469 BEAST_EXPECT(
1470 jrr[jss::error_message] ==
1471 "Invalid field 'taker_pays.issuer', not string.");
1472 }
1473
1474 {
1475 Json::Value jvParams;
1476 jvParams[jss::ledger_index] = "validated";
1477 jvParams[jss::taker_pays][jss::currency] = "XRP";
1478 jvParams[jss::taker_pays][jss::issuer] = gw.human() + "DEAD";
1479 jvParams[jss::taker_gets][jss::currency] = "USD";
1480 auto const jrr = env.rpc(
1481 "json", "book_offers", to_string(jvParams))[jss::result];
1482 BEAST_EXPECT(jrr[jss::error] == "srcIsrMalformed");
1483 BEAST_EXPECT(
1484 jrr[jss::error_message] ==
1485 "Invalid field 'taker_pays.issuer', bad issuer.");
1486 }
1487
1488 {
1489 Json::Value jvParams;
1490 jvParams[jss::ledger_index] = "validated";
1491 jvParams[jss::taker_pays][jss::currency] = "XRP";
1492 jvParams[jss::taker_pays][jss::issuer] = toBase58(noAccount());
1493 jvParams[jss::taker_gets][jss::currency] = "USD";
1494 auto const jrr = env.rpc(
1495 "json", "book_offers", to_string(jvParams))[jss::result];
1496 BEAST_EXPECT(jrr[jss::error] == "srcIsrMalformed");
1497 BEAST_EXPECT(
1498 jrr[jss::error_message] ==
1499 "Invalid field 'taker_pays.issuer', bad issuer account one.");
1500 }
1501
1502 {
1503 Json::Value jvParams;
1504 jvParams[jss::ledger_index] = "validated";
1505 jvParams[jss::taker_pays][jss::currency] = "XRP";
1506 jvParams[jss::taker_gets][jss::currency] = "USD";
1507 jvParams[jss::taker_gets][jss::issuer] = gw.human() + "DEAD";
1508 auto const jrr = env.rpc(
1509 "json", "book_offers", to_string(jvParams))[jss::result];
1510 BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
1511 BEAST_EXPECT(
1512 jrr[jss::error_message] ==
1513 "Invalid field 'taker_gets.issuer', bad issuer.");
1514 }
1515
1516 {
1517 Json::Value jvParams;
1518 jvParams[jss::ledger_index] = "validated";
1519 jvParams[jss::taker_pays][jss::currency] = "XRP";
1520 jvParams[jss::taker_gets][jss::currency] = "USD";
1521 jvParams[jss::taker_gets][jss::issuer] = toBase58(noAccount());
1522 auto const jrr = env.rpc(
1523 "json", "book_offers", to_string(jvParams))[jss::result];
1524 BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
1525 BEAST_EXPECT(
1526 jrr[jss::error_message] ==
1527 "Invalid field 'taker_gets.issuer', bad issuer account one.");
1528 }
1529
1530 {
1531 Json::Value jvParams;
1532 jvParams[jss::ledger_index] = "validated";
1533 jvParams[jss::taker_pays][jss::currency] = "XRP";
1534 jvParams[jss::taker_pays][jss::issuer] = alice.human();
1535 jvParams[jss::taker_gets][jss::currency] = "USD";
1536 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1537 auto const jrr = env.rpc(
1538 "json", "book_offers", to_string(jvParams))[jss::result];
1539 BEAST_EXPECT(jrr[jss::error] == "srcIsrMalformed");
1540 BEAST_EXPECT(
1541 jrr[jss::error_message] ==
1542 "Unneeded field 'taker_pays.issuer' "
1543 "for XRP currency specification.");
1544 }
1545
1546 {
1547 Json::Value jvParams;
1548 jvParams[jss::ledger_index] = "validated";
1549 jvParams[jss::taker_pays][jss::currency] = "USD";
1550 jvParams[jss::taker_pays][jss::issuer] = toBase58(xrpAccount());
1551 jvParams[jss::taker_gets][jss::currency] = "USD";
1552 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1553 auto const jrr = env.rpc(
1554 "json", "book_offers", to_string(jvParams))[jss::result];
1555 BEAST_EXPECT(jrr[jss::error] == "srcIsrMalformed");
1556 BEAST_EXPECT(
1557 jrr[jss::error_message] ==
1558 "Invalid field 'taker_pays.issuer', expected non-XRP issuer.");
1559 }
1560
1561 {
1562 Json::Value jvParams;
1563 jvParams[jss::ledger_index] = "validated";
1564 jvParams[jss::taker] = 1;
1565 jvParams[jss::taker_pays][jss::currency] = "XRP";
1566 jvParams[jss::taker_gets][jss::currency] = "USD";
1567 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1568 auto const jrr = env.rpc(
1569 "json", "book_offers", to_string(jvParams))[jss::result];
1570 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1571 BEAST_EXPECT(
1572 jrr[jss::error_message] ==
1573 "Invalid field 'taker', not string.");
1574 }
1575
1576 {
1577 Json::Value jvParams;
1578 jvParams[jss::ledger_index] = "validated";
1579 jvParams[jss::taker] = env.master.human() + "DEAD";
1580 jvParams[jss::taker_pays][jss::currency] = "XRP";
1581 jvParams[jss::taker_gets][jss::currency] = "USD";
1582 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1583 auto const jrr = env.rpc(
1584 "json", "book_offers", to_string(jvParams))[jss::result];
1585 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1586 BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'taker'.");
1587 }
1588
1589 {
1590 Json::Value jvParams;
1591 jvParams[jss::ledger_index] = "validated";
1592 jvParams[jss::taker] = env.master.human();
1593 jvParams[jss::taker_pays][jss::currency] = "USD";
1594 jvParams[jss::taker_pays][jss::issuer] = gw.human();
1595 jvParams[jss::taker_gets][jss::currency] = "USD";
1596 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1597 auto const jrr = env.rpc(
1598 "json", "book_offers", to_string(jvParams))[jss::result];
1599 BEAST_EXPECT(jrr[jss::error] == "badMarket");
1600 BEAST_EXPECT(jrr[jss::error_message] == "No such market.");
1601 }
1602
1603 {
1604 Json::Value jvParams;
1605 jvParams[jss::ledger_index] = "validated";
1606 jvParams[jss::taker] = env.master.human();
1607 jvParams[jss::limit] = "0"; // NOT an integer
1608 jvParams[jss::taker_pays][jss::currency] = "XRP";
1609 jvParams[jss::taker_gets][jss::currency] = "USD";
1610 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1611 auto const jrr = env.rpc(
1612 "json", "book_offers", to_string(jvParams))[jss::result];
1613 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1614 BEAST_EXPECT(
1615 jrr[jss::error_message] ==
1616 "Invalid field 'limit', not unsigned integer.");
1617 }
1618
1619 {
1620 Json::Value jvParams;
1621 jvParams[jss::ledger_index] = "validated";
1622 jvParams[jss::taker] = env.master.human();
1623 jvParams[jss::limit] = 0; // must be > 0
1624 jvParams[jss::taker_pays][jss::currency] = "XRP";
1625 jvParams[jss::taker_gets][jss::currency] = "USD";
1626 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1627 auto const jrr = env.rpc(
1628 "json", "book_offers", to_string(jvParams))[jss::result];
1629 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
1630 BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit'.");
1631 }
1632
1633 {
1634 Json::Value jvParams;
1635 jvParams[jss::ledger_index] = "validated";
1636 jvParams[jss::taker_pays][jss::currency] = "USD";
1637 jvParams[jss::taker_pays][jss::issuer] = gw.human();
1638 jvParams[jss::taker_gets][jss::currency] = "USD";
1639 auto const jrr = env.rpc(
1640 "json", "book_offers", to_string(jvParams))[jss::result];
1641 BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
1642 BEAST_EXPECT(
1643 jrr[jss::error_message] ==
1644 "Invalid field 'taker_gets.issuer', "
1645 "expected non-XRP issuer.");
1646 }
1647
1648 {
1649 Json::Value jvParams;
1650 jvParams[jss::ledger_index] = "validated";
1651 jvParams[jss::taker_pays][jss::currency] = "USD";
1652 jvParams[jss::taker_pays][jss::issuer] = gw.human();
1653 jvParams[jss::taker_gets][jss::currency] = "XRP";
1654 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1655 auto const jrr = env.rpc(
1656 "json", "book_offers", to_string(jvParams))[jss::result];
1657 BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
1658 BEAST_EXPECT(
1659 jrr[jss::error_message] ==
1660 "Unneeded field 'taker_gets.issuer' "
1661 "for XRP currency specification.");
1662 }
1663 {
1664 Json::Value jvParams;
1665 jvParams[jss::ledger_index] = "validated";
1666 jvParams[jss::taker_pays][jss::currency] = "USD";
1667 jvParams[jss::taker_pays][jss::issuer] = gw.human();
1668 jvParams[jss::taker_gets][jss::currency] = "EUR";
1669 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1670 jvParams[jss::domain] = "badString";
1671 auto const jrr = env.rpc(
1672 "json", "book_offers", to_string(jvParams))[jss::result];
1673 BEAST_EXPECT(jrr[jss::error] == "domainMalformed");
1674 BEAST_EXPECT(jrr[jss::error_message] == "Unable to parse domain.");
1675 }
1676 }
1677
1678 void
1680 {
1681 testcase("BookOffer Limits");
1682 using namespace jtx;
1683 Env env{*this, asAdmin ? envconfig() : envconfig(no_admin)};
1684 Account gw{"gw"};
1685 env.fund(XRP(200000), gw);
1686 // Note that calls to env.close() fail without admin permission.
1687 if (asAdmin)
1688 env.close();
1689
1690 auto USD = gw["USD"];
1691
1692 for (auto i = 0; i <= RPC::Tuning::bookOffers.rmax; i++)
1693 env(offer(gw, XRP(50 + 1 * i), USD(1.0 + 0.1 * i)));
1694
1695 if (asAdmin)
1696 env.close();
1697
1698 Json::Value jvParams;
1699 jvParams[jss::limit] = 1;
1700 jvParams[jss::ledger_index] = "validated";
1701 jvParams[jss::taker_pays][jss::currency] = "XRP";
1702 jvParams[jss::taker_gets][jss::currency] = "USD";
1703 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1704 auto jrr =
1705 env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
1706 BEAST_EXPECT(jrr[jss::offers].isArray());
1707 BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? 1u : 0u));
1708 // NOTE - a marker field is not returned for this method
1709
1710 jvParams[jss::limit] = RPC::Tuning::bookOffers.rmax + 1;
1711 jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
1712 BEAST_EXPECT(jrr[jss::offers].isArray());
1713 BEAST_EXPECT(
1714 jrr[jss::offers].size() ==
1715 (asAdmin ? RPC::Tuning::bookOffers.rmax + 1 : 0u));
1716
1717 jvParams[jss::limit] = Json::nullValue;
1718 jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
1719 BEAST_EXPECT(jrr[jss::offers].isArray());
1720 BEAST_EXPECT(
1721 jrr[jss::offers].size() ==
1722 (asAdmin ? RPC::Tuning::bookOffers.rdefault : 0u));
1723 }
1724
1725 void
1727 {
1728 testcase("TrackDomainOffer");
1729 using namespace jtx;
1730
1731 FeatureBitset const all{
1732 jtx::testable_amendments() | featurePermissionedDomains |
1733 featureCredentials | featurePermissionedDEX};
1734
1735 Env env(*this, all);
1736 PermissionedDEX permDex(env);
1737 auto const alice = permDex.alice;
1738 auto const bob = permDex.bob;
1739 auto const carol = permDex.carol;
1740 auto const domainID = permDex.domainID;
1741 auto const gw = permDex.gw;
1742 auto const USD = permDex.USD;
1743
1744 auto wsc = makeWSClient(env.app().config());
1745
1746 env(offer(alice, XRP(10), USD(10)), domain(domainID));
1747 env.close();
1748
1749 auto checkBookOffers = [&](Json::Value const& jrr) {
1750 BEAST_EXPECT(jrr[jss::offers].isArray());
1751 BEAST_EXPECT(jrr[jss::offers].size() == 1);
1752 auto const jrOffer = jrr[jss::offers][0u];
1753 BEAST_EXPECT(jrOffer[sfAccount.fieldName] == alice.human());
1754 BEAST_EXPECT(
1755 jrOffer[sfBookDirectory.fieldName] ==
1756 getBookDir(env, XRP, USD.issue(), domainID));
1757 BEAST_EXPECT(jrOffer[sfBookNode.fieldName] == "0");
1758 BEAST_EXPECT(jrOffer[jss::Flags] == 0);
1759 BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer);
1760 BEAST_EXPECT(jrOffer[sfOwnerNode.fieldName] == "0");
1761 BEAST_EXPECT(
1762 jrOffer[jss::TakerGets] ==
1763 USD(10).value().getJson(JsonOptions::none));
1764 BEAST_EXPECT(
1765 jrOffer[jss::TakerPays] ==
1766 XRP(10).value().getJson(JsonOptions::none));
1767 BEAST_EXPECT(
1768 jrOffer[sfDomainID.jsonName].asString() == to_string(domainID));
1769 };
1770
1771 // book_offers: open book doesn't return offer
1772 {
1773 Json::Value jvParams;
1774 jvParams[jss::taker] = env.master.human();
1775 jvParams[jss::taker_pays][jss::currency] = "XRP";
1776 jvParams[jss::ledger_index] = "validated";
1777 jvParams[jss::taker_gets][jss::currency] = "USD";
1778 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1779
1780 auto jv = wsc->invoke("book_offers", jvParams);
1781 auto jrr = jv[jss::result];
1782 BEAST_EXPECT(jrr[jss::offers].isArray());
1783 BEAST_EXPECT(jrr[jss::offers].size() == 0);
1784 }
1785
1786 auto checkSubBooks = [&](Json::Value const& jv) {
1787 BEAST_EXPECT(
1788 jv[jss::result].isMember(jss::offers) &&
1789 jv[jss::result][jss::offers].size() == 1);
1790 BEAST_EXPECT(
1791 jv[jss::result][jss::offers][0u][jss::TakerGets] ==
1792 USD(10).value().getJson(JsonOptions::none));
1793 BEAST_EXPECT(
1794 jv[jss::result][jss::offers][0u][jss::TakerPays] ==
1795 XRP(10).value().getJson(JsonOptions::none));
1796 BEAST_EXPECT(
1797 jv[jss::result][jss::offers][0u][sfDomainID.jsonName]
1798 .asString() == to_string(domainID));
1799 };
1800
1801 // book_offers: requesting domain book returns hybrid offer
1802 {
1803 Json::Value jvParams;
1804 jvParams[jss::taker] = env.master.human();
1805 jvParams[jss::taker_pays][jss::currency] = "XRP";
1806 jvParams[jss::ledger_index] = "validated";
1807 jvParams[jss::taker_gets][jss::currency] = "USD";
1808 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1809 jvParams[jss::domain] = to_string(domainID);
1810
1811 auto jv = wsc->invoke("book_offers", jvParams);
1812 auto jrr = jv[jss::result];
1813 checkBookOffers(jrr);
1814 }
1815
1816 // subscribe to domain book should return domain offer
1817 {
1818 Json::Value books;
1819 books[jss::books] = Json::arrayValue;
1820 {
1821 auto& j = books[jss::books].append(Json::objectValue);
1822 j[jss::snapshot] = true;
1823 j[jss::taker_pays][jss::currency] = "XRP";
1824 j[jss::taker_gets][jss::currency] = "USD";
1825 j[jss::taker_gets][jss::issuer] = gw.human();
1826 j[jss::domain] = to_string(domainID);
1827 }
1828
1829 auto jv = wsc->invoke("subscribe", books);
1830 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1831 return;
1832 checkSubBooks(jv);
1833 }
1834
1835 // subscribe to open book should not return domain offer
1836 {
1837 Json::Value books;
1838 books[jss::books] = Json::arrayValue;
1839 {
1840 auto& j = books[jss::books].append(Json::objectValue);
1841 j[jss::snapshot] = true;
1842 j[jss::taker_pays][jss::currency] = "XRP";
1843 j[jss::taker_gets][jss::currency] = "USD";
1844 j[jss::taker_gets][jss::issuer] = gw.human();
1845 }
1846
1847 auto jv = wsc->invoke("subscribe", books);
1848 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1849 return;
1850 BEAST_EXPECT(
1851 jv[jss::result].isMember(jss::offers) &&
1852 jv[jss::result][jss::offers].size() == 0);
1853 }
1854 }
1855
1856 void
1858 {
1859 testcase("TrackHybridOffer");
1860 using namespace jtx;
1861
1862 FeatureBitset const all{
1863 jtx::testable_amendments() | featurePermissionedDomains |
1864 featureCredentials | featurePermissionedDEX};
1865
1866 Env env(*this, all);
1867 PermissionedDEX permDex(env);
1868 auto const alice = permDex.alice;
1869 auto const bob = permDex.bob;
1870 auto const carol = permDex.carol;
1871 auto const domainID = permDex.domainID;
1872 auto const gw = permDex.gw;
1873 auto const USD = permDex.USD;
1874
1875 auto wsc = makeWSClient(env.app().config());
1876
1877 env(offer(alice, XRP(10), USD(10)),
1878 domain(domainID),
1879 txflags(tfHybrid));
1880 env.close();
1881
1882 auto checkBookOffers = [&](Json::Value const& jrr) {
1883 BEAST_EXPECT(jrr[jss::offers].isArray());
1884 BEAST_EXPECT(jrr[jss::offers].size() == 1);
1885 auto const jrOffer = jrr[jss::offers][0u];
1886 BEAST_EXPECT(jrOffer[sfAccount.fieldName] == alice.human());
1887 BEAST_EXPECT(
1888 jrOffer[sfBookDirectory.fieldName] ==
1889 getBookDir(env, XRP, USD.issue(), domainID));
1890 BEAST_EXPECT(jrOffer[sfBookNode.fieldName] == "0");
1891 BEAST_EXPECT(jrOffer[jss::Flags] == lsfHybrid);
1892 BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer);
1893 BEAST_EXPECT(jrOffer[sfOwnerNode.fieldName] == "0");
1894 BEAST_EXPECT(
1895 jrOffer[jss::TakerGets] ==
1896 USD(10).value().getJson(JsonOptions::none));
1897 BEAST_EXPECT(
1898 jrOffer[jss::TakerPays] ==
1899 XRP(10).value().getJson(JsonOptions::none));
1900 BEAST_EXPECT(
1901 jrOffer[sfDomainID.jsonName].asString() == to_string(domainID));
1902 BEAST_EXPECT(jrOffer[sfAdditionalBooks.jsonName].size() == 1);
1903 };
1904
1905 // book_offers: open book returns hybrid offer
1906 {
1907 Json::Value jvParams;
1908 jvParams[jss::taker] = env.master.human();
1909 jvParams[jss::taker_pays][jss::currency] = "XRP";
1910 jvParams[jss::ledger_index] = "validated";
1911 jvParams[jss::taker_gets][jss::currency] = "USD";
1912 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1913
1914 auto jv = wsc->invoke("book_offers", jvParams);
1915 auto jrr = jv[jss::result];
1916 checkBookOffers(jrr);
1917 }
1918
1919 auto checkSubBooks = [&](Json::Value const& jv) {
1920 BEAST_EXPECT(
1921 jv[jss::result].isMember(jss::offers) &&
1922 jv[jss::result][jss::offers].size() == 1);
1923 BEAST_EXPECT(
1924 jv[jss::result][jss::offers][0u][jss::TakerGets] ==
1925 USD(10).value().getJson(JsonOptions::none));
1926 BEAST_EXPECT(
1927 jv[jss::result][jss::offers][0u][jss::TakerPays] ==
1928 XRP(10).value().getJson(JsonOptions::none));
1929 BEAST_EXPECT(
1930 jv[jss::result][jss::offers][0u][sfDomainID.jsonName]
1931 .asString() == to_string(domainID));
1932 };
1933
1934 // book_offers: requesting domain book returns hybrid offer
1935 {
1936 Json::Value jvParams;
1937 jvParams[jss::taker] = env.master.human();
1938 jvParams[jss::taker_pays][jss::currency] = "XRP";
1939 jvParams[jss::ledger_index] = "validated";
1940 jvParams[jss::taker_gets][jss::currency] = "USD";
1941 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1942 jvParams[jss::domain] = to_string(domainID);
1943
1944 auto jv = wsc->invoke("book_offers", jvParams);
1945 auto jrr = jv[jss::result];
1946 checkBookOffers(jrr);
1947 }
1948
1949 // subscribe to domain book should return hybrid offer
1950 {
1951 Json::Value books;
1952 books[jss::books] = Json::arrayValue;
1953 {
1954 auto& j = books[jss::books].append(Json::objectValue);
1955 j[jss::snapshot] = true;
1956 j[jss::taker_pays][jss::currency] = "XRP";
1957 j[jss::taker_gets][jss::currency] = "USD";
1958 j[jss::taker_gets][jss::issuer] = gw.human();
1959 j[jss::domain] = to_string(domainID);
1960 }
1961
1962 auto jv = wsc->invoke("subscribe", books);
1963 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1964 return;
1965 checkSubBooks(jv);
1966
1967 // RPC unsubscribe
1968 auto unsubJv = wsc->invoke("unsubscribe", books);
1969 if (wsc->version() == 2)
1970 BEAST_EXPECT(unsubJv[jss::status] == "success");
1971 }
1972
1973 // subscribe to open book should return hybrid offer
1974 {
1975 Json::Value books;
1976 books[jss::books] = Json::arrayValue;
1977 {
1978 auto& j = books[jss::books].append(Json::objectValue);
1979 j[jss::snapshot] = true;
1980 j[jss::taker_pays][jss::currency] = "XRP";
1981 j[jss::taker_gets][jss::currency] = "USD";
1982 j[jss::taker_gets][jss::issuer] = gw.human();
1983 }
1984
1985 auto jv = wsc->invoke("subscribe", books);
1986 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1987 return;
1988 checkSubBooks(jv);
1989 }
1990 }
1991
1992 void
2012};
2013
2014BEAST_DEFINE_TESTSUITE_PRIO(Book, rpc, ripple, 1);
2015
2016} // namespace test
2017} // namespace ripple
Represents a JSON value.
Definition json_value.h:130
Value & append(Value const &value)
Append value to array at the end.
A testsuite class.
Definition suite.h:52
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:152
virtual Config & config()=0
Specifies an order book.
Definition Book.h:17
A currency issued by an account.
Definition Issue.h:14
Json::Value getJson(JsonOptions=JsonOptions::none) const override
Definition STAmount.cpp:753
void testMultipleBooksBothSidesOffersInBook()
void testMultipleBooksBothSidesEmptyBook()
std::string getBookDir(jtx::Env &env, Issue const &in, Issue const &out, std::optional< uint256 > const &domain=std::nullopt)
Definition Book_test.cpp:18
void testMultipleBooksOneSideEmptyBook()
void testBookOfferLimits(bool asAdmin)
void testMultipleBooksOneSideOffersInBook()
void run() override
Runs the suite.
static bool offerOnlyOnceInStream(std::unique_ptr< WSClient > const &wsc, std::chrono::milliseconds const &timeout, jtx::PrettyAmount const &takerGets, jtx::PrettyAmount const &takerPays)
Immutable cryptographic account descriptor.
Definition Account.h:20
std::string const & human() const
Returns the human readable public key.
Definition Account.h:99
A transaction testing environment.
Definition Env.h:102
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition Env.cpp:97
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:528
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:103
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:302
Account const & master
Definition Env.h:106
Application & app()
Definition Env.h:242
Json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:772
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:271
Set the domain on a JTx.
Definition domain.h:11
Match the number of items in the account's owner directory.
Definition owners.h:54
Check a set of conditions.
Definition require.h:47
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition rpc.h:16
Set the flags on a JTx.
Definition txflags.h:12
T is_same_v
@ nullValue
'null' value
Definition json_value.h:19
@ arrayValue
array value (ordered list)
Definition json_value.h:25
@ objectValue
object value (collection of name/value pairs).
Definition json_value.h:26
static LimitRange constexpr bookOffers
Limits for the book_offers command.
Keylet page(uint256 const &root, std::uint64_t index=0) noexcept
A page in a directory.
Definition Indexes.cpp:361
Keylet offer(AccountID const &id, std::uint32_t seq) noexcept
An offer from an account.
Definition Indexes.cpp:255
owner_count< ltOFFER > offers
Match the number of offers in the account's owner directory.
Definition owners.h:73
Json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:13
std::unique_ptr< Config > no_admin(std::unique_ptr< Config >)
adjust config so no admin ports are enabled
Definition envconfig.cpp:57
Json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:11
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:35
FeatureBitset testable_amendments()
Definition Env.h:55
Json::Value rate(Account const &account, double multiplier)
Set a transfer rate.
Definition rate.cpp:13
Json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:10
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
std::unique_ptr< WSClient > makeWSClient(Config const &cfg, bool v2, unsigned rpc_version, std::unordered_map< std::string, std::string > const &headers)
Returns a client operating through WebSockets/S.
Definition WSClient.cpp:304
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
AccountID const & noAccount()
A placeholder for empty accounts.
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:95
AccountID const & xrpAccount()
Compute AccountID from public key.
constexpr std::uint32_t tfHybrid
Definition TxFlags.h:83
uint256 getQualityNext(uint256 const &uBase)
Definition Indexes.cpp:122
bool cdirFirst(ReadView const &view, uint256 const &root, std::shared_ptr< SLE const > &page, unsigned int &index, uint256 &entry)
Returns the first entry in the directory, advancing the index.
Definition View.cpp:126
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:611
Json::Value getJson(LedgerFill const &fill)
Return a new Json::Value representing the ledger with given options.
uint256 getBookBase(Book const &book)
Definition Indexes.cpp:96
Represents an XRP or IOU quantity This customizes the string conversion and supports XRP conversions ...