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