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