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_pays][jss::currency] = "USD";
1640 jvParams[jss::taker_pays][jss::issuer] = gw.human();
1641 jvParams[jss::taker_gets][jss::currency] = "USD";
1642 auto const jrr = env.rpc(
1643 "json", "book_offers", to_string(jvParams))[jss::result];
1644 BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
1645 BEAST_EXPECT(
1646 jrr[jss::error_message] ==
1647 "Invalid field 'taker_gets.issuer', "
1648 "expected non-XRP issuer.");
1649 }
1650
1651 {
1652 Json::Value jvParams;
1653 jvParams[jss::ledger_index] = "validated";
1654 jvParams[jss::taker_pays][jss::currency] = "USD";
1655 jvParams[jss::taker_pays][jss::issuer] = gw.human();
1656 jvParams[jss::taker_gets][jss::currency] = "XRP";
1657 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1658 auto const jrr = env.rpc(
1659 "json", "book_offers", to_string(jvParams))[jss::result];
1660 BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
1661 BEAST_EXPECT(
1662 jrr[jss::error_message] ==
1663 "Unneeded field 'taker_gets.issuer' "
1664 "for XRP currency specification.");
1665 }
1666 {
1667 Json::Value jvParams;
1668 jvParams[jss::ledger_index] = "validated";
1669 jvParams[jss::taker_pays][jss::currency] = "USD";
1670 jvParams[jss::taker_pays][jss::issuer] = gw.human();
1671 jvParams[jss::taker_gets][jss::currency] = "EUR";
1672 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1673 jvParams[jss::domain] = "badString";
1674 auto const jrr = env.rpc(
1675 "json", "book_offers", to_string(jvParams))[jss::result];
1676 BEAST_EXPECT(jrr[jss::error] == "domainMalformed");
1677 BEAST_EXPECT(jrr[jss::error_message] == "Unable to parse domain.");
1678 }
1679 }
1680
1681 void
1683 {
1684 testcase("BookOffer Limits");
1685 using namespace jtx;
1686 Env env{*this, asAdmin ? envconfig() : envconfig(no_admin)};
1687 Account gw{"gw"};
1688 env.fund(XRP(200000), gw);
1689 // Note that calls to env.close() fail without admin permission.
1690 if (asAdmin)
1691 env.close();
1692
1693 auto USD = gw["USD"];
1694
1695 for (auto i = 0; i <= RPC::Tuning::bookOffers.rmax; i++)
1696 env(offer(gw, XRP(50 + 1 * i), USD(1.0 + 0.1 * i)));
1697
1698 if (asAdmin)
1699 env.close();
1700
1701 Json::Value jvParams;
1702 jvParams[jss::limit] = 1;
1703 jvParams[jss::ledger_index] = "validated";
1704 jvParams[jss::taker_pays][jss::currency] = "XRP";
1705 jvParams[jss::taker_gets][jss::currency] = "USD";
1706 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1707 auto jrr =
1708 env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
1709 BEAST_EXPECT(jrr[jss::offers].isArray());
1710 BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? 1u : 0u));
1711 // NOTE - a marker field is not returned for this method
1712
1713 jvParams[jss::limit] = 0u;
1714 jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
1715 BEAST_EXPECT(jrr[jss::offers].isArray());
1716 BEAST_EXPECT(jrr[jss::offers].size() == 0u);
1717
1718 jvParams[jss::limit] = RPC::Tuning::bookOffers.rmax + 1;
1719 jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
1720 BEAST_EXPECT(jrr[jss::offers].isArray());
1721 BEAST_EXPECT(
1722 jrr[jss::offers].size() ==
1723 (asAdmin ? RPC::Tuning::bookOffers.rmax + 1 : 0u));
1724
1725 jvParams[jss::limit] = Json::nullValue;
1726 jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
1727 BEAST_EXPECT(jrr[jss::offers].isArray());
1728 BEAST_EXPECT(
1729 jrr[jss::offers].size() ==
1730 (asAdmin ? RPC::Tuning::bookOffers.rdefault : 0u));
1731 }
1732
1733 void
1735 {
1736 testcase("TrackDomainOffer");
1737 using namespace jtx;
1738
1739 FeatureBitset const all{
1740 jtx::testable_amendments() | featurePermissionedDomains |
1741 featureCredentials | featurePermissionedDEX};
1742
1743 Env env(*this, all);
1744 PermissionedDEX permDex(env);
1745 auto const alice = permDex.alice;
1746 auto const bob = permDex.bob;
1747 auto const carol = permDex.carol;
1748 auto const domainID = permDex.domainID;
1749 auto const gw = permDex.gw;
1750 auto const USD = permDex.USD;
1751
1752 auto wsc = makeWSClient(env.app().config());
1753
1754 env(offer(alice, XRP(10), USD(10)), domain(domainID));
1755 env.close();
1756
1757 auto checkBookOffers = [&](Json::Value const& jrr) {
1758 BEAST_EXPECT(jrr[jss::offers].isArray());
1759 BEAST_EXPECT(jrr[jss::offers].size() == 1);
1760 auto const jrOffer = jrr[jss::offers][0u];
1761 BEAST_EXPECT(jrOffer[sfAccount.fieldName] == alice.human());
1762 BEAST_EXPECT(
1763 jrOffer[sfBookDirectory.fieldName] ==
1764 getBookDir(env, XRP, USD.issue(), domainID));
1765 BEAST_EXPECT(jrOffer[sfBookNode.fieldName] == "0");
1766 BEAST_EXPECT(jrOffer[jss::Flags] == 0);
1767 BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer);
1768 BEAST_EXPECT(jrOffer[sfOwnerNode.fieldName] == "0");
1769 BEAST_EXPECT(
1770 jrOffer[jss::TakerGets] ==
1771 USD(10).value().getJson(JsonOptions::none));
1772 BEAST_EXPECT(
1773 jrOffer[jss::TakerPays] ==
1774 XRP(10).value().getJson(JsonOptions::none));
1775 BEAST_EXPECT(
1776 jrOffer[sfDomainID.jsonName].asString() == to_string(domainID));
1777 };
1778
1779 // book_offers: open book doesn't return offer
1780 {
1781 Json::Value jvParams;
1782 jvParams[jss::taker] = env.master.human();
1783 jvParams[jss::taker_pays][jss::currency] = "XRP";
1784 jvParams[jss::ledger_index] = "validated";
1785 jvParams[jss::taker_gets][jss::currency] = "USD";
1786 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1787
1788 auto jv = wsc->invoke("book_offers", jvParams);
1789 auto jrr = jv[jss::result];
1790 BEAST_EXPECT(jrr[jss::offers].isArray());
1791 BEAST_EXPECT(jrr[jss::offers].size() == 0);
1792 }
1793
1794 auto checkSubBooks = [&](Json::Value const& jv) {
1795 BEAST_EXPECT(
1796 jv[jss::result].isMember(jss::offers) &&
1797 jv[jss::result][jss::offers].size() == 1);
1798 BEAST_EXPECT(
1799 jv[jss::result][jss::offers][0u][jss::TakerGets] ==
1800 USD(10).value().getJson(JsonOptions::none));
1801 BEAST_EXPECT(
1802 jv[jss::result][jss::offers][0u][jss::TakerPays] ==
1803 XRP(10).value().getJson(JsonOptions::none));
1804 BEAST_EXPECT(
1805 jv[jss::result][jss::offers][0u][sfDomainID.jsonName]
1806 .asString() == to_string(domainID));
1807 };
1808
1809 // book_offers: requesting domain book returns hybrid offer
1810 {
1811 Json::Value jvParams;
1812 jvParams[jss::taker] = env.master.human();
1813 jvParams[jss::taker_pays][jss::currency] = "XRP";
1814 jvParams[jss::ledger_index] = "validated";
1815 jvParams[jss::taker_gets][jss::currency] = "USD";
1816 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1817 jvParams[jss::domain] = to_string(domainID);
1818
1819 auto jv = wsc->invoke("book_offers", jvParams);
1820 auto jrr = jv[jss::result];
1821 checkBookOffers(jrr);
1822 }
1823
1824 // subscribe to domain book should return domain offer
1825 {
1826 Json::Value books;
1827 books[jss::books] = Json::arrayValue;
1828 {
1829 auto& j = books[jss::books].append(Json::objectValue);
1830 j[jss::snapshot] = true;
1831 j[jss::taker_pays][jss::currency] = "XRP";
1832 j[jss::taker_gets][jss::currency] = "USD";
1833 j[jss::taker_gets][jss::issuer] = gw.human();
1834 j[jss::domain] = to_string(domainID);
1835 }
1836
1837 auto jv = wsc->invoke("subscribe", books);
1838 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1839 return;
1840 checkSubBooks(jv);
1841 }
1842
1843 // subscribe to open book should not return domain offer
1844 {
1845 Json::Value books;
1846 books[jss::books] = Json::arrayValue;
1847 {
1848 auto& j = books[jss::books].append(Json::objectValue);
1849 j[jss::snapshot] = true;
1850 j[jss::taker_pays][jss::currency] = "XRP";
1851 j[jss::taker_gets][jss::currency] = "USD";
1852 j[jss::taker_gets][jss::issuer] = gw.human();
1853 }
1854
1855 auto jv = wsc->invoke("subscribe", books);
1856 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1857 return;
1858 BEAST_EXPECT(
1859 jv[jss::result].isMember(jss::offers) &&
1860 jv[jss::result][jss::offers].size() == 0);
1861 }
1862 }
1863
1864 void
1866 {
1867 testcase("TrackHybridOffer");
1868 using namespace jtx;
1869
1870 FeatureBitset const all{
1871 jtx::testable_amendments() | featurePermissionedDomains |
1872 featureCredentials | featurePermissionedDEX};
1873
1874 Env env(*this, all);
1875 PermissionedDEX permDex(env);
1876 auto const alice = permDex.alice;
1877 auto const bob = permDex.bob;
1878 auto const carol = permDex.carol;
1879 auto const domainID = permDex.domainID;
1880 auto const gw = permDex.gw;
1881 auto const USD = permDex.USD;
1882
1883 auto wsc = makeWSClient(env.app().config());
1884
1885 env(offer(alice, XRP(10), USD(10)),
1886 domain(domainID),
1887 txflags(tfHybrid));
1888 env.close();
1889
1890 auto checkBookOffers = [&](Json::Value const& jrr) {
1891 BEAST_EXPECT(jrr[jss::offers].isArray());
1892 BEAST_EXPECT(jrr[jss::offers].size() == 1);
1893 auto const jrOffer = jrr[jss::offers][0u];
1894 BEAST_EXPECT(jrOffer[sfAccount.fieldName] == alice.human());
1895 BEAST_EXPECT(
1896 jrOffer[sfBookDirectory.fieldName] ==
1897 getBookDir(env, XRP, USD.issue(), domainID));
1898 BEAST_EXPECT(jrOffer[sfBookNode.fieldName] == "0");
1899 BEAST_EXPECT(jrOffer[jss::Flags] == lsfHybrid);
1900 BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer);
1901 BEAST_EXPECT(jrOffer[sfOwnerNode.fieldName] == "0");
1902 BEAST_EXPECT(
1903 jrOffer[jss::TakerGets] ==
1904 USD(10).value().getJson(JsonOptions::none));
1905 BEAST_EXPECT(
1906 jrOffer[jss::TakerPays] ==
1907 XRP(10).value().getJson(JsonOptions::none));
1908 BEAST_EXPECT(
1909 jrOffer[sfDomainID.jsonName].asString() == to_string(domainID));
1910 BEAST_EXPECT(jrOffer[sfAdditionalBooks.jsonName].size() == 1);
1911 };
1912
1913 // book_offers: open book returns hybrid offer
1914 {
1915 Json::Value jvParams;
1916 jvParams[jss::taker] = env.master.human();
1917 jvParams[jss::taker_pays][jss::currency] = "XRP";
1918 jvParams[jss::ledger_index] = "validated";
1919 jvParams[jss::taker_gets][jss::currency] = "USD";
1920 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1921
1922 auto jv = wsc->invoke("book_offers", jvParams);
1923 auto jrr = jv[jss::result];
1924 checkBookOffers(jrr);
1925 }
1926
1927 auto checkSubBooks = [&](Json::Value const& jv) {
1928 BEAST_EXPECT(
1929 jv[jss::result].isMember(jss::offers) &&
1930 jv[jss::result][jss::offers].size() == 1);
1931 BEAST_EXPECT(
1932 jv[jss::result][jss::offers][0u][jss::TakerGets] ==
1933 USD(10).value().getJson(JsonOptions::none));
1934 BEAST_EXPECT(
1935 jv[jss::result][jss::offers][0u][jss::TakerPays] ==
1936 XRP(10).value().getJson(JsonOptions::none));
1937 BEAST_EXPECT(
1938 jv[jss::result][jss::offers][0u][sfDomainID.jsonName]
1939 .asString() == to_string(domainID));
1940 };
1941
1942 // book_offers: requesting domain book returns hybrid offer
1943 {
1944 Json::Value jvParams;
1945 jvParams[jss::taker] = env.master.human();
1946 jvParams[jss::taker_pays][jss::currency] = "XRP";
1947 jvParams[jss::ledger_index] = "validated";
1948 jvParams[jss::taker_gets][jss::currency] = "USD";
1949 jvParams[jss::taker_gets][jss::issuer] = gw.human();
1950 jvParams[jss::domain] = to_string(domainID);
1951
1952 auto jv = wsc->invoke("book_offers", jvParams);
1953 auto jrr = jv[jss::result];
1954 checkBookOffers(jrr);
1955 }
1956
1957 // subscribe to domain book should return hybrid offer
1958 {
1959 Json::Value books;
1960 books[jss::books] = Json::arrayValue;
1961 {
1962 auto& j = books[jss::books].append(Json::objectValue);
1963 j[jss::snapshot] = true;
1964 j[jss::taker_pays][jss::currency] = "XRP";
1965 j[jss::taker_gets][jss::currency] = "USD";
1966 j[jss::taker_gets][jss::issuer] = gw.human();
1967 j[jss::domain] = to_string(domainID);
1968 }
1969
1970 auto jv = wsc->invoke("subscribe", books);
1971 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1972 return;
1973 checkSubBooks(jv);
1974
1975 // RPC unsubscribe
1976 auto unsubJv = wsc->invoke("unsubscribe", books);
1977 if (wsc->version() == 2)
1978 BEAST_EXPECT(unsubJv[jss::status] == "success");
1979 }
1980
1981 // subscribe to open book should return hybrid offer
1982 {
1983 Json::Value books;
1984 books[jss::books] = Json::arrayValue;
1985 {
1986 auto& j = books[jss::books].append(Json::objectValue);
1987 j[jss::snapshot] = true;
1988 j[jss::taker_pays][jss::currency] = "XRP";
1989 j[jss::taker_gets][jss::currency] = "USD";
1990 j[jss::taker_gets][jss::issuer] = gw.human();
1991 }
1992
1993 auto jv = wsc->invoke("subscribe", books);
1994 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1995 return;
1996 checkSubBooks(jv);
1997 }
1998 }
1999
2000 void
2020};
2021
2022BEAST_DEFINE_TESTSUITE_PRIO(Book, rpc, ripple, 1);
2023
2024} // namespace test
2025} // 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:795
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:115
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:544
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:121
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:310
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:788
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:279
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:65
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:105
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.
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:146
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
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 ...