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/protocol/ErrorCodes.h>
21 #include <ripple/protocol/jss.h>
22 #include <ripple/protocol/TxFlags.h>
23 #include <test/jtx.h>
24 #include <ripple/beast/unit_test.h>
25 
26 namespace ripple {
27 
28 namespace RPC {
29 
30 class AccountLinesRPC_test : public beast::unit_test::suite
31 {
32 public:
34  {
35  testcase ("acccount_lines");
36 
37  using namespace test::jtx;
38  Env env(*this);
39  {
40  // account_lines with no account.
41  auto const lines = env.rpc ("json", "account_lines", "{ }");
42  BEAST_EXPECT(lines[jss::result][jss::error_message] ==
43  RPC::missing_field_error(jss::account)[jss::error_message]);
44  }
45  {
46  // account_lines with a malformed account.
47  auto const lines = env.rpc ("json", "account_lines",
48  R"({"account": )"
49  R"("n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"})");
50  BEAST_EXPECT(lines[jss::result][jss::error_message] ==
51  RPC::make_error(rpcBAD_SEED)[jss::error_message]);
52  }
53  Account const alice {"alice"};
54  {
55  // account_lines on an unfunded account.
56  auto const lines = env.rpc ("json", "account_lines",
57  R"({"account": ")" + alice.human() + R"("})");
58  BEAST_EXPECT(lines[jss::result][jss::error_message] ==
59  RPC::make_error(rpcACT_NOT_FOUND)[jss::error_message]);
60  }
61  env.fund(XRP(10000), alice);
62  env.close();
63  LedgerInfo const ledger3Info = env.closed()->info();
64  BEAST_EXPECT(ledger3Info.seq == 3);
65 
66  {
67  // alice is funded but has no lines. An empty array is returned.
68  auto const lines = env.rpc ("json", "account_lines",
69  R"({"account": ")" + alice.human() + R"("})");
70  BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
71  BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0);
72  }
73  {
74  // Specify a ledger that doesn't exist.
75  auto const lines = env.rpc ("json", "account_lines",
76  R"({"account": ")" + alice.human() + R"(", )"
77  R"("ledger_index": "nonsense"})");
78  BEAST_EXPECT(lines[jss::result][jss::error_message] ==
79  "ledgerIndexMalformed");
80  }
81  {
82  // Specify a different ledger that doesn't exist.
83  auto const lines = env.rpc ("json", "account_lines",
84  R"({"account": ")" + alice.human() + R"(", )"
85  R"("ledger_index": 50000})");
86  BEAST_EXPECT(lines[jss::result][jss::error_message] ==
87  "ledgerNotFound");
88  }
89  // Create trust lines to share with alice.
90  Account const gw1 {"gw1"};
91  env.fund(XRP(10000), gw1);
92  std::vector<IOU> gw1Currencies;
93 
94  for (char c = 0; c <= ('Z' - 'A'); ++c)
95  {
96  // gw1 currencies have names "YAA" -> "YAZ".
97  gw1Currencies.push_back(
98  gw1[std::string("YA") + static_cast<char>('A' + c)]);
99  IOU const& gw1Currency = gw1Currencies.back();
100 
101  // Establish trust lines.
102  env(trust(alice, gw1Currency(100 + c)));
103  env(pay(gw1, alice, gw1Currency(50 + c)));
104  }
105  env.close();
106  LedgerInfo const ledger4Info = env.closed()->info();
107  BEAST_EXPECT(ledger4Info.seq == 4);
108 
109  // Add another set of trust lines in another ledger so we can see
110  // differences in historic ledgers.
111  Account const gw2 {"gw2"};
112  env.fund(XRP(10000), gw2);
113 
114  // gw2 requires authorization.
115  env(fset(gw2, asfRequireAuth));
116  env.close();
117  std::vector<IOU> gw2Currencies;
118 
119  for (char c = 0; c <= ('Z' - 'A'); ++c)
120  {
121  // gw2 currencies have names "ZAA" -> "ZAZ".
122  gw2Currencies.push_back(
123  gw2[std::string("ZA") + static_cast<char>('A' + c)]);
124  IOU const& gw2Currency = gw2Currencies.back();
125 
126  // Establish trust lines.
127  env(trust(alice, gw2Currency(200 + c)));
128  env(trust(gw2, gw2Currency(0), alice, tfSetfAuth));
129  env.close();
130  env(pay(gw2, alice, gw2Currency(100 + c)));
131  env.close();
132 
133  // Set flags on gw2 trust lines so we can look for them.
134  env(trust(alice, gw2Currency(0), gw2, tfSetNoRipple | tfSetFreeze));
135  }
136  env.close();
137  LedgerInfo const ledger58Info = env.closed()->info();
138  BEAST_EXPECT(ledger58Info.seq == 58);
139 
140  // A re-usable test for historic ledgers.
141  auto testAccountLinesHistory =
142  [this, &env](Account const& account, LedgerInfo const& info, int count)
143  {
144  // Get account_lines by ledger index.
145  auto const linesSeq = env.rpc ("json", "account_lines",
146  R"({"account": ")" + account.human() + R"(", )"
147  R"("ledger_index": )" + std::to_string(info.seq) + "}");
148  BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
149  BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
150 
151  // Get account_lines by ledger hash.
152  auto const linesHash = env.rpc ("json", "account_lines",
153  R"({"account": ")" + account.human() + R"(", )"
154  R"("ledger_hash": ")" + to_string(info.hash) + R"("})");
155  BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
156  BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
157  };
158 
159  // Alice should have no trust lines in ledger 3.
160  testAccountLinesHistory (alice, ledger3Info, 0);
161 
162  // Alice should have 26 trust lines in ledger 4.
163  testAccountLinesHistory (alice, ledger4Info, 26);
164 
165  // Alice should have 52 trust lines in ledger 58.
166  testAccountLinesHistory (alice, ledger58Info, 52);
167 
168  {
169  // Surprisingly, it's valid to specify both index and hash, in
170  // which case the hash wins.
171  auto const lines = env.rpc ("json", "account_lines",
172  R"({"account": ")" + alice.human() + R"(", )"
173  R"("ledger_hash": ")" + to_string(ledger4Info.hash) + R"(", )"
174  R"("ledger_index": )" + std::to_string(ledger58Info.seq) + "}");
175  BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
176  BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
177  }
178  {
179  // alice should have 52 trust lines in the current ledger.
180  auto const lines = env.rpc ("json", "account_lines",
181  R"({"account": ")" + alice.human() + R"("})");
182  BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
183  BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
184  }
185  {
186  // alice should have 26 trust lines with gw1.
187  auto const lines = env.rpc ("json", "account_lines",
188  R"({"account": ")" + alice.human() + R"(", )"
189  R"("peer": ")" + gw1.human() + R"("})");
190  BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
191  BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
192  }
193  {
194  // Use a malformed peer.
195  auto const lines = env.rpc ("json", "account_lines",
196  R"({"account": ")" + alice.human() + R"(", )"
197  R"("peer": )"
198  R"("n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"})");
199  BEAST_EXPECT(lines[jss::result][jss::error_message] ==
200  RPC::make_error(rpcBAD_SEED)[jss::error_message]);
201  }
202  {
203  // A negative limit should fail.
204  auto const lines = env.rpc ("json", "account_lines",
205  R"({"account": ")" + alice.human() + R"(", )"
206  R"("limit": -1})");
207  BEAST_EXPECT(lines[jss::result][jss::error_message] ==
208  RPC::expected_field_message(jss::limit, "unsigned integer"));
209  }
210  {
211  // Limit the response to 1 trust line.
212  auto const linesA = env.rpc ("json", "account_lines",
213  R"({"account": ")" + alice.human() + R"(", )"
214  R"("limit": 1})");
215  BEAST_EXPECT(linesA[jss::result][jss::lines].isArray());
216  BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1);
217 
218  // Pick up from where the marker left off. We should get 51.
219  auto marker = linesA[jss::result][jss::marker].asString();
220  auto const linesB = env.rpc ("json", "account_lines",
221  R"({"account": ")" + alice.human() + R"(", )"
222  R"("marker": ")" + marker + R"("})");
223  BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
224  BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);
225 
226  // Go again from where the marker left off, but set a limit of 3.
227  auto const linesC = env.rpc ("json", "account_lines",
228  R"({"account": ")" + alice.human() + R"(", )"
229  R"("limit": 3, )"
230  R"("marker": ")" + marker + R"("})");
231  BEAST_EXPECT(linesC[jss::result][jss::lines].isArray());
232  BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3);
233 
234  // Mess with the marker so it becomes bad and check for the error.
235  marker[5] = marker[5] == '7' ? '8' : '7';
236  auto const linesD = env.rpc ("json", "account_lines",
237  R"({"account": ")" + alice.human() + R"(", )"
238  R"("marker": ")" + marker + R"("})");
239  BEAST_EXPECT(linesD[jss::result][jss::error_message] ==
240  RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
241  }
242  {
243  // A non-string marker should also fail.
244  auto const lines = env.rpc ("json", "account_lines",
245  R"({"account": ")" + alice.human() + R"(", )"
246  R"("marker": true})");
247  BEAST_EXPECT(lines[jss::result][jss::error_message] ==
248  RPC::expected_field_message(jss::marker, "string"));
249  }
250  {
251  // Check that the flags we expect from alice to gw2 are present.
252  auto const lines = env.rpc ("json", "account_lines",
253  R"({"account": ")" + alice.human() + R"(", )"
254  R"("limit": 1, )"
255  R"("peer": ")" + gw2.human() + R"("})");
256  auto const& line = lines[jss::result][jss::lines][0u];
257  BEAST_EXPECT(line[jss::freeze].asBool() == true);
258  BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
259  BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
260  }
261  {
262  // Check that the flags we expect from gw2 to alice are present.
263  auto const linesA = env.rpc ("json", "account_lines",
264  R"({"account": ")" + gw2.human() + R"(", )"
265  R"("limit": 1, )"
266  R"("peer": ")" + alice.human() + R"("})");
267  auto const& lineA = linesA[jss::result][jss::lines][0u];
268  BEAST_EXPECT(lineA[jss::freeze_peer].asBool() == true);
269  BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
270  BEAST_EXPECT(lineA[jss::authorized].asBool() == true);
271 
272  // Continue from the returned marker to make sure that works.
273  BEAST_EXPECT(linesA[jss::result].isMember(jss::marker));
274  auto const marker = linesA[jss::result][jss::marker].asString();
275  auto const linesB = env.rpc ("json", "account_lines",
276  R"({"account": ")" + gw2.human() + R"(", )"
277  R"("limit": 25, )"
278  R"("marker": ")" + marker + R"(", )"
279  R"("peer": ")" + alice.human() + R"("})");
280  BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
281  BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25);
282  BEAST_EXPECT(! linesB[jss::result].isMember(jss::marker));
283  }
284  }
285 
286  void testAccountLineDelete()
287  {
288  testcase ("Entry pointed to by marker is removed");
289  using namespace test::jtx;
290  Env env(*this);
291 
292  // The goal here is to observe account_lines marker behavior if the
293  // entry pointed at by a returned marker is removed from the ledger.
294  //
295  // It isn't easy to explicitly delete a trust line, so we do so in a
296  // round-about fashion. It takes 4 actors:
297  // o Gateway gw1 issues USD
298  // o alice offers to buy 100 USD for 100 XRP.
299  // o becky offers to sell 100 USD for 100 XRP.
300  // There will now be an inferred trustline between alice and gw1.
301  // o alice pays her 100 USD to cheri.
302  // alice should now have no USD and no trustline to gw1.
303  Account const alice {"alice"};
304  Account const becky {"becky"};
305  Account const cheri {"cheri"};
306  Account const gw1 {"gw1"};
307  Account const gw2 {"gw2"};
308  env.fund(XRP(10000), alice, becky, cheri, gw1, gw2);
309  env.close();
310 
311  auto const USD = gw1["USD"];
312  auto const EUR = gw2["EUR"];
313  env(trust(alice, USD(200)));
314  env(trust(becky, EUR(200)));
315  env(trust(cheri, EUR(200)));
316  env.close();
317 
318  // becky gets 100 USD from gw1.
319  env(pay(gw2, becky, EUR(100)));
320  env.close();
321 
322  // alice offers to buy 100 EUR for 100 XRP.
323  env(offer(alice, EUR(100), XRP(100)));
324  env.close();
325 
326  // becky offers to buy 100 XRP for 100 EUR.
327  env(offer(becky, XRP(100), EUR(100)));
328  env.close();
329 
330  // Get account_lines for alice. Limit at 1, so we get a marker.
331  auto const linesBeg = env.rpc ("json", "account_lines",
332  R"({"account": ")" + alice.human() + R"(", )"
333  R"("limit": 1})");
334  BEAST_EXPECT(linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD");
335  BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker));
336 
337  // alice pays 100 EUR to cheri.
338  env(pay(alice, cheri, EUR(100)));
339  env.close();
340 
341  // Since alice paid all her EUR to cheri, alice should no longer
342  // have a trust line to gw1. So the old marker should now be invalid.
343  auto const linesEnd = env.rpc ("json", "account_lines",
344  R"({"account": ")" + alice.human() + R"(", )"
345  R"("marker": ")" +
346  linesBeg[jss::result][jss::marker].asString() + R"("})");
347  BEAST_EXPECT(linesEnd[jss::result][jss::error_message] ==
348  RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
349  }
350 
351  // test API V2
352  void testAccountLines2()
353  {
354  testcase ("V2: acccount_lines");
355 
356  using namespace test::jtx;
357  Env env(*this);
358  {
359  // account_lines with mal-formed json2 (missing id field).
360  auto const lines = env.rpc ("json2", "{ "
361  R"("method" : "account_lines",)"
362  R"("jsonrpc" : "2.0",)"
363  R"("ripplerpc" : "2.0")"
364  " }");
365  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
366  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
367  }
368  {
369  // account_lines with no account.
370  auto const lines = env.rpc ("json2", "{ "
371  R"("method" : "account_lines",)"
372  R"("jsonrpc" : "2.0",)"
373  R"("ripplerpc" : "2.0",)"
374  R"("id" : 5)"
375  " }");
376  BEAST_EXPECT(lines[jss::error][jss::message] ==
377  RPC::missing_field_error(jss::account)[jss::error_message]);
378  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
379  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
380  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
381  }
382  {
383  // account_lines with a malformed account.
384  auto const lines = env.rpc ("json2", "{ "
385  R"("method" : "account_lines",)"
386  R"("jsonrpc" : "2.0",)"
387  R"("ripplerpc" : "2.0",)"
388  R"("id" : 5,)"
389  R"("params": )"
390  R"({"account": )"
391  R"("n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"}})");
392  BEAST_EXPECT(lines[jss::error][jss::message] ==
393  RPC::make_error(rpcBAD_SEED)[jss::error_message]);
394  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
395  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
396  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
397  }
398  Account const alice {"alice"};
399  {
400  // account_lines on an unfunded account.
401  auto const lines = env.rpc ("json2", "{ "
402  R"("method" : "account_lines",)"
403  R"("jsonrpc" : "2.0",)"
404  R"("ripplerpc" : "2.0",)"
405  R"("id" : 5,)"
406  R"("params": )"
407  R"({"account": ")" + alice.human() + R"("}})");
408  BEAST_EXPECT(lines[jss::error][jss::message] ==
409  RPC::make_error(rpcACT_NOT_FOUND)[jss::error_message]);
410  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
411  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
412  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
413  }
414  env.fund(XRP(10000), alice);
415  env.close();
416  LedgerInfo const ledger3Info = env.closed()->info();
417  BEAST_EXPECT(ledger3Info.seq == 3);
418 
419  {
420  // alice is funded but has no lines. An empty array is returned.
421  auto const lines = env.rpc ("json2", "{ "
422  R"("method" : "account_lines",)"
423  R"("jsonrpc" : "2.0",)"
424  R"("ripplerpc" : "2.0",)"
425  R"("id" : 5,)"
426  R"("params": )"
427  R"({"account": ")" + alice.human() + R"("}})");
428  BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
429  BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0);
430  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
431  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
432  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
433  }
434  {
435  // Specify a ledger that doesn't exist.
436  auto const lines = env.rpc ("json2", "{ "
437  R"("method" : "account_lines",)"
438  R"("jsonrpc" : "2.0",)"
439  R"("ripplerpc" : "2.0",)"
440  R"("id" : 5,)"
441  R"("params": )"
442  R"({"account": ")" + alice.human() + R"(", )"
443  R"("ledger_index": "nonsense"}})");
444  BEAST_EXPECT(lines[jss::error][jss::message] ==
445  "ledgerIndexMalformed");
446  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
447  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
448  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
449  }
450  {
451  // Specify a different ledger that doesn't exist.
452  auto const lines = env.rpc ("json2", "{ "
453  R"("method" : "account_lines",)"
454  R"("jsonrpc" : "2.0",)"
455  R"("ripplerpc" : "2.0",)"
456  R"("id" : 5,)"
457  R"("params": )"
458  R"({"account": ")" + alice.human() + R"(", )"
459  R"("ledger_index": 50000}})");
460  BEAST_EXPECT(lines[jss::error][jss::message] ==
461  "ledgerNotFound");
462  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
463  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
464  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
465  }
466  // Create trust lines to share with alice.
467  Account const gw1 {"gw1"};
468  env.fund(XRP(10000), gw1);
469  std::vector<IOU> gw1Currencies;
470 
471  for (char c = 0; c <= ('Z' - 'A'); ++c)
472  {
473  // gw1 currencies have names "YAA" -> "YAZ".
474  gw1Currencies.push_back(
475  gw1[std::string("YA") + static_cast<char>('A' + c)]);
476  IOU const& gw1Currency = gw1Currencies.back();
477 
478  // Establish trust lines.
479  env(trust(alice, gw1Currency(100 + c)));
480  env(pay(gw1, alice, gw1Currency(50 + c)));
481  }
482  env.close();
483  LedgerInfo const ledger4Info = env.closed()->info();
484  BEAST_EXPECT(ledger4Info.seq == 4);
485 
486  // Add another set of trust lines in another ledger so we can see
487  // differences in historic ledgers.
488  Account const gw2 {"gw2"};
489  env.fund(XRP(10000), gw2);
490 
491  // gw2 requires authorization.
492  env(fset(gw2, asfRequireAuth));
493  env.close();
494  std::vector<IOU> gw2Currencies;
495 
496  for (char c = 0; c <= ('Z' - 'A'); ++c)
497  {
498  // gw2 currencies have names "ZAA" -> "ZAZ".
499  gw2Currencies.push_back(
500  gw2[std::string("ZA") + static_cast<char>('A' + c)]);
501  IOU const& gw2Currency = gw2Currencies.back();
502 
503  // Establish trust lines.
504  env(trust(alice, gw2Currency(200 + c)));
505  env(trust(gw2, gw2Currency(0), alice, tfSetfAuth));
506  env.close();
507  env(pay(gw2, alice, gw2Currency(100 + c)));
508  env.close();
509 
510  // Set flags on gw2 trust lines so we can look for them.
511  env(trust(alice, gw2Currency(0), gw2, tfSetNoRipple | tfSetFreeze));
512  }
513  env.close();
514  LedgerInfo const ledger58Info = env.closed()->info();
515  BEAST_EXPECT(ledger58Info.seq == 58);
516 
517  // A re-usable test for historic ledgers.
518  auto testAccountLinesHistory =
519  [this, &env](Account const& account, LedgerInfo const& info, int count)
520  {
521  // Get account_lines by ledger index.
522  auto const linesSeq = env.rpc ("json2", "{ "
523  R"("method" : "account_lines",)"
524  R"("jsonrpc" : "2.0",)"
525  R"("ripplerpc" : "2.0",)"
526  R"("id" : 5,)"
527  R"("params": )"
528  R"({"account": ")" + account.human() + R"(", )"
529  R"("ledger_index": )" + std::to_string(info.seq) + "}}");
530  BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
531  BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
532  BEAST_EXPECT(linesSeq.isMember(jss::jsonrpc) && linesSeq[jss::jsonrpc] == "2.0");
533  BEAST_EXPECT(linesSeq.isMember(jss::ripplerpc) && linesSeq[jss::ripplerpc] == "2.0");
534  BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5);
535 
536  // Get account_lines by ledger hash.
537  auto const linesHash = env.rpc ("json2", "{ "
538  R"("method" : "account_lines",)"
539  R"("jsonrpc" : "2.0",)"
540  R"("ripplerpc" : "2.0",)"
541  R"("id" : 5,)"
542  R"("params": )"
543  R"({"account": ")" + account.human() + R"(", )"
544  R"("ledger_hash": ")" + to_string(info.hash) + R"("}})");
545  BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
546  BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
547  BEAST_EXPECT(linesHash.isMember(jss::jsonrpc) && linesHash[jss::jsonrpc] == "2.0");
548  BEAST_EXPECT(linesHash.isMember(jss::ripplerpc) && linesHash[jss::ripplerpc] == "2.0");
549  BEAST_EXPECT(linesHash.isMember(jss::id) && linesHash[jss::id] == 5);
550  };
551 
552  // Alice should have no trust lines in ledger 3.
553  testAccountLinesHistory (alice, ledger3Info, 0);
554 
555  // Alice should have 26 trust lines in ledger 4.
556  testAccountLinesHistory (alice, ledger4Info, 26);
557 
558  // Alice should have 52 trust lines in ledger 58.
559  testAccountLinesHistory (alice, ledger58Info, 52);
560 
561  {
562  // Surprisingly, it's valid to specify both index and hash, in
563  // which case the hash wins.
564  auto const lines = env.rpc ("json2", "{ "
565  R"("method" : "account_lines",)"
566  R"("jsonrpc" : "2.0",)"
567  R"("ripplerpc" : "2.0",)"
568  R"("id" : 5,)"
569  R"("params": )"
570  R"({"account": ")" + alice.human() + R"(", )"
571  R"("ledger_hash": ")" + to_string(ledger4Info.hash) + R"(", )"
572  R"("ledger_index": )" + std::to_string(ledger58Info.seq) + "}}");
573  BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
574  BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
575  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
576  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
577  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
578  }
579  {
580  // alice should have 52 trust lines in the current ledger.
581  auto const lines = env.rpc ("json2", "{ "
582  R"("method" : "account_lines",)"
583  R"("jsonrpc" : "2.0",)"
584  R"("ripplerpc" : "2.0",)"
585  R"("id" : 5,)"
586  R"("params": )"
587  R"({"account": ")" + alice.human() + R"("}})");
588  BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
589  BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
590  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
591  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
592  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
593  }
594  {
595  // alice should have 26 trust lines with gw1.
596  auto const lines = env.rpc ("json2", "{ "
597  R"("method" : "account_lines",)"
598  R"("jsonrpc" : "2.0",)"
599  R"("ripplerpc" : "2.0",)"
600  R"("id" : 5,)"
601  R"("params": )"
602  R"({"account": ")" + alice.human() + R"(", )"
603  R"("peer": ")" + gw1.human() + R"("}})");
604  BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
605  BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
606  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
607  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
608  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
609  }
610  {
611  // Use a malformed peer.
612  auto const lines = env.rpc ("json2", "{ "
613  R"("method" : "account_lines",)"
614  R"("jsonrpc" : "2.0",)"
615  R"("ripplerpc" : "2.0",)"
616  R"("id" : 5,)"
617  R"("params": )"
618  R"({"account": ")" + alice.human() + R"(", )"
619  R"("peer": )"
620  R"("n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"}})");
621  BEAST_EXPECT(lines[jss::error][jss::message] ==
622  RPC::make_error(rpcBAD_SEED)[jss::error_message]);
623  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
624  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
625  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
626  }
627  {
628  // A negative limit should fail.
629  auto const lines = env.rpc ("json2", "{ "
630  R"("method" : "account_lines",)"
631  R"("jsonrpc" : "2.0",)"
632  R"("ripplerpc" : "2.0",)"
633  R"("id" : 5,)"
634  R"("params": )"
635  R"({"account": ")" + alice.human() + R"(", )"
636  R"("limit": -1}})");
637  BEAST_EXPECT(lines[jss::error][jss::message] ==
638  RPC::expected_field_message(jss::limit, "unsigned integer"));
639  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
640  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
641  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
642  }
643  {
644  // Limit the response to 1 trust line.
645  auto const linesA = env.rpc ("json2", "{ "
646  R"("method" : "account_lines",)"
647  R"("jsonrpc" : "2.0",)"
648  R"("ripplerpc" : "2.0",)"
649  R"("id" : 5,)"
650  R"("params": )"
651  R"({"account": ")" + alice.human() + R"(", )"
652  R"("limit": 1}})");
653  BEAST_EXPECT(linesA[jss::result][jss::lines].isArray());
654  BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1);
655  BEAST_EXPECT(linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
656  BEAST_EXPECT(linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0");
657  BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
658 
659  // Pick up from where the marker left off. We should get 51.
660  auto marker = linesA[jss::result][jss::marker].asString();
661  auto const linesB = env.rpc ("json2", "{ "
662  R"("method" : "account_lines",)"
663  R"("jsonrpc" : "2.0",)"
664  R"("ripplerpc" : "2.0",)"
665  R"("id" : 5,)"
666  R"("params": )"
667  R"({"account": ")" + alice.human() + R"(", )"
668  R"("marker": ")" + marker + R"("}})");
669  BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
670  BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);
671  BEAST_EXPECT(linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
672  BEAST_EXPECT(linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0");
673  BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
674 
675  // Go again from where the marker left off, but set a limit of 3.
676  auto const linesC = env.rpc ("json2", "{ "
677  R"("method" : "account_lines",)"
678  R"("jsonrpc" : "2.0",)"
679  R"("ripplerpc" : "2.0",)"
680  R"("id" : 5,)"
681  R"("params": )"
682  R"({"account": ")" + alice.human() + R"(", )"
683  R"("limit": 3, )"
684  R"("marker": ")" + marker + R"("}})");
685  BEAST_EXPECT(linesC[jss::result][jss::lines].isArray());
686  BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3);
687  BEAST_EXPECT(linesC.isMember(jss::jsonrpc) && linesC[jss::jsonrpc] == "2.0");
688  BEAST_EXPECT(linesC.isMember(jss::ripplerpc) && linesC[jss::ripplerpc] == "2.0");
689  BEAST_EXPECT(linesC.isMember(jss::id) && linesC[jss::id] == 5);
690 
691  // Mess with the marker so it becomes bad and check for the error.
692  marker[5] = marker[5] == '7' ? '8' : '7';
693  auto const linesD = env.rpc ("json2", "{ "
694  R"("method" : "account_lines",)"
695  R"("jsonrpc" : "2.0",)"
696  R"("ripplerpc" : "2.0",)"
697  R"("id" : 5,)"
698  R"("params": )"
699  R"({"account": ")" + alice.human() + R"(", )"
700  R"("marker": ")" + marker + R"("}})");
701  BEAST_EXPECT(linesD[jss::error][jss::message] ==
702  RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
703  BEAST_EXPECT(linesD.isMember(jss::jsonrpc) && linesD[jss::jsonrpc] == "2.0");
704  BEAST_EXPECT(linesD.isMember(jss::ripplerpc) && linesD[jss::ripplerpc] == "2.0");
705  BEAST_EXPECT(linesD.isMember(jss::id) && linesD[jss::id] == 5);
706  }
707  {
708  // A non-string marker should also fail.
709  auto const lines = env.rpc ("json2", "{ "
710  R"("method" : "account_lines",)"
711  R"("jsonrpc" : "2.0",)"
712  R"("ripplerpc" : "2.0",)"
713  R"("id" : 5,)"
714  R"("params": )"
715  R"({"account": ")" + alice.human() + R"(", )"
716  R"("marker": true}})");
717  BEAST_EXPECT(lines[jss::error][jss::message] ==
718  RPC::expected_field_message(jss::marker, "string"));
719  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
720  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
721  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
722  }
723  {
724  // Check that the flags we expect from alice to gw2 are present.
725  auto const lines = env.rpc ("json2", "{ "
726  R"("method" : "account_lines",)"
727  R"("jsonrpc" : "2.0",)"
728  R"("ripplerpc" : "2.0",)"
729  R"("id" : 5,)"
730  R"("params": )"
731  R"({"account": ")" + alice.human() + R"(", )"
732  R"("limit": 1, )"
733  R"("peer": ")" + gw2.human() + R"("}})");
734  auto const& line = lines[jss::result][jss::lines][0u];
735  BEAST_EXPECT(line[jss::freeze].asBool() == true);
736  BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
737  BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
738  BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
739  BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
740  BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
741  }
742  {
743  // Check that the flags we expect from gw2 to alice are present.
744  auto const linesA = env.rpc ("json2", "{ "
745  R"("method" : "account_lines",)"
746  R"("jsonrpc" : "2.0",)"
747  R"("ripplerpc" : "2.0",)"
748  R"("id" : 5,)"
749  R"("params": )"
750  R"({"account": ")" + gw2.human() + R"(", )"
751  R"("limit": 1, )"
752  R"("peer": ")" + alice.human() + R"("}})");
753  auto const& lineA = linesA[jss::result][jss::lines][0u];
754  BEAST_EXPECT(lineA[jss::freeze_peer].asBool() == true);
755  BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
756  BEAST_EXPECT(lineA[jss::authorized].asBool() == true);
757  BEAST_EXPECT(linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
758  BEAST_EXPECT(linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0");
759  BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
760 
761  // Continue from the returned marker to make sure that works.
762  BEAST_EXPECT(linesA[jss::result].isMember(jss::marker));
763  auto const marker = linesA[jss::result][jss::marker].asString();
764  auto const linesB = env.rpc ("json2", "{ "
765  R"("method" : "account_lines",)"
766  R"("jsonrpc" : "2.0",)"
767  R"("ripplerpc" : "2.0",)"
768  R"("id" : 5,)"
769  R"("params": )"
770  R"({"account": ")" + gw2.human() + R"(", )"
771  R"("limit": 25, )"
772  R"("marker": ")" + marker + R"(", )"
773  R"("peer": ")" + alice.human() + R"("}})");
774  BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
775  BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25);
776  BEAST_EXPECT(! linesB[jss::result].isMember(jss::marker));
777  BEAST_EXPECT(linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
778  BEAST_EXPECT(linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0");
779  BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
780  }
781  }
782 
783  // test API V2
784  void testAccountLineDelete2()
785  {
786  testcase ("V2: account_lines with removed marker");
787 
788  using namespace test::jtx;
789  Env env(*this);
790 
791  // The goal here is to observe account_lines marker behavior if the
792  // entry pointed at by a returned marker is removed from the ledger.
793  //
794  // It isn't easy to explicitly delete a trust line, so we do so in a
795  // round-about fashion. It takes 4 actors:
796  // o Gateway gw1 issues EUR
797  // o alice offers to buy 100 EUR for 100 XRP.
798  // o becky offers to sell 100 EUR for 100 XRP.
799  // There will now be an inferred trustline between alice and gw2.
800  // o alice pays her 100 EUR to cheri.
801  // alice should now have no EUR and no trustline to gw2.
802  Account const alice {"alice"};
803  Account const becky {"becky"};
804  Account const cheri {"cheri"};
805  Account const gw1 {"gw1"};
806  Account const gw2 {"gw2"};
807  env.fund(XRP(10000), alice, becky, cheri, gw1, gw2);
808  env.close();
809 
810  auto const USD = gw1["USD"];
811  auto const EUR = gw2["EUR"];
812  env(trust(alice, USD(200)));
813  env(trust(becky, EUR(200)));
814  env(trust(cheri, EUR(200)));
815  env.close();
816 
817  // becky gets 100 EUR from gw1.
818  env(pay(gw2, becky, EUR(100)));
819  env.close();
820 
821  // alice offers to buy 100 EUR for 100 XRP.
822  env(offer(alice, EUR(100), XRP(100)));
823  env.close();
824 
825  // becky offers to buy 100 XRP for 100 EUR.
826  env(offer(becky, XRP(100), EUR(100)));
827  env.close();
828 
829  // Get account_lines for alice. Limit at 1, so we get a marker.
830  auto const linesBeg = env.rpc ("json2", "{ "
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": ")" + alice.human() + R"(", )"
837  R"("limit": 1}})");
838  BEAST_EXPECT(linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD");
839  BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker));
840  BEAST_EXPECT(linesBeg.isMember(jss::jsonrpc) && linesBeg[jss::jsonrpc] == "2.0");
841  BEAST_EXPECT(linesBeg.isMember(jss::ripplerpc) && linesBeg[jss::ripplerpc] == "2.0");
842  BEAST_EXPECT(linesBeg.isMember(jss::id) && linesBeg[jss::id] == 5);
843 
844  // alice pays 100 USD to cheri.
845  env(pay(alice, cheri, EUR(100)));
846  env.close();
847 
848  // Since alice paid all her EUR to cheri, alice should no longer
849  // have a trust line to gw1. So the old marker should now be invalid.
850  auto const linesEnd = env.rpc ("json2", "{ "
851  R"("method" : "account_lines",)"
852  R"("jsonrpc" : "2.0",)"
853  R"("ripplerpc" : "2.0",)"
854  R"("id" : 5,)"
855  R"("params": )"
856  R"({"account": ")" + alice.human() + R"(", )"
857  R"("marker": ")" +
858  linesBeg[jss::result][jss::marker].asString() + R"("}})");
859  BEAST_EXPECT(linesEnd[jss::error][jss::message] ==
860  RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]);
861  BEAST_EXPECT(linesEnd.isMember(jss::jsonrpc) && linesEnd[jss::jsonrpc] == "2.0");
862  BEAST_EXPECT(linesEnd.isMember(jss::ripplerpc) && linesEnd[jss::ripplerpc] == "2.0");
863  BEAST_EXPECT(linesEnd.isMember(jss::id) && linesEnd[jss::id] == 5);
864  }
865 
866  void run () override
867  {
869  testAccountLineDelete();
870  testAccountLines2();
871  testAccountLineDelete2();
872  }
873 };
874 
875 BEAST_DEFINE_TESTSUITE(AccountLinesRPC,app,ripple);
876 
877 } // RPC
878 } // ripple
879 
ripple::RPC::BEAST_DEFINE_TESTSUITE
BEAST_DEFINE_TESTSUITE(AccountLinesRPC, app, ripple)
ripple::test::jtx::XRP
const XRP_t XRP
Converts to XRP Issue or STAmount.
Definition: amount.cpp:109
std::string
STL class.
ripple::rpcINVALID_PARAMS
@ rpcINVALID_PARAMS
Definition: ErrorCodes.h:85
ripple::LedgerInfo::hash
uint256 hash
Definition: ReadView.h:95
std::vector
STL class.
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
std::vector::back
T back(T... args)
ripple::LedgerInfo::seq
LedgerIndex seq
Definition: ReadView.h:87
ripple::to_string
std::string to_string(ListDisposition disposition)
Definition: ValidatorList.cpp:41
ripple::test::jtx::offer
Json::Value offer(Account const &account, STAmount const &in, STAmount const &out, std::uint32_t flags)
Create an offer.
Definition: offer.cpp:28
ripple::RPC::missing_field_error
Json::Value missing_field_error(std::string const &name)
Definition: ErrorCodes.h:229
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:274
ripple::asfRequireAuth
const std::uint32_t asfRequireAuth
Definition: TxFlags.h:66
ripple::rpcBAD_SEED
@ rpcBAD_SEED
Definition: ErrorCodes.h:100
ripple::RPC::AccountLinesRPC_test::testAccountLines
void testAccountLines()
Definition: AccountLinesRPC_test.cpp:33
ripple::rpcACT_NOT_FOUND
@ rpcACT_NOT_FOUND
Definition: ErrorCodes.h:71
ripple::test::jtx::fset
Json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition: flags.cpp:28
std::to_string
T to_string(T... args)
ripple::tfSetFreeze
const std::uint32_t tfSetFreeze
Definition: TxFlags.h:92
ripple
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: RCLCensorshipDetector.h:29
ripple::tfSetNoRipple
const std::uint32_t tfSetNoRipple
Definition: TxFlags.h:90
ripple::test::jtx::pay
Json::Value pay(Account const &account, Account const &to, AnyAmount amount)
Create a payment.
Definition: pay.cpp:29
std::count
T count(T... args)
ripple::LedgerInfo
Information about the notional ledger backing the view.
Definition: ReadView.h:79
ripple::tfSetfAuth
const std::uint32_t tfSetfAuth
Definition: TxFlags.h:89
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:91
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:182