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