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