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