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