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