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