rippled
Loading...
Searching...
No Matches
AccountLines_test.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2016 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#include <test/jtx.h>
21
22#include <xrpl/beast/unit_test.h>
23#include <xrpl/protocol/ErrorCodes.h>
24#include <xrpl/protocol/TxFlags.h>
25#include <xrpl/protocol/jss.h>
26
27namespace ripple {
28namespace RPC {
29
31{
32public:
33 void
35 {
36 testcase("account_lines");
37
38 using namespace test::jtx;
39 Env env(*this);
40 {
41 // account_lines with no account.
42 auto const lines = env.rpc("json", "account_lines", "{ }");
44 lines[jss::result][jss::error_message] ==
45 RPC::missing_field_error(jss::account)[jss::error_message]);
46 }
47 {
48 // account_lines with a malformed account.
49 auto const lines = env.rpc(
50 "json",
51 "account_lines",
52 R"({"account": )"
53 R"("n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"})");
55 lines[jss::result][jss::error_message] ==
56 RPC::make_error(rpcACT_MALFORMED)[jss::error_message]);
57 }
58 {
59 // test account non-string
60 auto testInvalidAccountParam = [&](auto const& param) {
61 Json::Value params;
62 params[jss::account] = param;
63 auto jrr = env.rpc(
64 "json", "account_lines", to_string(params))[jss::result];
65 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
67 jrr[jss::error_message] == "Invalid field 'account'.");
68 };
69
70 testInvalidAccountParam(1);
71 testInvalidAccountParam(1.1);
72 testInvalidAccountParam(true);
73 testInvalidAccountParam(Json::Value(Json::nullValue));
74 testInvalidAccountParam(Json::Value(Json::objectValue));
75 testInvalidAccountParam(Json::Value(Json::arrayValue));
76 }
77 Account const alice{"alice"};
78 {
79 // account_lines on an unfunded account.
80 auto const lines = env.rpc(
81 "json",
82 "account_lines",
83 R"({"account": ")" + alice.human() + R"("})");
85 lines[jss::result][jss::error_message] ==
86 RPC::make_error(rpcACT_NOT_FOUND)[jss::error_message]);
87 }
88 env.fund(XRP(10000), alice);
89 env.close();
90 LedgerInfo const ledger3Info = env.closed()->info();
92
93 {
94 // alice is funded but has no lines. An empty array is returned.
95 auto const lines = env.rpc(
96 "json",
97 "account_lines",
98 R"({"account": ")" + alice.human() + R"("})");
99 BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
100 BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0);
101 }
102 {
103 // Specify a ledger that doesn't exist.
104 auto const lines = env.rpc(
105 "json",
106 "account_lines",
107 R"({"account": ")" + alice.human() +
108 R"(", )"
109 R"("ledger_index": "nonsense"})");
111 lines[jss::result][jss::error_message] ==
112 "ledgerIndexMalformed");
113 }
114 {
115 // Specify a different ledger that doesn't exist.
116 auto const lines = env.rpc(
117 "json",
118 "account_lines",
119 R"({"account": ")" + alice.human() +
120 R"(", )"
121 R"("ledger_index": 50000})");
123 lines[jss::result][jss::error_message] == "ledgerNotFound");
124 }
125 // Create trust lines to share with alice.
126 Account const gw1{"gw1"};
127 env.fund(XRP(10000), gw1);
129
130 for (char c = 0; c <= ('Z' - 'A'); ++c)
131 {
132 // gw1 currencies have names "YAA" -> "YAZ".
134 gw1[std::string("YA") + static_cast<char>('A' + c)]);
135 IOU const& gw1Currency = gw1Currencies.back();
136
137 // Establish trust lines.
138 env(trust(alice, gw1Currency(100 + c)));
139 env(pay(gw1, alice, gw1Currency(50 + c)));
140 }
141 env.close();
142 LedgerInfo const ledger4Info = env.closed()->info();
144
145 // Add another set of trust lines in another ledger so we can see
146 // differences in historic ledgers.
147 Account const gw2{"gw2"};
148 env.fund(XRP(10000), gw2);
149
150 // gw2 requires authorization.
151 env(fset(gw2, asfRequireAuth));
152 env.close();
154
155 for (char c = 0; c <= ('Z' - 'A'); ++c)
156 {
157 // gw2 currencies have names "ZAA" -> "ZAZ".
159 gw2[std::string("ZA") + static_cast<char>('A' + c)]);
160 IOU const& gw2Currency = gw2Currencies.back();
161
162 // Establish trust lines.
163 env(trust(alice, gw2Currency(200 + c)));
164 env(trust(gw2, gw2Currency(0), alice, tfSetfAuth));
165 env.close();
166 env(pay(gw2, alice, gw2Currency(100 + c)));
167 env.close();
168
169 // Set flags on gw2 trust lines so we can look for them.
170 env(trust(
171 alice,
172 gw2Currency(0),
173 gw2,
175 }
176 env.close();
177 LedgerInfo const ledger58Info = env.closed()->info();
179
180 // A re-usable test for historic ledgers.
181 auto testAccountLinesHistory = [this, &env](
182 Account const& account,
183 LedgerInfo const& info,
184 int count) {
185 // Get account_lines by ledger index.
186 auto const linesSeq = env.rpc(
187 "json",
188 "account_lines",
189 R"({"account": ")" + account.human() +
190 R"(", )"
191 R"("ledger_index": )" +
192 std::to_string(info.seq) + "}");
193 BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
194 BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
195
196 // Get account_lines by ledger hash.
197 auto const linesHash = env.rpc(
198 "json",
199 "account_lines",
200 R"({"account": ")" + account.human() +
201 R"(", )"
202 R"("ledger_hash": ")" +
203 to_string(info.hash) + R"("})");
204 BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
205 BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
206 };
207
208 // Alice should have no trust lines in ledger 3.
210
211 // Alice should have 26 trust lines in ledger 4.
213
214 // Alice should have 52 trust lines in ledger 58.
216
217 {
218 // Surprisingly, it's valid to specify both index and hash, in
219 // which case the hash wins.
220 auto const lines = env.rpc(
221 "json",
222 "account_lines",
223 R"({"account": ")" + alice.human() +
224 R"(", )"
225 R"("ledger_hash": ")" +
227 R"(", )"
228 R"("ledger_index": )" +
230 BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
231 BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
232 }
233 {
234 // alice should have 52 trust lines in the current ledger.
235 auto const lines = env.rpc(
236 "json",
237 "account_lines",
238 R"({"account": ")" + alice.human() + R"("})");
239 BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
240 BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
241 }
242 {
243 // alice should have 26 trust lines with gw1.
244 auto const lines = env.rpc(
245 "json",
246 "account_lines",
247 R"({"account": ")" + alice.human() +
248 R"(", )"
249 R"("peer": ")" +
250 gw1.human() + R"("})");
251 BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
252 BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
253
254 // Check no ripple is not set for trustlines between alice and gw1
255 auto const& line = lines[jss::result][jss::lines][0u];
256 BEAST_EXPECT(!line[jss::no_ripple].isMember(jss::no_ripple));
257 }
258 {
259 // Use a malformed peer.
260 auto const lines = env.rpc(
261 "json",
262 "account_lines",
263 R"({"account": ")" + alice.human() +
264 R"(", )"
265 R"("peer": )"
266 R"("n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"})");
268 lines[jss::result][jss::error_message] ==
269 RPC::make_error(rpcACT_MALFORMED)[jss::error_message]);
270 }
271 {
272 // A negative limit should fail.
273 auto const lines = env.rpc(
274 "json",
275 "account_lines",
276 R"({"account": ")" + alice.human() +
277 R"(", )"
278 R"("limit": -1})");
280 lines[jss::result][jss::error_message] ==
281 RPC::expected_field_message(jss::limit, "unsigned integer"));
282 }
283 {
284 // Limit the response to 1 trust line.
285 auto const linesA = env.rpc(
286 "json",
287 "account_lines",
288 R"({"account": ")" + alice.human() +
289 R"(", )"
290 R"("limit": 1})");
291 BEAST_EXPECT(linesA[jss::result][jss::lines].isArray());
292 BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1);
293
294 // Pick up from where the marker left off. We should get 51.
295 auto marker = linesA[jss::result][jss::marker].asString();
296 auto const linesB = env.rpc(
297 "json",
298 "account_lines",
299 R"({"account": ")" + alice.human() +
300 R"(", )"
301 R"("marker": ")" +
302 marker + R"("})");
303 BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
304 BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);
305
306 // Go again from where the marker left off, but set a limit of 3.
307 auto const linesC = env.rpc(
308 "json",
309 "account_lines",
310 R"({"account": ")" + alice.human() +
311 R"(", )"
312 R"("limit": 3, )"
313 R"("marker": ")" +
314 marker + R"("})");
315 BEAST_EXPECT(linesC[jss::result][jss::lines].isArray());
316 BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3);
317
318 // Mess with the marker so it becomes bad and check for the error.
319 marker[5] = marker[5] == '7' ? '8' : '7';
320 auto const linesD = env.rpc(
321 "json",
322 "account_lines",
323 R"({"account": ")" + alice.human() +
324 R"(", )"
325 R"("marker": ")" +
326 marker + R"("})");
328 linesD[jss::result][jss::error_message] ==
329 RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
330 }
331 {
332 // A non-string marker should also fail.
333 auto const lines = env.rpc(
334 "json",
335 "account_lines",
336 R"({"account": ")" + alice.human() +
337 R"(", )"
338 R"("marker": true})");
340 lines[jss::result][jss::error_message] ==
341 RPC::expected_field_message(jss::marker, "string"));
342 }
343 {
344 // Check that the flags we expect from alice to gw2 are present.
345 auto const lines = env.rpc(
346 "json",
347 "account_lines",
348 R"({"account": ")" + alice.human() +
349 R"(", )"
350 R"("limit": 10, )"
351 R"("peer": ")" +
352 gw2.human() + R"("})");
353 auto const& line = lines[jss::result][jss::lines][0u];
354 BEAST_EXPECT(line[jss::freeze].asBool() == true);
355 BEAST_EXPECT(line[jss::deep_freeze].asBool() == true);
356 BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
357 BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
358 }
359 {
360 // Check that the flags we expect from gw2 to alice are present.
361 auto const linesA = env.rpc(
362 "json",
363 "account_lines",
364 R"({"account": ")" + gw2.human() +
365 R"(", )"
366 R"("limit": 1, )"
367 R"("peer": ")" +
368 alice.human() + R"("})");
369 auto const& lineA = linesA[jss::result][jss::lines][0u];
370 BEAST_EXPECT(lineA[jss::freeze_peer].asBool() == true);
371 BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true);
372 BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
373 BEAST_EXPECT(lineA[jss::authorized].asBool() == true);
374
375 // Continue from the returned marker to make sure that works.
376 BEAST_EXPECT(linesA[jss::result].isMember(jss::marker));
377 auto const marker = linesA[jss::result][jss::marker].asString();
378 auto const linesB = env.rpc(
379 "json",
380 "account_lines",
381 R"({"account": ")" + gw2.human() +
382 R"(", )"
383 R"("limit": 25, )"
384 R"("marker": ")" +
385 marker +
386 R"(", )"
387 R"("peer": ")" +
388 alice.human() + R"("})");
389 BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
390 BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25);
391 BEAST_EXPECT(!linesB[jss::result].isMember(jss::marker));
392 }
393 }
394
395 void
396 testAccountLinesMarker()
397 {
398 testcase("Entry pointed to by marker is not owned by account");
399 using namespace test::jtx;
400 Env env(*this);
401
402 // The goal of this test is observe account_lines RPC calls return an
403 // error message when the SLE pointed to by the marker is not owned by
404 // the Account being traversed.
405 //
406 // To start, we'll create an environment with some trust lines, offers
407 // and a signers list.
408 Account const alice{"alice"};
409 Account const becky{"becky"};
410 Account const gw1{"gw1"};
411 env.fund(XRP(10000), alice, becky, gw1);
412 env.close();
413
414 // Give alice a SignerList.
415 Account const bogie{"bogie"};
416 env(signers(alice, 2, {{bogie, 3}}));
417 env.close();
418
419 auto const EUR = gw1["EUR"];
420 env(trust(alice, EUR(200)));
421 env(trust(becky, EUR(200)));
422 env.close();
423
424 // Get all account objects for alice and verify that her
425 // signerlist is first. This is only a (reliable) coincidence of
426 // object naming. So if any of alice's objects are renamed this
427 // may fail.
428 Json::Value const aliceObjects = env.rpc(
429 "json",
430 "account_objects",
431 R"({"account": ")" + alice.human() +
432 R"(", )"
433 R"("limit": 10})");
434 Json::Value const& aliceSignerList =
435 aliceObjects[jss::result][jss::account_objects][0u];
436 if (!(aliceSignerList[sfLedgerEntryType.jsonName] == jss::SignerList))
437 {
438 fail(
439 "alice's account objects are misordered. "
440 "Please reorder the objects so the SignerList is first.",
441 __FILE__,
442 __LINE__);
443 return;
444 }
445
446 // Get account_lines for alice. Limit at 1, so we get a marker
447 // pointing to her SignerList.
448 auto const aliceLines1 = env.rpc(
449 "json",
450 "account_lines",
451 R"({"account": ")" + alice.human() + R"(", "limit": 1})");
452 BEAST_EXPECT(aliceLines1[jss::result].isMember(jss::marker));
453
454 // Verify that the marker points at the signer list.
455 std::string const aliceMarker =
456 aliceLines1[jss::result][jss::marker].asString();
457 std::string const markerIndex =
458 aliceMarker.substr(0, aliceMarker.find(','));
459 BEAST_EXPECT(markerIndex == aliceSignerList[jss::index].asString());
460
461 // When we fetch Alice's remaining lines we should find one and no more.
462 auto const aliceLines2 = env.rpc(
463 "json",
464 "account_lines",
465 R"({"account": ")" + alice.human() + R"(", "marker": ")" +
466 aliceMarker + R"("})");
467 BEAST_EXPECT(aliceLines2[jss::result][jss::lines].size() == 1);
468 BEAST_EXPECT(!aliceLines2[jss::result].isMember(jss::marker));
469
470 // Get account lines for beckys account, using alices SignerList as a
471 // marker. This should cause an error.
472 auto const beckyLines = env.rpc(
473 "json",
474 "account_lines",
475 R"({"account": ")" + becky.human() + R"(", "marker": ")" +
476 aliceMarker + R"("})");
477 BEAST_EXPECT(beckyLines[jss::result].isMember(jss::error_message));
478 }
479
480 void
481 testAccountLineDelete()
482 {
483 testcase("Entry pointed to by marker is removed");
484 using namespace test::jtx;
485 Env env(*this);
486
487 // The goal here is to observe account_lines marker behavior if the
488 // entry pointed at by a returned marker is removed from the ledger.
489 //
490 // It isn't easy to explicitly delete a trust line, so we do so in a
491 // round-about fashion. It takes 4 actors:
492 // o Gateway gw1 issues USD
493 // o alice offers to buy 100 USD for 100 XRP.
494 // o becky offers to sell 100 USD for 100 XRP.
495 // There will now be an inferred trustline between alice and gw1.
496 // o alice pays her 100 USD to cheri.
497 // alice should now have no USD and no trustline to gw1.
498 Account const alice{"alice"};
499 Account const becky{"becky"};
500 Account const cheri{"cheri"};
501 Account const gw1{"gw1"};
502 Account const gw2{"gw2"};
503 env.fund(XRP(10000), alice, becky, cheri, gw1, gw2);
504 env.close();
505
506 auto const USD = gw1["USD"];
507 auto const AUD = gw1["AUD"];
508 auto const EUR = gw2["EUR"];
509 env(trust(alice, USD(200)));
510 env(trust(alice, AUD(200)));
511 env(trust(becky, EUR(200)));
512 env(trust(cheri, EUR(200)));
513 env.close();
514
515 // becky gets 100 USD from gw1.
516 env(pay(gw2, becky, EUR(100)));
517 env.close();
518
519 // alice offers to buy 100 EUR for 100 XRP.
520 env(offer(alice, EUR(100), XRP(100)));
521 env.close();
522
523 // becky offers to buy 100 XRP for 100 EUR.
524 env(offer(becky, XRP(100), EUR(100)));
525 env.close();
526
527 // Get account_lines for alice. Limit at 1, so we get a marker.
528 auto const linesBeg = env.rpc(
529 "json",
530 "account_lines",
531 R"({"account": ")" + alice.human() +
532 R"(", )"
533 R"("limit": 2})");
535 linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD");
536 BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker));
537
538 // alice pays 100 EUR to cheri.
539 env(pay(alice, cheri, EUR(100)));
540 env.close();
541
542 // Since alice paid all her EUR to cheri, alice should no longer
543 // have a trust line to gw1. So the old marker should now be invalid.
544 auto const linesEnd = env.rpc(
545 "json",
546 "account_lines",
547 R"({"account": ")" + alice.human() +
548 R"(", )"
549 R"("marker": ")" +
550 linesBeg[jss::result][jss::marker].asString() + R"("})");
552 linesEnd[jss::result][jss::error_message] ==
553 RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
554 }
555
556 void
557 testAccountLinesWalkMarkers()
558 {
559 testcase("Marker can point to any appropriate ledger entry type");
560 using namespace test::jtx;
561 using namespace std::chrono_literals;
562 Env env(*this);
563
564 // The goal of this test is observe account_lines RPC calls return an
565 // error message when the SLE pointed to by the marker is not owned by
566 // the Account being traversed.
567 //
568 // To start, we'll create an environment with some trust lines, offers
569 // and a signers list.
570 Account const alice{"alice"};
571 Account const becky{"becky"};
572 Account const gw1{"gw1"};
573 env.fund(XRP(10000), alice, becky, gw1);
574 env.close();
575
576 // A couple of helper lambdas
577 auto escrow = [&env](
578 Account const& account,
579 Account const& to,
580 STAmount const& amount) {
581 Json::Value jv;
582 jv[jss::TransactionType] = jss::EscrowCreate;
583 jv[jss::Account] = account.human();
584 jv[jss::Destination] = to.human();
585 jv[jss::Amount] = amount.getJson(JsonOptions::none);
586 NetClock::time_point finish = env.now() + 1s;
587 jv[sfFinishAfter.jsonName] = finish.time_since_epoch().count();
588 return jv;
589 };
590
591 auto payChan = [](Account const& account,
592 Account const& to,
593 STAmount const& amount,
594 NetClock::duration const& settleDelay,
595 PublicKey const& pk) {
596 Json::Value jv;
597 jv[jss::TransactionType] = jss::PaymentChannelCreate;
598 jv[jss::Account] = account.human();
599 jv[jss::Destination] = to.human();
600 jv[jss::Amount] = amount.getJson(JsonOptions::none);
601 jv["SettleDelay"] = settleDelay.count();
602 jv["PublicKey"] = strHex(pk.slice());
603 return jv;
604 };
605
606 // Test all available object types. Not all of these objects will be
607 // included in the search, nor found by `account_objects`. If that ever
608 // changes for any reason, this test will help catch that.
609 //
610 // SignerList, for alice
611 Account const bogie{"bogie"};
612 env(signers(alice, 2, {{bogie, 3}}));
613 env.close();
614
615 // SignerList, includes alice
616 env(signers(becky, 2, {{alice, 3}}));
617 env.close();
618
619 // Trust lines
620 auto const EUR = gw1["EUR"];
621 env(trust(alice, EUR(200)));
622 env(trust(becky, EUR(200)));
623 env.close();
624
625 // Escrow, in each direction
626 env(escrow(alice, becky, XRP(1000)));
627 env(escrow(becky, alice, XRP(1000)));
628
629 // Pay channels, in each direction
630 env(payChan(alice, becky, XRP(1000), 100s, alice.pk()));
631 env(payChan(becky, alice, XRP(1000), 100s, becky.pk()));
632
633 // Mint NFTs, for each account
634 uint256 const aliceNFtokenID =
635 token::getNextID(env, alice, 0, tfTransferable);
636 env(token::mint(alice, 0), txflags(tfTransferable));
637
638 uint256 const beckyNFtokenID =
639 token::getNextID(env, becky, 0, tfTransferable);
640 env(token::mint(becky, 0), txflags(tfTransferable));
641
642 // NFT Offers, for each other's NFTs
643 env(token::createOffer(alice, beckyNFtokenID, drops(1)),
644 token::owner(becky));
645 env(token::createOffer(becky, aliceNFtokenID, drops(1)),
646 token::owner(alice));
647
648 env(token::createOffer(becky, beckyNFtokenID, drops(1)),
649 txflags(tfSellNFToken),
650 token::destination(alice));
651 env(token::createOffer(alice, aliceNFtokenID, drops(1)),
652 txflags(tfSellNFToken),
653 token::destination(becky));
654
655 env(token::createOffer(gw1, beckyNFtokenID, drops(1)),
656 token::owner(becky),
657 token::destination(alice));
658 env(token::createOffer(gw1, aliceNFtokenID, drops(1)),
659 token::owner(alice),
660 token::destination(becky));
661
662 env(token::createOffer(becky, beckyNFtokenID, drops(1)),
663 txflags(tfSellNFToken));
664 env(token::createOffer(alice, aliceNFtokenID, drops(1)),
665 txflags(tfSellNFToken));
666
667 // Checks, in each direction
668 env(check::create(alice, becky, XRP(50)));
669 env(check::create(becky, alice, XRP(50)));
670
671 // Deposit preauth, in each direction
672 env(deposit::auth(alice, becky));
673 env(deposit::auth(becky, alice));
674
675 // Offers, one where alice is the owner, and one where alice is the
676 // issuer
677 auto const USDalice = alice["USD"];
678 env(offer(alice, EUR(10), XRP(100)));
679 env(offer(becky, USDalice(10), XRP(100)));
680
681 // Tickets
682 env(ticket::create(alice, 2));
683
684 // Add another trustline for good measure
685 auto const BTCbecky = becky["BTC"];
686 env(trust(alice, BTCbecky(200)));
687
688 env.close();
689
690 {
691 // Now make repeated calls to `account_lines` with a limit of 1.
692 // That should iterate all of alice's relevant objects, even though
693 // the list will be empty for most calls.
694 auto getNextLine = [](Env& env,
695 Account const& alice,
698 params[jss::account] = alice.human();
699 params[jss::limit] = 1;
700 if (marker)
701 params[jss::marker] = *marker;
702
703 return env.rpc("json", "account_lines", to_string(params));
704 };
705
706 auto aliceLines = getNextLine(env, alice, std::nullopt);
707 constexpr std::size_t expectedIterations = 16;
708 constexpr std::size_t expectedLines = 2;
709 constexpr std::size_t expectedNFTs = 1;
710 std::size_t foundLines = 0;
711
712 auto hasMarker = [](auto const& aliceLines) {
713 return aliceLines[jss::result].isMember(jss::marker);
714 };
715 auto marker = [](auto const& aliceLines) {
716 return aliceLines[jss::result][jss::marker].asString();
717 };
718 auto checkLines = [](auto const& aliceLines) {
719 return aliceLines.isMember(jss::result) &&
720 !aliceLines[jss::result].isMember(jss::error_message) &&
721 aliceLines[jss::result].isMember(jss::lines) &&
722 aliceLines[jss::result][jss::lines].isArray() &&
723 aliceLines[jss::result][jss::lines].size() <= 1;
724 };
725
726 BEAST_EXPECT(hasMarker(aliceLines));
727 BEAST_EXPECT(checkLines(aliceLines));
728 BEAST_EXPECT(aliceLines[jss::result][jss::lines].size() == 0);
729
730 int iterations = 1;
731
732 while (hasMarker(aliceLines))
733 {
734 // Iterate through the markers
735 aliceLines = getNextLine(env, alice, marker(aliceLines));
736 BEAST_EXPECT(checkLines(aliceLines));
737 foundLines += aliceLines[jss::result][jss::lines].size();
738 ++iterations;
739 }
740 BEAST_EXPECT(expectedLines == foundLines);
741
742 Json::Value const aliceObjects = env.rpc(
743 "json",
744 "account_objects",
745 R"({"account": ")" + alice.human() +
746 R"(", )"
747 R"("limit": 200})");
748 BEAST_EXPECT(aliceObjects.isMember(jss::result));
750 !aliceObjects[jss::result].isMember(jss::error_message));
752 aliceObjects[jss::result].isMember(jss::account_objects));
754 aliceObjects[jss::result][jss::account_objects].isArray());
755 // account_objects does not currently return NFTPages. If
756 // that ever changes, without also changing account_lines,
757 // this test will need to be updated.
759 aliceObjects[jss::result][jss::account_objects].size() ==
760 iterations + expectedNFTs);
761 // If ledger object association ever changes, for whatever
762 // reason, this test will need to be updated.
763 BEAST_EXPECTS(
764 iterations == expectedIterations, std::to_string(iterations));
765
766 // Get becky's objects just to confirm that they're symmetrical
767 Json::Value const beckyObjects = env.rpc(
768 "json",
769 "account_objects",
770 R"({"account": ")" + becky.human() +
771 R"(", )"
772 R"("limit": 200})");
773 BEAST_EXPECT(beckyObjects.isMember(jss::result));
775 !beckyObjects[jss::result].isMember(jss::error_message));
777 beckyObjects[jss::result].isMember(jss::account_objects));
779 beckyObjects[jss::result][jss::account_objects].isArray());
780 // becky should have the same number of objects as alice, except the
781 // 2 tickets that only alice created.
783 beckyObjects[jss::result][jss::account_objects].size() ==
784 aliceObjects[jss::result][jss::account_objects].size() - 2);
785 }
786 }
787
788 // test API V2
789 void
791 {
792 testcase("V2: account_lines");
793
794 using namespace test::jtx;
795 Env env(*this);
796 {
797 // account_lines with mal-formed json2 (missing id field).
798 auto const lines = env.rpc(
799 "json2",
800 "{ "
801 R"("method" : "account_lines",)"
802 R"("jsonrpc" : "2.0",)"
803 R"("ripplerpc" : "2.0")"
804 " }");
806 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
808 lines.isMember(jss::ripplerpc) &&
809 lines[jss::ripplerpc] == "2.0");
810 }
811 {
812 // account_lines with no account.
813 auto const lines = env.rpc(
814 "json2",
815 "{ "
816 R"("method" : "account_lines",)"
817 R"("jsonrpc" : "2.0",)"
818 R"("ripplerpc" : "2.0",)"
819 R"("id" : 5)"
820 " }");
822 lines[jss::error][jss::message] ==
823 RPC::missing_field_error(jss::account)[jss::error_message]);
825 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
827 lines.isMember(jss::ripplerpc) &&
828 lines[jss::ripplerpc] == "2.0");
829 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
830 }
831 {
832 // account_lines with a malformed account.
833 auto const lines = env.rpc(
834 "json2",
835 "{ "
836 R"("method" : "account_lines",)"
837 R"("jsonrpc" : "2.0",)"
838 R"("ripplerpc" : "2.0",)"
839 R"("id" : 5,)"
840 R"("params": )"
841 R"({"account": )"
842 R"("n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"}})");
844 lines[jss::error][jss::message] ==
845 RPC::make_error(rpcACT_MALFORMED)[jss::error_message]);
847 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
849 lines.isMember(jss::ripplerpc) &&
850 lines[jss::ripplerpc] == "2.0");
851 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
852 }
853 Account const alice{"alice"};
854 {
855 // account_lines on an unfunded account.
856 auto const lines = env.rpc(
857 "json2",
858 "{ "
859 R"("method" : "account_lines",)"
860 R"("jsonrpc" : "2.0",)"
861 R"("ripplerpc" : "2.0",)"
862 R"("id" : 5,)"
863 R"("params": )"
864 R"({"account": ")" +
865 alice.human() + R"("}})");
867 lines[jss::error][jss::message] ==
868 RPC::make_error(rpcACT_NOT_FOUND)[jss::error_message]);
870 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
872 lines.isMember(jss::ripplerpc) &&
873 lines[jss::ripplerpc] == "2.0");
874 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
875 }
876 env.fund(XRP(10000), alice);
877 env.close();
878 LedgerInfo const ledger3Info = env.closed()->info();
880
881 {
882 // alice is funded but has no lines. An empty array is returned.
883 auto const lines = env.rpc(
884 "json2",
885 "{ "
886 R"("method" : "account_lines",)"
887 R"("jsonrpc" : "2.0",)"
888 R"("ripplerpc" : "2.0",)"
889 R"("id" : 5,)"
890 R"("params": )"
891 R"({"account": ")" +
892 alice.human() + R"("}})");
893 BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
894 BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0);
896 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
898 lines.isMember(jss::ripplerpc) &&
899 lines[jss::ripplerpc] == "2.0");
900 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
901 }
902 {
903 // Specify a ledger that doesn't exist.
904 auto const lines = env.rpc(
905 "json2",
906 "{ "
907 R"("method" : "account_lines",)"
908 R"("jsonrpc" : "2.0",)"
909 R"("ripplerpc" : "2.0",)"
910 R"("id" : 5,)"
911 R"("params": )"
912 R"({"account": ")" +
913 alice.human() +
914 R"(", )"
915 R"("ledger_index": "nonsense"}})");
917 lines[jss::error][jss::message] == "ledgerIndexMalformed");
919 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
921 lines.isMember(jss::ripplerpc) &&
922 lines[jss::ripplerpc] == "2.0");
923 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
924 }
925 {
926 // Specify a different ledger that doesn't exist.
927 auto const lines = env.rpc(
928 "json2",
929 "{ "
930 R"("method" : "account_lines",)"
931 R"("jsonrpc" : "2.0",)"
932 R"("ripplerpc" : "2.0",)"
933 R"("id" : 5,)"
934 R"("params": )"
935 R"({"account": ")" +
936 alice.human() +
937 R"(", )"
938 R"("ledger_index": 50000}})");
939 BEAST_EXPECT(lines[jss::error][jss::message] == "ledgerNotFound");
941 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
943 lines.isMember(jss::ripplerpc) &&
944 lines[jss::ripplerpc] == "2.0");
945 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
946 }
947 // Create trust lines to share with alice.
948 Account const gw1{"gw1"};
949 env.fund(XRP(10000), gw1);
951
952 for (char c = 0; c <= ('Z' - 'A'); ++c)
953 {
954 // gw1 currencies have names "YAA" -> "YAZ".
956 gw1[std::string("YA") + static_cast<char>('A' + c)]);
957 IOU const& gw1Currency = gw1Currencies.back();
958
959 // Establish trust lines.
960 env(trust(alice, gw1Currency(100 + c)));
961 env(pay(gw1, alice, gw1Currency(50 + c)));
962 }
963 env.close();
964 LedgerInfo const ledger4Info = env.closed()->info();
966
967 // Add another set of trust lines in another ledger so we can see
968 // differences in historic ledgers.
969 Account const gw2{"gw2"};
970 env.fund(XRP(10000), gw2);
971
972 // gw2 requires authorization.
974 env.close();
976
977 for (char c = 0; c <= ('Z' - 'A'); ++c)
978 {
979 // gw2 currencies have names "ZAA" -> "ZAZ".
981 gw2[std::string("ZA") + static_cast<char>('A' + c)]);
982 IOU const& gw2Currency = gw2Currencies.back();
983
984 // Establish trust lines.
985 env(trust(alice, gw2Currency(200 + c)));
986 env(trust(gw2, gw2Currency(0), alice, tfSetfAuth));
987 env.close();
988 env(pay(gw2, alice, gw2Currency(100 + c)));
989 env.close();
990
991 // Set flags on gw2 trust lines so we can look for them.
992 env(trust(
993 alice,
994 gw2Currency(0),
995 gw2,
997 }
998 env.close();
999 LedgerInfo const ledger58Info = env.closed()->info();
1001
1002 // A re-usable test for historic ledgers.
1004 Account const& account,
1005 LedgerInfo const& info,
1006 int count) {
1007 // Get account_lines by ledger index.
1008 auto const linesSeq = env.rpc(
1009 "json2",
1010 "{ "
1011 R"("method" : "account_lines",)"
1012 R"("jsonrpc" : "2.0",)"
1013 R"("ripplerpc" : "2.0",)"
1014 R"("id" : 5,)"
1015 R"("params": )"
1016 R"({"account": ")" +
1017 account.human() +
1018 R"(", )"
1019 R"("ledger_index": )" +
1020 std::to_string(info.seq) + "}}");
1021 BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
1022 BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
1024 linesSeq.isMember(jss::jsonrpc) &&
1025 linesSeq[jss::jsonrpc] == "2.0");
1027 linesSeq.isMember(jss::ripplerpc) &&
1028 linesSeq[jss::ripplerpc] == "2.0");
1029 BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5);
1030
1031 // Get account_lines by ledger hash.
1032 auto const linesHash = env.rpc(
1033 "json2",
1034 "{ "
1035 R"("method" : "account_lines",)"
1036 R"("jsonrpc" : "2.0",)"
1037 R"("ripplerpc" : "2.0",)"
1038 R"("id" : 5,)"
1039 R"("params": )"
1040 R"({"account": ")" +
1041 account.human() +
1042 R"(", )"
1043 R"("ledger_hash": ")" +
1044 to_string(info.hash) + R"("}})");
1045 BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
1046 BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
1048 linesHash.isMember(jss::jsonrpc) &&
1049 linesHash[jss::jsonrpc] == "2.0");
1051 linesHash.isMember(jss::ripplerpc) &&
1052 linesHash[jss::ripplerpc] == "2.0");
1054 linesHash.isMember(jss::id) && linesHash[jss::id] == 5);
1055 };
1056
1057 // Alice should have no trust lines in ledger 3.
1059
1060 // Alice should have 26 trust lines in ledger 4.
1062
1063 // Alice should have 52 trust lines in ledger 58.
1065
1066 {
1067 // Surprisingly, it's valid to specify both index and hash, in
1068 // which case the hash wins.
1069 auto const lines = env.rpc(
1070 "json2",
1071 "{ "
1072 R"("method" : "account_lines",)"
1073 R"("jsonrpc" : "2.0",)"
1074 R"("ripplerpc" : "2.0",)"
1075 R"("id" : 5,)"
1076 R"("params": )"
1077 R"({"account": ")" +
1078 alice.human() +
1079 R"(", )"
1080 R"("ledger_hash": ")" +
1082 R"(", )"
1083 R"("ledger_index": )" +
1085 BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
1086 BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
1088 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1090 lines.isMember(jss::ripplerpc) &&
1091 lines[jss::ripplerpc] == "2.0");
1092 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1093 }
1094 {
1095 // alice should have 52 trust lines in the current ledger.
1096 auto const lines = env.rpc(
1097 "json2",
1098 "{ "
1099 R"("method" : "account_lines",)"
1100 R"("jsonrpc" : "2.0",)"
1101 R"("ripplerpc" : "2.0",)"
1102 R"("id" : 5,)"
1103 R"("params": )"
1104 R"({"account": ")" +
1105 alice.human() + R"("}})");
1106 BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
1107 BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
1109 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1111 lines.isMember(jss::ripplerpc) &&
1112 lines[jss::ripplerpc] == "2.0");
1113 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1114 }
1115 {
1116 // alice should have 26 trust lines with gw1.
1117 auto const lines = env.rpc(
1118 "json2",
1119 "{ "
1120 R"("method" : "account_lines",)"
1121 R"("jsonrpc" : "2.0",)"
1122 R"("ripplerpc" : "2.0",)"
1123 R"("id" : 5,)"
1124 R"("params": )"
1125 R"({"account": ")" +
1126 alice.human() +
1127 R"(", )"
1128 R"("peer": ")" +
1129 gw1.human() + R"("}})");
1130 BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
1131 BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
1133 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1135 lines.isMember(jss::ripplerpc) &&
1136 lines[jss::ripplerpc] == "2.0");
1137 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1138 }
1139 {
1140 // Use a malformed peer.
1141 auto const lines = env.rpc(
1142 "json2",
1143 "{ "
1144 R"("method" : "account_lines",)"
1145 R"("jsonrpc" : "2.0",)"
1146 R"("ripplerpc" : "2.0",)"
1147 R"("id" : 5,)"
1148 R"("params": )"
1149 R"({"account": ")" +
1150 alice.human() +
1151 R"(", )"
1152 R"("peer": )"
1153 R"("n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"}})");
1155 lines[jss::error][jss::message] ==
1156 RPC::make_error(rpcACT_MALFORMED)[jss::error_message]);
1158 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1160 lines.isMember(jss::ripplerpc) &&
1161 lines[jss::ripplerpc] == "2.0");
1162 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1163 }
1164 {
1165 // A negative limit should fail.
1166 auto const lines = env.rpc(
1167 "json2",
1168 "{ "
1169 R"("method" : "account_lines",)"
1170 R"("jsonrpc" : "2.0",)"
1171 R"("ripplerpc" : "2.0",)"
1172 R"("id" : 5,)"
1173 R"("params": )"
1174 R"({"account": ")" +
1175 alice.human() +
1176 R"(", )"
1177 R"("limit": -1}})");
1179 lines[jss::error][jss::message] ==
1180 RPC::expected_field_message(jss::limit, "unsigned integer"));
1182 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1184 lines.isMember(jss::ripplerpc) &&
1185 lines[jss::ripplerpc] == "2.0");
1186 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1187 }
1188 {
1189 // Limit the response to 1 trust line.
1190 auto const linesA = env.rpc(
1191 "json2",
1192 "{ "
1193 R"("method" : "account_lines",)"
1194 R"("jsonrpc" : "2.0",)"
1195 R"("ripplerpc" : "2.0",)"
1196 R"("id" : 5,)"
1197 R"("params": )"
1198 R"({"account": ")" +
1199 alice.human() +
1200 R"(", )"
1201 R"("limit": 1}})");
1202 BEAST_EXPECT(linesA[jss::result][jss::lines].isArray());
1203 BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1);
1205 linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
1207 linesA.isMember(jss::ripplerpc) &&
1208 linesA[jss::ripplerpc] == "2.0");
1209 BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
1210
1211 // Pick up from where the marker left off. We should get 51.
1212 auto marker = linesA[jss::result][jss::marker].asString();
1213 auto const linesB = env.rpc(
1214 "json2",
1215 "{ "
1216 R"("method" : "account_lines",)"
1217 R"("jsonrpc" : "2.0",)"
1218 R"("ripplerpc" : "2.0",)"
1219 R"("id" : 5,)"
1220 R"("params": )"
1221 R"({"account": ")" +
1222 alice.human() +
1223 R"(", )"
1224 R"("marker": ")" +
1225 marker + R"("}})");
1226 BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
1227 BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);
1229 linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
1231 linesB.isMember(jss::ripplerpc) &&
1232 linesB[jss::ripplerpc] == "2.0");
1233 BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
1234
1235 // Go again from where the marker left off, but set a limit of 3.
1236 auto const linesC = env.rpc(
1237 "json2",
1238 "{ "
1239 R"("method" : "account_lines",)"
1240 R"("jsonrpc" : "2.0",)"
1241 R"("ripplerpc" : "2.0",)"
1242 R"("id" : 5,)"
1243 R"("params": )"
1244 R"({"account": ")" +
1245 alice.human() +
1246 R"(", )"
1247 R"("limit": 3, )"
1248 R"("marker": ")" +
1249 marker + R"("}})");
1250 BEAST_EXPECT(linesC[jss::result][jss::lines].isArray());
1251 BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3);
1253 linesC.isMember(jss::jsonrpc) && linesC[jss::jsonrpc] == "2.0");
1255 linesC.isMember(jss::ripplerpc) &&
1256 linesC[jss::ripplerpc] == "2.0");
1257 BEAST_EXPECT(linesC.isMember(jss::id) && linesC[jss::id] == 5);
1258
1259 // Mess with the marker so it becomes bad and check for the error.
1260 marker[5] = marker[5] == '7' ? '8' : '7';
1261 auto const linesD = env.rpc(
1262 "json2",
1263 "{ "
1264 R"("method" : "account_lines",)"
1265 R"("jsonrpc" : "2.0",)"
1266 R"("ripplerpc" : "2.0",)"
1267 R"("id" : 5,)"
1268 R"("params": )"
1269 R"({"account": ")" +
1270 alice.human() +
1271 R"(", )"
1272 R"("marker": ")" +
1273 marker + R"("}})");
1275 linesD[jss::error][jss::message] ==
1276 RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
1278 linesD.isMember(jss::jsonrpc) && linesD[jss::jsonrpc] == "2.0");
1280 linesD.isMember(jss::ripplerpc) &&
1281 linesD[jss::ripplerpc] == "2.0");
1282 BEAST_EXPECT(linesD.isMember(jss::id) && linesD[jss::id] == 5);
1283 }
1284 {
1285 // A non-string marker should also fail.
1286 auto const lines = env.rpc(
1287 "json2",
1288 "{ "
1289 R"("method" : "account_lines",)"
1290 R"("jsonrpc" : "2.0",)"
1291 R"("ripplerpc" : "2.0",)"
1292 R"("id" : 5,)"
1293 R"("params": )"
1294 R"({"account": ")" +
1295 alice.human() +
1296 R"(", )"
1297 R"("marker": true}})");
1299 lines[jss::error][jss::message] ==
1300 RPC::expected_field_message(jss::marker, "string"));
1302 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1304 lines.isMember(jss::ripplerpc) &&
1305 lines[jss::ripplerpc] == "2.0");
1306 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1307 }
1308 {
1309 // Check that the flags we expect from alice to gw2 are present.
1310 auto const lines = env.rpc(
1311 "json2",
1312 "{ "
1313 R"("method" : "account_lines",)"
1314 R"("jsonrpc" : "2.0",)"
1315 R"("ripplerpc" : "2.0",)"
1316 R"("id" : 5,)"
1317 R"("params": )"
1318 R"({"account": ")" +
1319 alice.human() +
1320 R"(", )"
1321 R"("limit": 10, )"
1322 R"("peer": ")" +
1323 gw2.human() + R"("}})");
1324 auto const& line = lines[jss::result][jss::lines][0u];
1325 BEAST_EXPECT(line[jss::freeze].asBool() == true);
1326 BEAST_EXPECT(line[jss::deep_freeze].asBool() == true);
1327 BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
1328 BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
1330 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1332 lines.isMember(jss::ripplerpc) &&
1333 lines[jss::ripplerpc] == "2.0");
1334 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1335 }
1336 {
1337 // Check that the flags we expect from gw2 to alice are present.
1338 auto const linesA = env.rpc(
1339 "json2",
1340 "{ "
1341 R"("method" : "account_lines",)"
1342 R"("jsonrpc" : "2.0",)"
1343 R"("ripplerpc" : "2.0",)"
1344 R"("id" : 5,)"
1345 R"("params": )"
1346 R"({"account": ")" +
1347 gw2.human() +
1348 R"(", )"
1349 R"("limit": 1, )"
1350 R"("peer": ")" +
1351 alice.human() + R"("}})");
1352 auto const& lineA = linesA[jss::result][jss::lines][0u];
1353 BEAST_EXPECT(lineA[jss::freeze_peer].asBool() == true);
1354 BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true);
1355 BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
1356 BEAST_EXPECT(lineA[jss::authorized].asBool() == true);
1358 linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
1360 linesA.isMember(jss::ripplerpc) &&
1361 linesA[jss::ripplerpc] == "2.0");
1362 BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
1363
1364 // Continue from the returned marker to make sure that works.
1365 BEAST_EXPECT(linesA[jss::result].isMember(jss::marker));
1366 auto const marker = linesA[jss::result][jss::marker].asString();
1367 auto const linesB = env.rpc(
1368 "json2",
1369 "{ "
1370 R"("method" : "account_lines",)"
1371 R"("jsonrpc" : "2.0",)"
1372 R"("ripplerpc" : "2.0",)"
1373 R"("id" : 5,)"
1374 R"("params": )"
1375 R"({"account": ")" +
1376 gw2.human() +
1377 R"(", )"
1378 R"("limit": 25, )"
1379 R"("marker": ")" +
1380 marker +
1381 R"(", )"
1382 R"("peer": ")" +
1383 alice.human() + R"("}})");
1384 BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
1385 BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25);
1386 BEAST_EXPECT(!linesB[jss::result].isMember(jss::marker));
1388 linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
1390 linesB.isMember(jss::ripplerpc) &&
1391 linesB[jss::ripplerpc] == "2.0");
1392 BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
1393 }
1394 }
1395
1396 // test API V2
1397 void
1399 {
1400 testcase("V2: account_lines with removed marker");
1401
1402 using namespace test::jtx;
1403 Env env(*this);
1404
1405 // The goal here is to observe account_lines marker behavior if the
1406 // entry pointed at by a returned marker is removed from the ledger.
1407 //
1408 // It isn't easy to explicitly delete a trust line, so we do so in a
1409 // round-about fashion. It takes 4 actors:
1410 // o Gateway gw1 issues EUR
1411 // o alice offers to buy 100 EUR for 100 XRP.
1412 // o becky offers to sell 100 EUR for 100 XRP.
1413 // There will now be an inferred trustline between alice and gw2.
1414 // o alice pays her 100 EUR to cheri.
1415 // alice should now have no EUR and no trustline to gw2.
1416 Account const alice{"alice"};
1417 Account const becky{"becky"};
1418 Account const cheri{"cheri"};
1419 Account const gw1{"gw1"};
1420 Account const gw2{"gw2"};
1421 env.fund(XRP(10000), alice, becky, cheri, gw1, gw2);
1422 env.close();
1423
1424 auto const USD = gw1["USD"];
1425 auto const AUD = gw1["AUD"];
1426 auto const EUR = gw2["EUR"];
1427 env(trust(alice, USD(200)));
1428 env(trust(alice, AUD(200)));
1429 env(trust(becky, EUR(200)));
1430 env(trust(cheri, EUR(200)));
1431 env.close();
1432
1433 // becky gets 100 EUR from gw1.
1434 env(pay(gw2, becky, EUR(100)));
1435 env.close();
1436
1437 // alice offers to buy 100 EUR for 100 XRP.
1438 env(offer(alice, EUR(100), XRP(100)));
1439 env.close();
1440
1441 // becky offers to buy 100 XRP for 100 EUR.
1442 env(offer(becky, XRP(100), EUR(100)));
1443 env.close();
1444
1445 // Get account_lines for alice. Limit at 1, so we get a marker.
1446 auto const linesBeg = env.rpc(
1447 "json2",
1448 "{ "
1449 R"("method" : "account_lines",)"
1450 R"("jsonrpc" : "2.0",)"
1451 R"("ripplerpc" : "2.0",)"
1452 R"("id" : 5,)"
1453 R"("params": )"
1454 R"({"account": ")" +
1455 alice.human() +
1456 R"(", )"
1457 R"("limit": 2}})");
1459 linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD");
1460 BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker));
1462 linesBeg.isMember(jss::jsonrpc) && linesBeg[jss::jsonrpc] == "2.0");
1464 linesBeg.isMember(jss::ripplerpc) &&
1465 linesBeg[jss::ripplerpc] == "2.0");
1466 BEAST_EXPECT(linesBeg.isMember(jss::id) && linesBeg[jss::id] == 5);
1467
1468 // alice pays 100 USD to cheri.
1469 env(pay(alice, cheri, EUR(100)));
1470 env.close();
1471
1472 // Since alice paid all her EUR to cheri, alice should no longer
1473 // have a trust line to gw1. So the old marker should now be invalid.
1474 auto const linesEnd = env.rpc(
1475 "json2",
1476 "{ "
1477 R"("method" : "account_lines",)"
1478 R"("jsonrpc" : "2.0",)"
1479 R"("ripplerpc" : "2.0",)"
1480 R"("id" : 5,)"
1481 R"("params": )"
1482 R"({"account": ")" +
1483 alice.human() +
1484 R"(", )"
1485 R"("marker": ")" +
1486 linesBeg[jss::result][jss::marker].asString() + R"("}})");
1488 linesEnd[jss::error][jss::message] ==
1489 RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
1491 linesEnd.isMember(jss::jsonrpc) && linesEnd[jss::jsonrpc] == "2.0");
1493 linesEnd.isMember(jss::ripplerpc) &&
1494 linesEnd[jss::ripplerpc] == "2.0");
1495 BEAST_EXPECT(linesEnd.isMember(jss::id) && linesEnd[jss::id] == 5);
1496 }
1497
1498 void
1499 run() override
1500 {
1502 testAccountLinesMarker();
1503 testAccountLineDelete();
1504 testAccountLinesWalkMarkers();
1507 }
1508};
1509
1510BEAST_DEFINE_TESTSUITE(AccountLines, rpc, ripple);
1511
1512} // namespace RPC
1513} // namespace ripple
T back(T... args)
Represents a JSON value.
Definition: json_value.h:150
bool isMember(char const *key) const
Return true if the object has a member named key.
Definition: json_value.cpp:962
A testsuite class.
Definition: suite.h:55
testcase_t testcase
Memberspace for declaring test cases.
Definition: suite.h:155
void fail(String const &reason, char const *file, int line)
Record a failure.
Definition: suite.h:533
std::chrono::time_point< NetClock > time_point
Definition: chrono.h:69
std::chrono::duration< rep, period > duration
Definition: chrono.h:68
BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool()==true)
BEAST_EXPECT(linesB[jss::result][jss::lines].isArray())
BEAST_EXPECT(ledger3Info.seq==3)
BEAST_EXPECT(linesEnd.isMember(jss::ripplerpc) &&linesEnd[jss::ripplerpc]=="2.0")
BEAST_EXPECT(line[jss::peer_authorized].asBool()==true)
BEAST_EXPECT(linesD.isMember(jss::id) &&linesD[jss::id]==5)
BEAST_EXPECT(line[jss::no_ripple].asBool()==true)
BEAST_EXPECT(linesD[jss::error][jss::message]==RPC::make_error(rpcINVALID_PARAMS)[jss::error_message])
testAccountLinesHistory(alice, ledger4Info, 26)
BEAST_EXPECT(linesB.isMember(jss::ripplerpc) &&linesB[jss::ripplerpc]=="2.0")
BEAST_EXPECT(linesC.isMember(jss::jsonrpc) &&linesC[jss::jsonrpc]=="2.0")
BEAST_EXPECT(ledger4Info.seq==4)
BEAST_EXPECT(lines[jss::result][jss::lines].size()==26)
BEAST_EXPECT(linesA.isMember(jss::ripplerpc) &&linesA[jss::ripplerpc]=="2.0")
BEAST_EXPECT(linesB.isMember(jss::jsonrpc) &&linesB[jss::jsonrpc]=="2.0")
BEAST_EXPECT(linesD.isMember(jss::ripplerpc) &&linesD[jss::ripplerpc]=="2.0")
BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool()==true)
BEAST_EXPECT(lineA[jss::freeze_peer].asBool()==true)
BEAST_EXPECT(linesC[jss::result][jss::lines].size()==3)
BEAST_EXPECT(linesC.isMember(jss::ripplerpc) &&linesC[jss::ripplerpc]=="2.0")
BEAST_EXPECT(linesC[jss::result][jss::lines].isArray())
BEAST_EXPECT(ledger58Info.seq==58)
BEAST_EXPECT(linesA[jss::result].isMember(jss::marker))
BEAST_EXPECT(linesEnd[jss::error][jss::message]==RPC::make_error(rpcINVALID_PARAMS)[jss::error_message])
env(fset(gw2, asfRequireAuth))
BEAST_EXPECT(lines[jss::result][jss::lines].size()==0)
BEAST_EXPECT(lines.isMember(jss::id) &&lines[jss::id]==5)
BEAST_EXPECT(lines[jss::error][jss::message]=="ledgerIndexMalformed")
BEAST_EXPECT(lines.isMember(jss::jsonrpc) &&lines[jss::jsonrpc]=="2.0")
BEAST_EXPECT(linesA[jss::result][jss::lines].isArray())
BEAST_EXPECT(lines[jss::error][jss::message]==RPC::expected_field_message(jss::limit, "unsigned integer"))
BEAST_EXPECT(lines.isMember(jss::ripplerpc) &&lines[jss::ripplerpc]=="2.0")
BEAST_EXPECT(linesB.isMember(jss::id) &&linesB[jss::id]==5)
BEAST_EXPECT(linesB[jss::result][jss::lines].size()==25)
BEAST_EXPECT(!linesB[jss::result].isMember(jss::marker))
BEAST_EXPECT(linesC.isMember(jss::id) &&linesC[jss::id]==5)
testAccountLinesHistory(alice, ledger58Info, 52)
BEAST_EXPECT(linesD.isMember(jss::jsonrpc) &&linesD[jss::jsonrpc]=="2.0")
BEAST_EXPECT(lines[jss::result][jss::lines].isArray())
BEAST_EXPECT(linesEnd.isMember(jss::id) &&linesEnd[jss::id]==5)
BEAST_EXPECT(linesB[jss::result][jss::lines].size()==51)
BEAST_EXPECT(lines[jss::error][jss::message]==RPC::expected_field_message(jss::marker, "string"))
BEAST_EXPECT(linesEnd.isMember(jss::jsonrpc) &&linesEnd[jss::jsonrpc]=="2.0")
BEAST_EXPECT(linesA.isMember(jss::id) &&linesA[jss::id]==5)
void run() override
Runs the suite.
BEAST_EXPECT(lines[jss::error][jss::message]=="ledgerNotFound")
BEAST_EXPECT(lineA[jss::authorized].asBool()==true)
BEAST_EXPECT(lines[jss::error][jss::message]==RPC::make_error(rpcACT_NOT_FOUND)[jss::error_message])
BEAST_EXPECT(lines[jss::result][jss::lines].size()==52)
BEAST_EXPECT(linesA.isMember(jss::jsonrpc) &&linesA[jss::jsonrpc]=="2.0")
BEAST_EXPECT(line[jss::deep_freeze].asBool()==true)
testAccountLinesHistory(alice, ledger3Info, 0)
BEAST_EXPECT(lines[jss::error][jss::message]==RPC::make_error(rpcACT_MALFORMED)[jss::error_message])
BEAST_EXPECT(line[jss::freeze].asBool()==true)
BEAST_EXPECT(linesA[jss::result][jss::lines].size()==1)
T find(T... args)
@ nullValue
'null' value
Definition: json_value.h:39
@ arrayValue
array value (ordered list)
Definition: json_value.h:45
@ objectValue
object value (collection of name/value pairs).
Definition: json_value.h:46
Json::Value make_error(error_code_i code)
Returns a new json object that reflects the error code.
Definition: ErrorCodes.cpp:187
std::string expected_field_message(std::string const &name, std::string const &type)
Definition: ErrorCodes.h:327
Json::Value missing_field_error(std::string const &name)
Definition: ErrorCodes.h:273
Keylet escrow(AccountID const &src, std::uint32_t seq) noexcept
An escrow entry.
Definition: Indexes.cpp:382
Keylet signers(AccountID const &account) noexcept
A SignerList.
Definition: Indexes.cpp:323
Keylet offer(AccountID const &id, std::uint32_t seq) noexcept
An offer from an account.
Definition: Indexes.cpp:267
Keylet payChan(AccountID const &src, AccountID const &dst, std::uint32_t seq) noexcept
A PaymentChannel.
Definition: Indexes.cpp:388
Json::Value trust(AccountID const &account, STAmount const &amount, std::uint32_t flags=0)
Definition: AMM.cpp:805
Json::Value pay(Account const &account, AccountID const &to, STAmount const &amount)
Definition: AMM.cpp:817
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
Json::Value finish(AccountID const &account, AccountID const &from, std::uint32_t seq)
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition: amount.cpp:105
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: algorithm.h:26
constexpr std::uint32_t tfSetDeepFreeze
Definition: TxFlags.h:119
@ rpcACT_NOT_FOUND
Definition: ErrorCodes.h:70
@ rpcACT_MALFORMED
Definition: ErrorCodes.h:90
@ rpcINVALID_PARAMS
Definition: ErrorCodes.h:84
base_uint< 256 > uint256
Definition: base_uint.h:558
constexpr std::uint32_t const tfSellNFToken
Definition: TxFlags.h:200
std::string strHex(FwdIt begin, FwdIt end)
Definition: strHex.h:30
constexpr std::uint32_t tfSetfAuth
Definition: TxFlags.h:114
std::string to_string(base_uint< Bits, Tag > const &a)
Definition: base_uint.h:630
constexpr std::uint32_t asfRequireAuth
Definition: TxFlags.h:78
constexpr std::uint32_t tfSetFreeze
Definition: TxFlags.h:117
constexpr std::uint32_t tfSetNoRipple
Definition: TxFlags.h:115
constexpr std::uint32_t const tfTransferable
Definition: TxFlags.h:145
T push_back(T... args)
T size(T... args)
Information about the notional ledger backing the view.
Definition: LedgerHeader.h:34
T substr(T... args)
T to_string(T... args)