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