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