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