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(rpcACT_MALFORMED)[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(rpcACT_MALFORMED)[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  constexpr std::size_t expectedNFTs = 1;
683  std::size_t foundLines = 0;
684 
685  auto hasMarker = [](auto const& aliceLines) {
686  return aliceLines[jss::result].isMember(jss::marker);
687  };
688  auto marker = [](auto const& aliceLines) {
689  return aliceLines[jss::result][jss::marker].asString();
690  };
691  auto checkLines = [](auto const& aliceLines) {
692  return aliceLines.isMember(jss::result) &&
693  !aliceLines[jss::result].isMember(jss::error_message) &&
694  aliceLines[jss::result].isMember(jss::lines) &&
695  aliceLines[jss::result][jss::lines].isArray() &&
696  aliceLines[jss::result][jss::lines].size() <= 1;
697  };
698 
699  BEAST_EXPECT(hasMarker(aliceLines));
700  BEAST_EXPECT(checkLines(aliceLines));
701  BEAST_EXPECT(aliceLines[jss::result][jss::lines].size() == 0);
702 
703  int iterations = 1;
704 
705  while (hasMarker(aliceLines))
706  {
707  // Iterate through the markers
708  aliceLines = getNextLine(env, alice, marker(aliceLines));
709  BEAST_EXPECT(checkLines(aliceLines));
710  foundLines += aliceLines[jss::result][jss::lines].size();
711  ++iterations;
712  }
713  BEAST_EXPECT(expectedLines == foundLines);
714 
715  Json::Value const aliceObjects = env.rpc(
716  "json",
717  "account_objects",
718  R"({"account": ")" + alice.human() +
719  R"(", )"
720  R"("limit": 200})");
721  BEAST_EXPECT(aliceObjects.isMember(jss::result));
722  BEAST_EXPECT(
723  !aliceObjects[jss::result].isMember(jss::error_message));
724  BEAST_EXPECT(
725  aliceObjects[jss::result].isMember(jss::account_objects));
726  BEAST_EXPECT(
727  aliceObjects[jss::result][jss::account_objects].isArray());
728  // account_objects does not currently return NFTPages. If
729  // that ever changes, without also changing account_lines,
730  // this test will need to be updated.
731  BEAST_EXPECT(
732  aliceObjects[jss::result][jss::account_objects].size() ==
733  iterations + expectedNFTs);
734  // If ledger object association ever changes, for whatever
735  // reason, this test will need to be updated.
736  BEAST_EXPECTS(
737  iterations == expectedIterations, std::to_string(iterations));
738 
739  // Get becky's objects just to confirm that they're symmetrical
740  Json::Value const beckyObjects = env.rpc(
741  "json",
742  "account_objects",
743  R"({"account": ")" + becky.human() +
744  R"(", )"
745  R"("limit": 200})");
746  BEAST_EXPECT(beckyObjects.isMember(jss::result));
747  BEAST_EXPECT(
748  !beckyObjects[jss::result].isMember(jss::error_message));
749  BEAST_EXPECT(
750  beckyObjects[jss::result].isMember(jss::account_objects));
751  BEAST_EXPECT(
752  beckyObjects[jss::result][jss::account_objects].isArray());
753  // becky should have the same number of objects as alice, except the
754  // 2 tickets that only alice created.
755  BEAST_EXPECT(
756  beckyObjects[jss::result][jss::account_objects].size() ==
757  aliceObjects[jss::result][jss::account_objects].size() - 2);
758  }
759  }
760 
761  // test API V2
762  void
764  {
765  testcase("V2: account_lines");
766 
767  using namespace test::jtx;
768  Env env(*this);
769  {
770  // account_lines with mal-formed json2 (missing id field).
771  auto const lines = env.rpc(
772  "json2",
773  "{ "
774  R"("method" : "account_lines",)"
775  R"("jsonrpc" : "2.0",)"
776  R"("ripplerpc" : "2.0")"
777  " }");
778  BEAST_EXPECT(
779  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
780  BEAST_EXPECT(
781  lines.isMember(jss::ripplerpc) &&
782  lines[jss::ripplerpc] == "2.0");
783  }
784  {
785  // account_lines with no account.
786  auto const lines = env.rpc(
787  "json2",
788  "{ "
789  R"("method" : "account_lines",)"
790  R"("jsonrpc" : "2.0",)"
791  R"("ripplerpc" : "2.0",)"
792  R"("id" : 5)"
793  " }");
794  BEAST_EXPECT(
795  lines[jss::error][jss::message] ==
796  RPC::missing_field_error(jss::account)[jss::error_message]);
797  BEAST_EXPECT(
798  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
799  BEAST_EXPECT(
800  lines.isMember(jss::ripplerpc) &&
801  lines[jss::ripplerpc] == "2.0");
802  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
803  }
804  {
805  // account_lines with a malformed account.
806  auto const lines = env.rpc(
807  "json2",
808  "{ "
809  R"("method" : "account_lines",)"
810  R"("jsonrpc" : "2.0",)"
811  R"("ripplerpc" : "2.0",)"
812  R"("id" : 5,)"
813  R"("params": )"
814  R"({"account": )"
815  R"("n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"}})");
816  BEAST_EXPECT(
817  lines[jss::error][jss::message] ==
818  RPC::make_error(rpcACT_MALFORMED)[jss::error_message]);
819  BEAST_EXPECT(
820  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
821  BEAST_EXPECT(
822  lines.isMember(jss::ripplerpc) &&
823  lines[jss::ripplerpc] == "2.0");
824  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
825  }
826  Account const alice{"alice"};
827  {
828  // account_lines on an unfunded account.
829  auto const lines = env.rpc(
830  "json2",
831  "{ "
832  R"("method" : "account_lines",)"
833  R"("jsonrpc" : "2.0",)"
834  R"("ripplerpc" : "2.0",)"
835  R"("id" : 5,)"
836  R"("params": )"
837  R"({"account": ")" +
838  alice.human() + R"("}})");
839  BEAST_EXPECT(
840  lines[jss::error][jss::message] ==
841  RPC::make_error(rpcACT_NOT_FOUND)[jss::error_message]);
842  BEAST_EXPECT(
843  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
844  BEAST_EXPECT(
845  lines.isMember(jss::ripplerpc) &&
846  lines[jss::ripplerpc] == "2.0");
847  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
848  }
849  env.fund(XRP(10000), alice);
850  env.close();
851  LedgerInfo const ledger3Info = env.closed()->info();
853 
854  {
855  // alice is funded but has no lines. An empty array is returned.
856  auto const lines = env.rpc(
857  "json2",
858  "{ "
859  R"("method" : "account_lines",)"
860  R"("jsonrpc" : "2.0",)"
861  R"("ripplerpc" : "2.0",)"
862  R"("id" : 5,)"
863  R"("params": )"
864  R"({"account": ")" +
865  alice.human() + R"("}})");
866  BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
867  BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0);
868  BEAST_EXPECT(
869  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
870  BEAST_EXPECT(
871  lines.isMember(jss::ripplerpc) &&
872  lines[jss::ripplerpc] == "2.0");
873  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
874  }
875  {
876  // Specify a ledger that doesn't exist.
877  auto const lines = env.rpc(
878  "json2",
879  "{ "
880  R"("method" : "account_lines",)"
881  R"("jsonrpc" : "2.0",)"
882  R"("ripplerpc" : "2.0",)"
883  R"("id" : 5,)"
884  R"("params": )"
885  R"({"account": ")" +
886  alice.human() +
887  R"(", )"
888  R"("ledger_index": "nonsense"}})");
889  BEAST_EXPECT(
890  lines[jss::error][jss::message] == "ledgerIndexMalformed");
891  BEAST_EXPECT(
892  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
893  BEAST_EXPECT(
894  lines.isMember(jss::ripplerpc) &&
895  lines[jss::ripplerpc] == "2.0");
896  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
897  }
898  {
899  // Specify a different ledger that doesn't exist.
900  auto const lines = env.rpc(
901  "json2",
902  "{ "
903  R"("method" : "account_lines",)"
904  R"("jsonrpc" : "2.0",)"
905  R"("ripplerpc" : "2.0",)"
906  R"("id" : 5,)"
907  R"("params": )"
908  R"({"account": ")" +
909  alice.human() +
910  R"(", )"
911  R"("ledger_index": 50000}})");
912  BEAST_EXPECT(lines[jss::error][jss::message] == "ledgerNotFound");
913  BEAST_EXPECT(
914  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
915  BEAST_EXPECT(
916  lines.isMember(jss::ripplerpc) &&
917  lines[jss::ripplerpc] == "2.0");
918  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
919  }
920  // Create trust lines to share with alice.
921  Account const gw1{"gw1"};
922  env.fund(XRP(10000), gw1);
924 
925  for (char c = 0; c <= ('Z' - 'A'); ++c)
926  {
927  // gw1 currencies have names "YAA" -> "YAZ".
929  gw1[std::string("YA") + static_cast<char>('A' + c)]);
930  IOU const& gw1Currency = gw1Currencies.back();
931 
932  // Establish trust lines.
933  env(trust(alice, gw1Currency(100 + c)));
934  env(pay(gw1, alice, gw1Currency(50 + c)));
935  }
936  env.close();
937  LedgerInfo const ledger4Info = env.closed()->info();
939 
940  // Add another set of trust lines in another ledger so we can see
941  // differences in historic ledgers.
942  Account const gw2{"gw2"};
943  env.fund(XRP(10000), gw2);
944 
945  // gw2 requires authorization.
946  env(fset(gw2, asfRequireAuth));
947  env.close();
949 
950  for (char c = 0; c <= ('Z' - 'A'); ++c)
951  {
952  // gw2 currencies have names "ZAA" -> "ZAZ".
954  gw2[std::string("ZA") + static_cast<char>('A' + c)]);
955  IOU const& gw2Currency = gw2Currencies.back();
956 
957  // Establish trust lines.
958  env(trust(alice, gw2Currency(200 + c)));
959  env(trust(gw2, gw2Currency(0), alice, tfSetfAuth));
960  env.close();
961  env(pay(gw2, alice, gw2Currency(100 + c)));
962  env.close();
963 
964  // Set flags on gw2 trust lines so we can look for them.
965  env(trust(alice, gw2Currency(0), gw2, tfSetNoRipple | tfSetFreeze));
966  }
967  env.close();
968  LedgerInfo const ledger58Info = env.closed()->info();
970 
971  // A re-usable test for historic ledgers.
972  auto testAccountLinesHistory = [this, &env](
973  Account const& account,
974  LedgerInfo const& info,
975  int count) {
976  // Get account_lines by ledger index.
977  auto const linesSeq = env.rpc(
978  "json2",
979  "{ "
980  R"("method" : "account_lines",)"
981  R"("jsonrpc" : "2.0",)"
982  R"("ripplerpc" : "2.0",)"
983  R"("id" : 5,)"
984  R"("params": )"
985  R"({"account": ")" +
986  account.human() +
987  R"(", )"
988  R"("ledger_index": )" +
989  std::to_string(info.seq) + "}}");
990  BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
991  BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
992  BEAST_EXPECT(
993  linesSeq.isMember(jss::jsonrpc) &&
994  linesSeq[jss::jsonrpc] == "2.0");
995  BEAST_EXPECT(
996  linesSeq.isMember(jss::ripplerpc) &&
997  linesSeq[jss::ripplerpc] == "2.0");
998  BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5);
999 
1000  // Get account_lines by ledger hash.
1001  auto const linesHash = env.rpc(
1002  "json2",
1003  "{ "
1004  R"("method" : "account_lines",)"
1005  R"("jsonrpc" : "2.0",)"
1006  R"("ripplerpc" : "2.0",)"
1007  R"("id" : 5,)"
1008  R"("params": )"
1009  R"({"account": ")" +
1010  account.human() +
1011  R"(", )"
1012  R"("ledger_hash": ")" +
1013  to_string(info.hash) + R"("}})");
1014  BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
1015  BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
1016  BEAST_EXPECT(
1017  linesHash.isMember(jss::jsonrpc) &&
1018  linesHash[jss::jsonrpc] == "2.0");
1019  BEAST_EXPECT(
1020  linesHash.isMember(jss::ripplerpc) &&
1021  linesHash[jss::ripplerpc] == "2.0");
1022  BEAST_EXPECT(
1023  linesHash.isMember(jss::id) && linesHash[jss::id] == 5);
1024  };
1025 
1026  // Alice should have no trust lines in ledger 3.
1028 
1029  // Alice should have 26 trust lines in ledger 4.
1031 
1032  // Alice should have 52 trust lines in ledger 58.
1034 
1035  {
1036  // Surprisingly, it's valid to specify both index and hash, in
1037  // which case the hash wins.
1038  auto const lines = env.rpc(
1039  "json2",
1040  "{ "
1041  R"("method" : "account_lines",)"
1042  R"("jsonrpc" : "2.0",)"
1043  R"("ripplerpc" : "2.0",)"
1044  R"("id" : 5,)"
1045  R"("params": )"
1046  R"({"account": ")" +
1047  alice.human() +
1048  R"(", )"
1049  R"("ledger_hash": ")" +
1051  R"(", )"
1052  R"("ledger_index": )" +
1053  std::to_string(ledger58Info.seq) + "}}");
1054  BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
1055  BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
1056  BEAST_EXPECT(
1057  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1058  BEAST_EXPECT(
1059  lines.isMember(jss::ripplerpc) &&
1060  lines[jss::ripplerpc] == "2.0");
1061  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1062  }
1063  {
1064  // alice should have 52 trust lines in the current ledger.
1065  auto const lines = env.rpc(
1066  "json2",
1067  "{ "
1068  R"("method" : "account_lines",)"
1069  R"("jsonrpc" : "2.0",)"
1070  R"("ripplerpc" : "2.0",)"
1071  R"("id" : 5,)"
1072  R"("params": )"
1073  R"({"account": ")" +
1074  alice.human() + R"("}})");
1075  BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
1076  BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
1077  BEAST_EXPECT(
1078  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1079  BEAST_EXPECT(
1080  lines.isMember(jss::ripplerpc) &&
1081  lines[jss::ripplerpc] == "2.0");
1082  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1083  }
1084  {
1085  // alice should have 26 trust lines with gw1.
1086  auto const lines = env.rpc(
1087  "json2",
1088  "{ "
1089  R"("method" : "account_lines",)"
1090  R"("jsonrpc" : "2.0",)"
1091  R"("ripplerpc" : "2.0",)"
1092  R"("id" : 5,)"
1093  R"("params": )"
1094  R"({"account": ")" +
1095  alice.human() +
1096  R"(", )"
1097  R"("peer": ")" +
1098  gw1.human() + R"("}})");
1099  BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
1100  BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
1101  BEAST_EXPECT(
1102  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1103  BEAST_EXPECT(
1104  lines.isMember(jss::ripplerpc) &&
1105  lines[jss::ripplerpc] == "2.0");
1106  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1107  }
1108  {
1109  // Use a malformed peer.
1110  auto const lines = env.rpc(
1111  "json2",
1112  "{ "
1113  R"("method" : "account_lines",)"
1114  R"("jsonrpc" : "2.0",)"
1115  R"("ripplerpc" : "2.0",)"
1116  R"("id" : 5,)"
1117  R"("params": )"
1118  R"({"account": ")" +
1119  alice.human() +
1120  R"(", )"
1121  R"("peer": )"
1122  R"("n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"}})");
1123  BEAST_EXPECT(
1124  lines[jss::error][jss::message] ==
1125  RPC::make_error(rpcACT_MALFORMED)[jss::error_message]);
1126  BEAST_EXPECT(
1127  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1128  BEAST_EXPECT(
1129  lines.isMember(jss::ripplerpc) &&
1130  lines[jss::ripplerpc] == "2.0");
1131  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1132  }
1133  {
1134  // A negative limit should fail.
1135  auto const lines = env.rpc(
1136  "json2",
1137  "{ "
1138  R"("method" : "account_lines",)"
1139  R"("jsonrpc" : "2.0",)"
1140  R"("ripplerpc" : "2.0",)"
1141  R"("id" : 5,)"
1142  R"("params": )"
1143  R"({"account": ")" +
1144  alice.human() +
1145  R"(", )"
1146  R"("limit": -1}})");
1147  BEAST_EXPECT(
1148  lines[jss::error][jss::message] ==
1149  RPC::expected_field_message(jss::limit, "unsigned integer"));
1150  BEAST_EXPECT(
1151  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1152  BEAST_EXPECT(
1153  lines.isMember(jss::ripplerpc) &&
1154  lines[jss::ripplerpc] == "2.0");
1155  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1156  }
1157  {
1158  // Limit the response to 1 trust line.
1159  auto const linesA = env.rpc(
1160  "json2",
1161  "{ "
1162  R"("method" : "account_lines",)"
1163  R"("jsonrpc" : "2.0",)"
1164  R"("ripplerpc" : "2.0",)"
1165  R"("id" : 5,)"
1166  R"("params": )"
1167  R"({"account": ")" +
1168  alice.human() +
1169  R"(", )"
1170  R"("limit": 1}})");
1171  BEAST_EXPECT(linesA[jss::result][jss::lines].isArray());
1172  BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1);
1173  BEAST_EXPECT(
1174  linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
1175  BEAST_EXPECT(
1176  linesA.isMember(jss::ripplerpc) &&
1177  linesA[jss::ripplerpc] == "2.0");
1178  BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
1179 
1180  // Pick up from where the marker left off. We should get 51.
1181  auto marker = linesA[jss::result][jss::marker].asString();
1182  auto const linesB = env.rpc(
1183  "json2",
1184  "{ "
1185  R"("method" : "account_lines",)"
1186  R"("jsonrpc" : "2.0",)"
1187  R"("ripplerpc" : "2.0",)"
1188  R"("id" : 5,)"
1189  R"("params": )"
1190  R"({"account": ")" +
1191  alice.human() +
1192  R"(", )"
1193  R"("marker": ")" +
1194  marker + R"("}})");
1195  BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
1196  BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);
1197  BEAST_EXPECT(
1198  linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
1199  BEAST_EXPECT(
1200  linesB.isMember(jss::ripplerpc) &&
1201  linesB[jss::ripplerpc] == "2.0");
1202  BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
1203 
1204  // Go again from where the marker left off, but set a limit of 3.
1205  auto const linesC = env.rpc(
1206  "json2",
1207  "{ "
1208  R"("method" : "account_lines",)"
1209  R"("jsonrpc" : "2.0",)"
1210  R"("ripplerpc" : "2.0",)"
1211  R"("id" : 5,)"
1212  R"("params": )"
1213  R"({"account": ")" +
1214  alice.human() +
1215  R"(", )"
1216  R"("limit": 3, )"
1217  R"("marker": ")" +
1218  marker + R"("}})");
1219  BEAST_EXPECT(linesC[jss::result][jss::lines].isArray());
1220  BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3);
1221  BEAST_EXPECT(
1222  linesC.isMember(jss::jsonrpc) && linesC[jss::jsonrpc] == "2.0");
1223  BEAST_EXPECT(
1224  linesC.isMember(jss::ripplerpc) &&
1225  linesC[jss::ripplerpc] == "2.0");
1226  BEAST_EXPECT(linesC.isMember(jss::id) && linesC[jss::id] == 5);
1227 
1228  // Mess with the marker so it becomes bad and check for the error.
1229  marker[5] = marker[5] == '7' ? '8' : '7';
1230  auto const linesD = env.rpc(
1231  "json2",
1232  "{ "
1233  R"("method" : "account_lines",)"
1234  R"("jsonrpc" : "2.0",)"
1235  R"("ripplerpc" : "2.0",)"
1236  R"("id" : 5,)"
1237  R"("params": )"
1238  R"({"account": ")" +
1239  alice.human() +
1240  R"(", )"
1241  R"("marker": ")" +
1242  marker + R"("}})");
1243  BEAST_EXPECT(
1244  linesD[jss::error][jss::message] ==
1245  RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
1246  BEAST_EXPECT(
1247  linesD.isMember(jss::jsonrpc) && linesD[jss::jsonrpc] == "2.0");
1248  BEAST_EXPECT(
1249  linesD.isMember(jss::ripplerpc) &&
1250  linesD[jss::ripplerpc] == "2.0");
1251  BEAST_EXPECT(linesD.isMember(jss::id) && linesD[jss::id] == 5);
1252  }
1253  {
1254  // A non-string marker should also fail.
1255  auto const lines = env.rpc(
1256  "json2",
1257  "{ "
1258  R"("method" : "account_lines",)"
1259  R"("jsonrpc" : "2.0",)"
1260  R"("ripplerpc" : "2.0",)"
1261  R"("id" : 5,)"
1262  R"("params": )"
1263  R"({"account": ")" +
1264  alice.human() +
1265  R"(", )"
1266  R"("marker": true}})");
1267  BEAST_EXPECT(
1268  lines[jss::error][jss::message] ==
1269  RPC::expected_field_message(jss::marker, "string"));
1270  BEAST_EXPECT(
1271  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1272  BEAST_EXPECT(
1273  lines.isMember(jss::ripplerpc) &&
1274  lines[jss::ripplerpc] == "2.0");
1275  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1276  }
1277  {
1278  // Check that the flags we expect from alice to gw2 are present.
1279  auto const lines = env.rpc(
1280  "json2",
1281  "{ "
1282  R"("method" : "account_lines",)"
1283  R"("jsonrpc" : "2.0",)"
1284  R"("ripplerpc" : "2.0",)"
1285  R"("id" : 5,)"
1286  R"("params": )"
1287  R"({"account": ")" +
1288  alice.human() +
1289  R"(", )"
1290  R"("limit": 10, )"
1291  R"("peer": ")" +
1292  gw2.human() + R"("}})");
1293  auto const& line = lines[jss::result][jss::lines][0u];
1294  BEAST_EXPECT(line[jss::freeze].asBool() == true);
1295  BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
1296  BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
1297  BEAST_EXPECT(
1298  lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
1299  BEAST_EXPECT(
1300  lines.isMember(jss::ripplerpc) &&
1301  lines[jss::ripplerpc] == "2.0");
1302  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
1303  }
1304  {
1305  // Check that the flags we expect from gw2 to alice are present.
1306  auto const linesA = env.rpc(
1307  "json2",
1308  "{ "
1309  R"("method" : "account_lines",)"
1310  R"("jsonrpc" : "2.0",)"
1311  R"("ripplerpc" : "2.0",)"
1312  R"("id" : 5,)"
1313  R"("params": )"
1314  R"({"account": ")" +
1315  gw2.human() +
1316  R"(", )"
1317  R"("limit": 1, )"
1318  R"("peer": ")" +
1319  alice.human() + R"("}})");
1320  auto const& lineA = linesA[jss::result][jss::lines][0u];
1321  BEAST_EXPECT(lineA[jss::freeze_peer].asBool() == true);
1322  BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
1323  BEAST_EXPECT(lineA[jss::authorized].asBool() == true);
1324  BEAST_EXPECT(
1325  linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
1326  BEAST_EXPECT(
1327  linesA.isMember(jss::ripplerpc) &&
1328  linesA[jss::ripplerpc] == "2.0");
1329  BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
1330 
1331  // Continue from the returned marker to make sure that works.
1332  BEAST_EXPECT(linesA[jss::result].isMember(jss::marker));
1333  auto const marker = linesA[jss::result][jss::marker].asString();
1334  auto const linesB = env.rpc(
1335  "json2",
1336  "{ "
1337  R"("method" : "account_lines",)"
1338  R"("jsonrpc" : "2.0",)"
1339  R"("ripplerpc" : "2.0",)"
1340  R"("id" : 5,)"
1341  R"("params": )"
1342  R"({"account": ")" +
1343  gw2.human() +
1344  R"(", )"
1345  R"("limit": 25, )"
1346  R"("marker": ")" +
1347  marker +
1348  R"(", )"
1349  R"("peer": ")" +
1350  alice.human() + R"("}})");
1351  BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
1352  BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25);
1353  BEAST_EXPECT(!linesB[jss::result].isMember(jss::marker));
1354  BEAST_EXPECT(
1355  linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
1356  BEAST_EXPECT(
1357  linesB.isMember(jss::ripplerpc) &&
1358  linesB[jss::ripplerpc] == "2.0");
1359  BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
1360  }
1361  }
1362 
1363  // test API V2
1364  void
1366  {
1367  testcase("V2: account_lines with removed marker");
1368 
1369  using namespace test::jtx;
1370  Env env(*this);
1371 
1372  // The goal here is to observe account_lines marker behavior if the
1373  // entry pointed at by a returned marker is removed from the ledger.
1374  //
1375  // It isn't easy to explicitly delete a trust line, so we do so in a
1376  // round-about fashion. It takes 4 actors:
1377  // o Gateway gw1 issues EUR
1378  // o alice offers to buy 100 EUR for 100 XRP.
1379  // o becky offers to sell 100 EUR for 100 XRP.
1380  // There will now be an inferred trustline between alice and gw2.
1381  // o alice pays her 100 EUR to cheri.
1382  // alice should now have no EUR and no trustline to gw2.
1383  Account const alice{"alice"};
1384  Account const becky{"becky"};
1385  Account const cheri{"cheri"};
1386  Account const gw1{"gw1"};
1387  Account const gw2{"gw2"};
1388  env.fund(XRP(10000), alice, becky, cheri, gw1, gw2);
1389  env.close();
1390 
1391  auto const USD = gw1["USD"];
1392  auto const AUD = gw1["AUD"];
1393  auto const EUR = gw2["EUR"];
1394  env(trust(alice, USD(200)));
1395  env(trust(alice, AUD(200)));
1396  env(trust(becky, EUR(200)));
1397  env(trust(cheri, EUR(200)));
1398  env.close();
1399 
1400  // becky gets 100 EUR from gw1.
1401  env(pay(gw2, becky, EUR(100)));
1402  env.close();
1403 
1404  // alice offers to buy 100 EUR for 100 XRP.
1405  env(offer(alice, EUR(100), XRP(100)));
1406  env.close();
1407 
1408  // becky offers to buy 100 XRP for 100 EUR.
1409  env(offer(becky, XRP(100), EUR(100)));
1410  env.close();
1411 
1412  // Get account_lines for alice. Limit at 1, so we get a marker.
1413  auto const linesBeg = env.rpc(
1414  "json2",
1415  "{ "
1416  R"("method" : "account_lines",)"
1417  R"("jsonrpc" : "2.0",)"
1418  R"("ripplerpc" : "2.0",)"
1419  R"("id" : 5,)"
1420  R"("params": )"
1421  R"({"account": ")" +
1422  alice.human() +
1423  R"(", )"
1424  R"("limit": 2}})");
1425  BEAST_EXPECT(
1426  linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD");
1427  BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker));
1428  BEAST_EXPECT(
1429  linesBeg.isMember(jss::jsonrpc) && linesBeg[jss::jsonrpc] == "2.0");
1430  BEAST_EXPECT(
1431  linesBeg.isMember(jss::ripplerpc) &&
1432  linesBeg[jss::ripplerpc] == "2.0");
1433  BEAST_EXPECT(linesBeg.isMember(jss::id) && linesBeg[jss::id] == 5);
1434 
1435  // alice pays 100 USD to cheri.
1436  env(pay(alice, cheri, EUR(100)));
1437  env.close();
1438 
1439  // Since alice paid all her EUR to cheri, alice should no longer
1440  // have a trust line to gw1. So the old marker should now be invalid.
1441  auto const linesEnd = env.rpc(
1442  "json2",
1443  "{ "
1444  R"("method" : "account_lines",)"
1445  R"("jsonrpc" : "2.0",)"
1446  R"("ripplerpc" : "2.0",)"
1447  R"("id" : 5,)"
1448  R"("params": )"
1449  R"({"account": ")" +
1450  alice.human() +
1451  R"(", )"
1452  R"("marker": ")" +
1453  linesBeg[jss::result][jss::marker].asString() + R"("}})");
1454  BEAST_EXPECT(
1455  linesEnd[jss::error][jss::message] ==
1456  RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
1457  BEAST_EXPECT(
1458  linesEnd.isMember(jss::jsonrpc) && linesEnd[jss::jsonrpc] == "2.0");
1459  BEAST_EXPECT(
1460  linesEnd.isMember(jss::ripplerpc) &&
1461  linesEnd[jss::ripplerpc] == "2.0");
1462  BEAST_EXPECT(linesEnd.isMember(jss::id) && linesEnd[jss::id] == 5);
1463  }
1464 
1465  void
1466  run() override
1467  {
1468  testAccountLines();
1469  testAccountLinesMarker();
1470  testAccountLineDelete();
1471  testAccountLinesWalkMarkers();
1474  }
1475 };
1476 
1477 BEAST_DEFINE_TESTSUITE(AccountLinesRPC, app, ripple);
1478 
1479 } // namespace RPC
1480 } // 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:1365
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:1181
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:851
ripple::RPC::AccountLinesRPC_test::gw1Currencies
std::vector< IOU > gw1Currencies
Definition: AccountLinesRPC_test.cpp:923
ripple::RPC::AccountLinesRPC_test::run
void run() override
Definition: AccountLinesRPC_test.cpp:1466
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:972
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:968
ripple::RPC::AccountLinesRPC_test::gw1
const Account gw1
Definition: AccountLinesRPC_test.cpp:921
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:1293
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::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:1230
ripple::RPC::AccountLinesRPC_test::ledger4Info
const LedgerInfo ledger4Info
Definition: AccountLinesRPC_test.cpp:937
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:948
ripple::RPC::AccountLinesRPC_test::linesB
const auto linesB
Definition: AccountLinesRPC_test.cpp:1182
ripple::RPC::AccountLinesRPC_test::linesC
const auto linesC
Definition: AccountLinesRPC_test.cpp:1205
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:942
ripple::rpcACT_MALFORMED
@ rpcACT_MALFORMED
Definition: ErrorCodes.h:90
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:1320
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:763
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