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