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 auto payChan = [](Account const& account,
577 Account const& to,
578 STAmount const& amount,
579 NetClock::duration const& settleDelay,
580 PublicKey const& pk) {
581 Json::Value jv;
582 jv[jss::TransactionType] = jss::PaymentChannelCreate;
583 jv[jss::Account] = account.human();
584 jv[jss::Destination] = to.human();
585 jv[jss::Amount] = amount.getJson(JsonOptions::none);
586 jv["SettleDelay"] = settleDelay.count();
587 jv["PublicKey"] = strHex(pk.slice());
588 return jv;
589 };
590
591 // Test all available object types. Not all of these objects will be
592 // included in the search, nor found by `account_objects`. If that ever
593 // changes for any reason, this test will help catch that.
594 //
595 // SignerList, for alice
596 Account const bogie{"bogie"};
597 env(signers(alice, 2, {{bogie, 3}}));
598 env.close();
599
600 // SignerList, includes alice
601 env(signers(becky, 2, {{alice, 3}}));
602 env.close();
603
604 // Trust lines
605 auto const EUR = gw1["EUR"];
606 env(trust(alice, EUR(200)));
607 env(trust(becky, EUR(200)));
608 env.close();
609
610 // Escrow, in each direction
611 env(escrow::create(alice, becky, XRP(1000)),
612 escrow::finish_time(env.now() + 1s));
613 env(escrow::create(becky, alice, XRP(1000)),
614 escrow::finish_time(env.now() + 1s));
615
616 // Pay channels, in each direction
617 env(payChan(alice, becky, XRP(1000), 100s, alice.pk()));
618 env(payChan(becky, alice, XRP(1000), 100s, becky.pk()));
619
620 // Mint NFTs, for each account
621 uint256 const aliceNFtokenID =
622 token::getNextID(env, alice, 0, tfTransferable);
623 env(token::mint(alice, 0), txflags(tfTransferable));
624
625 uint256 const beckyNFtokenID =
626 token::getNextID(env, becky, 0, tfTransferable);
627 env(token::mint(becky, 0), txflags(tfTransferable));
628
629 // NFT Offers, for each other's NFTs
630 env(token::createOffer(alice, beckyNFtokenID, drops(1)),
631 token::owner(becky));
632 env(token::createOffer(becky, aliceNFtokenID, drops(1)),
633 token::owner(alice));
634
635 env(token::createOffer(becky, beckyNFtokenID, drops(1)),
636 txflags(tfSellNFToken),
637 token::destination(alice));
638 env(token::createOffer(alice, aliceNFtokenID, drops(1)),
639 txflags(tfSellNFToken),
640 token::destination(becky));
641
642 env(token::createOffer(gw1, beckyNFtokenID, drops(1)),
643 token::owner(becky),
644 token::destination(alice));
645 env(token::createOffer(gw1, aliceNFtokenID, drops(1)),
646 token::owner(alice),
647 token::destination(becky));
648
649 env(token::createOffer(becky, beckyNFtokenID, drops(1)),
650 txflags(tfSellNFToken));
651 env(token::createOffer(alice, aliceNFtokenID, drops(1)),
652 txflags(tfSellNFToken));
653
654 // Checks, in each direction
655 env(check::create(alice, becky, XRP(50)));
656 env(check::create(becky, alice, XRP(50)));
657
658 // Deposit preauth, in each direction
659 env(deposit::auth(alice, becky));
660 env(deposit::auth(becky, alice));
661
662 // Offers, one where alice is the owner, and one where alice is the
663 // issuer
664 auto const USDalice = alice["USD"];
665 env(offer(alice, EUR(10), XRP(100)));
666 env(offer(becky, USDalice(10), XRP(100)));
667
668 // Tickets
669 env(ticket::create(alice, 2));
670
671 // Add another trustline for good measure
672 auto const BTCbecky = becky["BTC"];
673 env(trust(alice, BTCbecky(200)));
674
675 env.close();
676
677 {
678 // Now make repeated calls to `account_lines` with a limit of 1.
679 // That should iterate all of alice's relevant objects, even though
680 // the list will be empty for most calls.
681 auto getNextLine = [](Env& env,
682 Account const& alice,
685 params[jss::account] = alice.human();
686 params[jss::limit] = 1;
687 if (marker)
688 params[jss::marker] = *marker;
689
690 return env.rpc("json", "account_lines", to_string(params));
691 };
692
693 auto aliceLines = getNextLine(env, alice, std::nullopt);
694 constexpr std::size_t expectedIterations = 16;
695 constexpr std::size_t expectedLines = 2;
696 constexpr std::size_t expectedNFTs = 1;
697 std::size_t foundLines = 0;
698
699 auto hasMarker = [](auto const& aliceLines) {
700 return aliceLines[jss::result].isMember(jss::marker);
701 };
702 auto marker = [](auto const& aliceLines) {
703 return aliceLines[jss::result][jss::marker].asString();
704 };
705 auto checkLines = [](auto const& aliceLines) {
706 return aliceLines.isMember(jss::result) &&
707 !aliceLines[jss::result].isMember(jss::error_message) &&
708 aliceLines[jss::result].isMember(jss::lines) &&
709 aliceLines[jss::result][jss::lines].isArray() &&
710 aliceLines[jss::result][jss::lines].size() <= 1;
711 };
712
713 BEAST_EXPECT(hasMarker(aliceLines));
714 BEAST_EXPECT(checkLines(aliceLines));
715 BEAST_EXPECT(aliceLines[jss::result][jss::lines].size() == 0);
716
717 int iterations = 1;
718
719 while (hasMarker(aliceLines))
720 {
721 // Iterate through the markers
722 aliceLines = getNextLine(env, alice, marker(aliceLines));
723 BEAST_EXPECT(checkLines(aliceLines));
724 foundLines += aliceLines[jss::result][jss::lines].size();
725 ++iterations;
726 }
727 BEAST_EXPECT(expectedLines == foundLines);
728
729 Json::Value const aliceObjects = env.rpc(
730 "json",
731 "account_objects",
732 R"({"account": ")" + alice.human() +
733 R"(", )"
734 R"("limit": 200})");
735 BEAST_EXPECT(aliceObjects.isMember(jss::result));
737 !aliceObjects[jss::result].isMember(jss::error_message));
739 aliceObjects[jss::result].isMember(jss::account_objects));
741 aliceObjects[jss::result][jss::account_objects].isArray());
742 // account_objects does not currently return NFTPages. If
743 // that ever changes, without also changing account_lines,
744 // this test will need to be updated.
746 aliceObjects[jss::result][jss::account_objects].size() ==
747 iterations + expectedNFTs);
748 // If ledger object association ever changes, for whatever
749 // reason, this test will need to be updated.
750 BEAST_EXPECTS(
751 iterations == expectedIterations, std::to_string(iterations));
752
753 // Get becky's objects just to confirm that they're symmetrical
754 Json::Value const beckyObjects = env.rpc(
755 "json",
756 "account_objects",
757 R"({"account": ")" + becky.human() +
758 R"(", )"
759 R"("limit": 200})");
760 BEAST_EXPECT(beckyObjects.isMember(jss::result));
762 !beckyObjects[jss::result].isMember(jss::error_message));
764 beckyObjects[jss::result].isMember(jss::account_objects));
766 beckyObjects[jss::result][jss::account_objects].isArray());
767 // becky should have the same number of objects as alice, except the
768 // 2 tickets that only alice created.
770 beckyObjects[jss::result][jss::account_objects].size() ==
771 aliceObjects[jss::result][jss::account_objects].size() - 2);
772 }
773 }
774
775 // test API V2
776 void
778 {
779 testcase("V2: account_lines");
780
781 using namespace test::jtx;
782 Env env(*this);
783 {
784 // account_lines with mal-formed json2 (missing id field).
785 auto const lines = env.rpc(
786 "json2",
787 "{ "
788 R"("method" : "account_lines",)"
789 R"("jsonrpc" : "2.0",)"
790 R"("ripplerpc" : "2.0")"
791 " }");
793 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
795 lines.isMember(jss::ripplerpc) &&
796 lines[jss::ripplerpc] == "2.0");
797 }
798 {
799 // account_lines with no account.
800 auto const lines = env.rpc(
801 "json2",
802 "{ "
803 R"("method" : "account_lines",)"
804 R"("jsonrpc" : "2.0",)"
805 R"("ripplerpc" : "2.0",)"
806 R"("id" : 5)"
807 " }");
809 lines[jss::error][jss::message] ==
810 RPC::missing_field_error(jss::account)[jss::error_message]);
812 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
814 lines.isMember(jss::ripplerpc) &&
815 lines[jss::ripplerpc] == "2.0");
816 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
817 }
818 {
819 // account_lines with a malformed account.
820 auto const lines = env.rpc(
821 "json2",
822 "{ "
823 R"("method" : "account_lines",)"
824 R"("jsonrpc" : "2.0",)"
825 R"("ripplerpc" : "2.0",)"
826 R"("id" : 5,)"
827 R"("params": )"
828 R"({"account": )"
829 R"("n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"}})");
831 lines[jss::error][jss::message] ==
832 RPC::make_error(rpcACT_MALFORMED)[jss::error_message]);
834 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
836 lines.isMember(jss::ripplerpc) &&
837 lines[jss::ripplerpc] == "2.0");
838 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
839 }
840 Account const alice{"alice"};
841 {
842 // account_lines on an unfunded account.
843 auto const lines = env.rpc(
844 "json2",
845 "{ "
846 R"("method" : "account_lines",)"
847 R"("jsonrpc" : "2.0",)"
848 R"("ripplerpc" : "2.0",)"
849 R"("id" : 5,)"
850 R"("params": )"
851 R"({"account": ")" +
852 alice.human() + R"("}})");
854 lines[jss::error][jss::message] ==
855 RPC::make_error(rpcACT_NOT_FOUND)[jss::error_message]);
857 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
859 lines.isMember(jss::ripplerpc) &&
860 lines[jss::ripplerpc] == "2.0");
861 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
862 }
863 env.fund(XRP(10000), alice);
864 env.close();
865 LedgerInfo const ledger3Info = env.closed()->info();
867
868 {
869 // alice is funded but has no lines. An empty array is returned.
870 auto const lines = env.rpc(
871 "json2",
872 "{ "
873 R"("method" : "account_lines",)"
874 R"("jsonrpc" : "2.0",)"
875 R"("ripplerpc" : "2.0",)"
876 R"("id" : 5,)"
877 R"("params": )"
878 R"({"account": ")" +
879 alice.human() + R"("}})");
880 BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
881 BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0);
883 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
885 lines.isMember(jss::ripplerpc) &&
886 lines[jss::ripplerpc] == "2.0");
887 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
888 }
889 {
890 // Specify a ledger that doesn't exist.
891 auto const lines = env.rpc(
892 "json2",
893 "{ "
894 R"("method" : "account_lines",)"
895 R"("jsonrpc" : "2.0",)"
896 R"("ripplerpc" : "2.0",)"
897 R"("id" : 5,)"
898 R"("params": )"
899 R"({"account": ")" +
900 alice.human() +
901 R"(", )"
902 R"("ledger_index": "nonsense"}})");
904 lines[jss::error][jss::message] == "ledgerIndexMalformed");
906 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
908 lines.isMember(jss::ripplerpc) &&
909 lines[jss::ripplerpc] == "2.0");
910 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
911 }
912 {
913 // Specify a different ledger that doesn't exist.
914 auto const lines = env.rpc(
915 "json2",
916 "{ "
917 R"("method" : "account_lines",)"
918 R"("jsonrpc" : "2.0",)"
919 R"("ripplerpc" : "2.0",)"
920 R"("id" : 5,)"
921 R"("params": )"
922 R"({"account": ")" +
923 alice.human() +
924 R"(", )"
925 R"("ledger_index": 50000}})");
926 BEAST_EXPECT(lines[jss::error][jss::message] == "ledgerNotFound");
928 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
930 lines.isMember(jss::ripplerpc) &&
931 lines[jss::ripplerpc] == "2.0");
932 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
933 }
934 // Create trust lines to share with alice.
935 Account const gw1{"gw1"};
936 env.fund(XRP(10000), gw1);
938
939 for (char c = 0; c <= ('Z' - 'A'); ++c)
940 {
941 // gw1 currencies have names "YAA" -> "YAZ".
943 gw1[std::string("YA") + static_cast<char>('A' + c)]);
944 IOU const& gw1Currency = gw1Currencies.back();
945
946 // Establish trust lines.
947 env(trust(alice, gw1Currency(100 + c)));
948 env(pay(gw1, alice, gw1Currency(50 + c)));
949 }
950 env.close();
951 LedgerInfo const ledger4Info = env.closed()->info();
953
954 // Add another set of trust lines in another ledger so we can see
955 // differences in historic ledgers.
956 Account const gw2{"gw2"};
957 env.fund(XRP(10000), gw2);
958
959 // gw2 requires authorization.
961 env.close();
963
964 for (char c = 0; c <= ('Z' - 'A'); ++c)
965 {
966 // gw2 currencies have names "ZAA" -> "ZAZ".
968 gw2[std::string("ZA") + static_cast<char>('A' + c)]);
969 IOU const& gw2Currency = gw2Currencies.back();
970
971 // Establish trust lines.
972 env(trust(alice, gw2Currency(200 + c)));
973 env(trust(gw2, gw2Currency(0), alice, tfSetfAuth));
974 env.close();
975 env(pay(gw2, alice, gw2Currency(100 + c)));
976 env.close();
977
978 // Set flags on gw2 trust lines so we can look for them.
979 env(trust(
980 alice,
981 gw2Currency(0),
982 gw2,
984 }
985 env.close();
986 LedgerInfo const ledger58Info = env.closed()->info();
988
989 // A re-usable test for historic ledgers.
991 Account const& account,
992 LedgerInfo const& info,
993 int count) {
994 // Get account_lines by ledger index.
995 auto const linesSeq = env.rpc(
996 "json2",
997 "{ "
998 R"("method" : "account_lines",)"
999 R"("jsonrpc" : "2.0",)"
1000 R"("ripplerpc" : "2.0",)"
1001 R"("id" : 5,)"
1002 R"("params": )"
1003 R"({"account": ")" +
1004 account.human() +
1005 R"(", )"
1006 R"("ledger_index": )" +
1007 std::to_string(info.seq) + "}}");
1008 BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
1009 BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
1011 linesSeq.isMember(jss::jsonrpc) &&
1012 linesSeq[jss::jsonrpc] == "2.0");
1014 linesSeq.isMember(jss::ripplerpc) &&
1015 linesSeq[jss::ripplerpc] == "2.0");
1016 BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5);
1017
1018 // Get account_lines by ledger hash.
1019 auto const linesHash = env.rpc(
1020 "json2",
1021 "{ "
1022 R"("method" : "account_lines",)"
1023 R"("jsonrpc" : "2.0",)"
1024 R"("ripplerpc" : "2.0",)"
1025 R"("id" : 5,)"
1026 R"("params": )"
1027 R"({"account": ")" +
1028 account.human() +
1029 R"(", )"
1030 R"("ledger_hash": ")" +
1031 to_string(info.hash) + R"("}})");
1032 BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
1033 BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
1035 linesHash.isMember(jss::jsonrpc) &&
1036 linesHash[jss::jsonrpc] == "2.0");
1038 linesHash.isMember(jss::ripplerpc) &&
1039 linesHash[jss::ripplerpc] == "2.0");
1041 linesHash.isMember(jss::id) && linesHash[jss::id] == 5);
1042 };
1043
1044 // Alice should have no trust lines in ledger 3.
1046
1047 // Alice should have 26 trust lines in ledger 4.
1049
1050 // Alice should have 52 trust lines in ledger 58.
1052
1053 {
1054 // Surprisingly, it's valid to specify both index and hash, in
1055 // which case the hash wins.
1056 auto const lines = env.rpc(
1057 "json2",
1058 "{ "
1059 R"("method" : "account_lines",)"
1060 R"("jsonrpc" : "2.0",)"
1061 R"("ripplerpc" : "2.0",)"
1062 R"("id" : 5,)"
1063 R"("params": )"
1064 R"({"account": ")" +
1065 alice.human() +
1066 R"(", )"
1067 R"("ledger_hash": ")" +
1069 R"(", )"
1070 R"("ledger_index": )" +
1072 BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
1073 BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
1075 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1077 lines.isMember(jss::ripplerpc) &&
1078 lines[jss::ripplerpc] == "2.0");
1079 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1080 }
1081 {
1082 // alice should have 52 trust lines in the current ledger.
1083 auto const lines = env.rpc(
1084 "json2",
1085 "{ "
1086 R"("method" : "account_lines",)"
1087 R"("jsonrpc" : "2.0",)"
1088 R"("ripplerpc" : "2.0",)"
1089 R"("id" : 5,)"
1090 R"("params": )"
1091 R"({"account": ")" +
1092 alice.human() + R"("}})");
1093 BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
1094 BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
1096 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1098 lines.isMember(jss::ripplerpc) &&
1099 lines[jss::ripplerpc] == "2.0");
1100 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1101 }
1102 {
1103 // alice should have 26 trust lines with gw1.
1104 auto const lines = env.rpc(
1105 "json2",
1106 "{ "
1107 R"("method" : "account_lines",)"
1108 R"("jsonrpc" : "2.0",)"
1109 R"("ripplerpc" : "2.0",)"
1110 R"("id" : 5,)"
1111 R"("params": )"
1112 R"({"account": ")" +
1113 alice.human() +
1114 R"(", )"
1115 R"("peer": ")" +
1116 gw1.human() + R"("}})");
1117 BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
1118 BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
1120 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1122 lines.isMember(jss::ripplerpc) &&
1123 lines[jss::ripplerpc] == "2.0");
1124 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1125 }
1126 {
1127 // Use a malformed peer.
1128 auto const lines = env.rpc(
1129 "json2",
1130 "{ "
1131 R"("method" : "account_lines",)"
1132 R"("jsonrpc" : "2.0",)"
1133 R"("ripplerpc" : "2.0",)"
1134 R"("id" : 5,)"
1135 R"("params": )"
1136 R"({"account": ")" +
1137 alice.human() +
1138 R"(", )"
1139 R"("peer": )"
1140 R"("n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"}})");
1142 lines[jss::error][jss::message] ==
1143 RPC::make_error(rpcACT_MALFORMED)[jss::error_message]);
1145 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1147 lines.isMember(jss::ripplerpc) &&
1148 lines[jss::ripplerpc] == "2.0");
1149 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1150 }
1151 {
1152 // A negative limit should fail.
1153 auto const lines = env.rpc(
1154 "json2",
1155 "{ "
1156 R"("method" : "account_lines",)"
1157 R"("jsonrpc" : "2.0",)"
1158 R"("ripplerpc" : "2.0",)"
1159 R"("id" : 5,)"
1160 R"("params": )"
1161 R"({"account": ")" +
1162 alice.human() +
1163 R"(", )"
1164 R"("limit": -1}})");
1166 lines[jss::error][jss::message] ==
1167 RPC::expected_field_message(jss::limit, "unsigned integer"));
1169 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1171 lines.isMember(jss::ripplerpc) &&
1172 lines[jss::ripplerpc] == "2.0");
1173 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1174 }
1175 {
1176 // Limit the response to 1 trust line.
1177 auto const linesA = env.rpc(
1178 "json2",
1179 "{ "
1180 R"("method" : "account_lines",)"
1181 R"("jsonrpc" : "2.0",)"
1182 R"("ripplerpc" : "2.0",)"
1183 R"("id" : 5,)"
1184 R"("params": )"
1185 R"({"account": ")" +
1186 alice.human() +
1187 R"(", )"
1188 R"("limit": 1}})");
1189 BEAST_EXPECT(linesA[jss::result][jss::lines].isArray());
1190 BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1);
1192 linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
1194 linesA.isMember(jss::ripplerpc) &&
1195 linesA[jss::ripplerpc] == "2.0");
1196 BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
1197
1198 // Pick up from where the marker left off. We should get 51.
1199 auto marker = linesA[jss::result][jss::marker].asString();
1200 auto const linesB = env.rpc(
1201 "json2",
1202 "{ "
1203 R"("method" : "account_lines",)"
1204 R"("jsonrpc" : "2.0",)"
1205 R"("ripplerpc" : "2.0",)"
1206 R"("id" : 5,)"
1207 R"("params": )"
1208 R"({"account": ")" +
1209 alice.human() +
1210 R"(", )"
1211 R"("marker": ")" +
1212 marker + R"("}})");
1213 BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
1214 BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);
1216 linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
1218 linesB.isMember(jss::ripplerpc) &&
1219 linesB[jss::ripplerpc] == "2.0");
1220 BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
1221
1222 // Go again from where the marker left off, but set a limit of 3.
1223 auto const linesC = env.rpc(
1224 "json2",
1225 "{ "
1226 R"("method" : "account_lines",)"
1227 R"("jsonrpc" : "2.0",)"
1228 R"("ripplerpc" : "2.0",)"
1229 R"("id" : 5,)"
1230 R"("params": )"
1231 R"({"account": ")" +
1232 alice.human() +
1233 R"(", )"
1234 R"("limit": 3, )"
1235 R"("marker": ")" +
1236 marker + R"("}})");
1237 BEAST_EXPECT(linesC[jss::result][jss::lines].isArray());
1238 BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3);
1240 linesC.isMember(jss::jsonrpc) && linesC[jss::jsonrpc] == "2.0");
1242 linesC.isMember(jss::ripplerpc) &&
1243 linesC[jss::ripplerpc] == "2.0");
1244 BEAST_EXPECT(linesC.isMember(jss::id) && linesC[jss::id] == 5);
1245
1246 // Mess with the marker so it becomes bad and check for the error.
1247 marker[5] = marker[5] == '7' ? '8' : '7';
1248 auto const linesD = env.rpc(
1249 "json2",
1250 "{ "
1251 R"("method" : "account_lines",)"
1252 R"("jsonrpc" : "2.0",)"
1253 R"("ripplerpc" : "2.0",)"
1254 R"("id" : 5,)"
1255 R"("params": )"
1256 R"({"account": ")" +
1257 alice.human() +
1258 R"(", )"
1259 R"("marker": ")" +
1260 marker + R"("}})");
1262 linesD[jss::error][jss::message] ==
1263 RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
1265 linesD.isMember(jss::jsonrpc) && linesD[jss::jsonrpc] == "2.0");
1267 linesD.isMember(jss::ripplerpc) &&
1268 linesD[jss::ripplerpc] == "2.0");
1269 BEAST_EXPECT(linesD.isMember(jss::id) && linesD[jss::id] == 5);
1270 }
1271 {
1272 // A non-string marker should also fail.
1273 auto const lines = env.rpc(
1274 "json2",
1275 "{ "
1276 R"("method" : "account_lines",)"
1277 R"("jsonrpc" : "2.0",)"
1278 R"("ripplerpc" : "2.0",)"
1279 R"("id" : 5,)"
1280 R"("params": )"
1281 R"({"account": ")" +
1282 alice.human() +
1283 R"(", )"
1284 R"("marker": true}})");
1286 lines[jss::error][jss::message] ==
1287 RPC::expected_field_message(jss::marker, "string"));
1289 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1291 lines.isMember(jss::ripplerpc) &&
1292 lines[jss::ripplerpc] == "2.0");
1293 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1294 }
1295 {
1296 // Check that the flags we expect from alice to gw2 are present.
1297 auto const lines = env.rpc(
1298 "json2",
1299 "{ "
1300 R"("method" : "account_lines",)"
1301 R"("jsonrpc" : "2.0",)"
1302 R"("ripplerpc" : "2.0",)"
1303 R"("id" : 5,)"
1304 R"("params": )"
1305 R"({"account": ")" +
1306 alice.human() +
1307 R"(", )"
1308 R"("limit": 10, )"
1309 R"("peer": ")" +
1310 gw2.human() + R"("}})");
1311 auto const& line = lines[jss::result][jss::lines][0u];
1312 BEAST_EXPECT(line[jss::freeze].asBool() == true);
1313 BEAST_EXPECT(line[jss::deep_freeze].asBool() == true);
1314 BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
1315 BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
1317 lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1319 lines.isMember(jss::ripplerpc) &&
1320 lines[jss::ripplerpc] == "2.0");
1321 BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1322 }
1323 {
1324 // Check that the flags we expect from gw2 to alice are present.
1325 auto const linesA = env.rpc(
1326 "json2",
1327 "{ "
1328 R"("method" : "account_lines",)"
1329 R"("jsonrpc" : "2.0",)"
1330 R"("ripplerpc" : "2.0",)"
1331 R"("id" : 5,)"
1332 R"("params": )"
1333 R"({"account": ")" +
1334 gw2.human() +
1335 R"(", )"
1336 R"("limit": 1, )"
1337 R"("peer": ")" +
1338 alice.human() + R"("}})");
1339 auto const& lineA = linesA[jss::result][jss::lines][0u];
1340 BEAST_EXPECT(lineA[jss::freeze_peer].asBool() == true);
1341 BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true);
1342 BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
1343 BEAST_EXPECT(lineA[jss::authorized].asBool() == true);
1345 linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
1347 linesA.isMember(jss::ripplerpc) &&
1348 linesA[jss::ripplerpc] == "2.0");
1349 BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
1350
1351 // Continue from the returned marker to make sure that works.
1352 BEAST_EXPECT(linesA[jss::result].isMember(jss::marker));
1353 auto const marker = linesA[jss::result][jss::marker].asString();
1354 auto const linesB = env.rpc(
1355 "json2",
1356 "{ "
1357 R"("method" : "account_lines",)"
1358 R"("jsonrpc" : "2.0",)"
1359 R"("ripplerpc" : "2.0",)"
1360 R"("id" : 5,)"
1361 R"("params": )"
1362 R"({"account": ")" +
1363 gw2.human() +
1364 R"(", )"
1365 R"("limit": 25, )"
1366 R"("marker": ")" +
1367 marker +
1368 R"(", )"
1369 R"("peer": ")" +
1370 alice.human() + R"("}})");
1371 BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
1372 BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25);
1373 BEAST_EXPECT(!linesB[jss::result].isMember(jss::marker));
1375 linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
1377 linesB.isMember(jss::ripplerpc) &&
1378 linesB[jss::ripplerpc] == "2.0");
1379 BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
1380 }
1381 }
1382
1383 // test API V2
1384 void
1386 {
1387 testcase("V2: account_lines with removed marker");
1388
1389 using namespace test::jtx;
1390 Env env(*this);
1391
1392 // The goal here is to observe account_lines marker behavior if the
1393 // entry pointed at by a returned marker is removed from the ledger.
1394 //
1395 // It isn't easy to explicitly delete a trust line, so we do so in a
1396 // round-about fashion. It takes 4 actors:
1397 // o Gateway gw1 issues EUR
1398 // o alice offers to buy 100 EUR for 100 XRP.
1399 // o becky offers to sell 100 EUR for 100 XRP.
1400 // There will now be an inferred trustline between alice and gw2.
1401 // o alice pays her 100 EUR to cheri.
1402 // alice should now have no EUR and no trustline to gw2.
1403 Account const alice{"alice"};
1404 Account const becky{"becky"};
1405 Account const cheri{"cheri"};
1406 Account const gw1{"gw1"};
1407 Account const gw2{"gw2"};
1408 env.fund(XRP(10000), alice, becky, cheri, gw1, gw2);
1409 env.close();
1410
1411 auto const USD = gw1["USD"];
1412 auto const AUD = gw1["AUD"];
1413 auto const EUR = gw2["EUR"];
1414 env(trust(alice, USD(200)));
1415 env(trust(alice, AUD(200)));
1416 env(trust(becky, EUR(200)));
1417 env(trust(cheri, EUR(200)));
1418 env.close();
1419
1420 // becky gets 100 EUR from gw1.
1421 env(pay(gw2, becky, EUR(100)));
1422 env.close();
1423
1424 // alice offers to buy 100 EUR for 100 XRP.
1425 env(offer(alice, EUR(100), XRP(100)));
1426 env.close();
1427
1428 // becky offers to buy 100 XRP for 100 EUR.
1429 env(offer(becky, XRP(100), EUR(100)));
1430 env.close();
1431
1432 // Get account_lines for alice. Limit at 1, so we get a marker.
1433 auto const linesBeg = env.rpc(
1434 "json2",
1435 "{ "
1436 R"("method" : "account_lines",)"
1437 R"("jsonrpc" : "2.0",)"
1438 R"("ripplerpc" : "2.0",)"
1439 R"("id" : 5,)"
1440 R"("params": )"
1441 R"({"account": ")" +
1442 alice.human() +
1443 R"(", )"
1444 R"("limit": 2}})");
1446 linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD");
1447 BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker));
1449 linesBeg.isMember(jss::jsonrpc) && linesBeg[jss::jsonrpc] == "2.0");
1451 linesBeg.isMember(jss::ripplerpc) &&
1452 linesBeg[jss::ripplerpc] == "2.0");
1453 BEAST_EXPECT(linesBeg.isMember(jss::id) && linesBeg[jss::id] == 5);
1454
1455 // alice pays 100 USD to cheri.
1456 env(pay(alice, cheri, EUR(100)));
1457 env.close();
1458
1459 // Since alice paid all her EUR to cheri, alice should no longer
1460 // have a trust line to gw1. So the old marker should now be invalid.
1461 auto const linesEnd = env.rpc(
1462 "json2",
1463 "{ "
1464 R"("method" : "account_lines",)"
1465 R"("jsonrpc" : "2.0",)"
1466 R"("ripplerpc" : "2.0",)"
1467 R"("id" : 5,)"
1468 R"("params": )"
1469 R"({"account": ")" +
1470 alice.human() +
1471 R"(", )"
1472 R"("marker": ")" +
1473 linesBeg[jss::result][jss::marker].asString() + R"("}})");
1475 linesEnd[jss::error][jss::message] ==
1476 RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
1478 linesEnd.isMember(jss::jsonrpc) && linesEnd[jss::jsonrpc] == "2.0");
1480 linesEnd.isMember(jss::ripplerpc) &&
1481 linesEnd[jss::ripplerpc] == "2.0");
1482 BEAST_EXPECT(linesEnd.isMember(jss::id) && linesEnd[jss::id] == 5);
1483 }
1484
1485 void
1486 run() override
1487 {
1489 testAccountLinesMarker();
1490 testAccountLineDelete();
1491 testAccountLinesWalkMarkers();
1494 }
1495};
1496
1497BEAST_DEFINE_TESTSUITE(AccountLines, rpc, ripple);
1498
1499} // namespace RPC
1500} // namespace ripple
T back(T... args)
Represents a JSON value.
Definition json_value.h:149
bool isMember(char const *key) const
Return true if the object has a member named key.
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::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)
T is_same_v
@ nullValue
'null' value
Definition json_value.h:38
@ arrayValue
array value (ordered list)
Definition json_value.h:44
@ objectValue
object value (collection of name/value pairs).
Definition json_value.h:45
Json::Value make_error(error_code_i code)
Returns a new json object that reflects the error code.
std::string expected_field_message(std::string const &name, std::string const &type)
Definition ErrorCodes.h:337
Json::Value missing_field_error(std::string const &name)
Definition ErrorCodes.h:283
Keylet signers(AccountID const &account) noexcept
A SignerList.
Definition Indexes.cpp:330
Keylet offer(AccountID const &id, std::uint32_t seq) noexcept
An offer from an account.
Definition Indexes.cpp:274
Keylet payChan(AccountID const &src, AccountID const &dst, std::uint32_t seq) noexcept
A PaymentChannel.
Definition Indexes.cpp:395
Json::Value trust(AccountID const &account, STAmount const &amount, std::uint32_t flags=0)
Definition AMM.cpp:810
Json::Value pay(Account const &account, AccountID const &to, STAmount const &amount)
Definition AMM.cpp:822
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:111
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:25
constexpr std::uint32_t tfSetDeepFreeze
Definition TxFlags.h:120
@ 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:230
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:30
constexpr std::uint32_t tfSetfAuth
Definition TxFlags.h:115
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:118
constexpr std::uint32_t tfSetNoRipple
Definition TxFlags.h:116
constexpr std::uint32_t const tfTransferable
Definition TxFlags.h:142
T push_back(T... args)
T size(T... args)
Information about the notional ledger backing the view.
T substr(T... args)
T to_string(T... args)