#include #include #include #include #include namespace ripple { namespace RPC { class AccountLines_test : public beast::unit_test::suite { public: void testAccountLines() { testcase("account_lines"); using namespace test::jtx; Env env(*this); { // account_lines with no account. auto const lines = env.rpc("json", "account_lines", "{ }"); BEAST_EXPECT( lines[jss::result][jss::error_message] == RPC::missing_field_error(jss::account)[jss::error_message]); } { // account_lines with a malformed account. Json::Value params; params[jss::account] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"; auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == RPC::make_error(rpcACT_MALFORMED)[jss::error_message]); } { // test account non-string auto testInvalidAccountParam = [&](auto const& param) { Json::Value params; params[jss::account] = param; auto jrr = env.rpc( "json", "account_lines", to_string(params))[jss::result]; BEAST_EXPECT(jrr[jss::error] == "invalidParams"); BEAST_EXPECT( jrr[jss::error_message] == "Invalid field 'account'."); }; testInvalidAccountParam(1); testInvalidAccountParam(1.1); testInvalidAccountParam(true); testInvalidAccountParam(Json::Value(Json::nullValue)); testInvalidAccountParam(Json::Value(Json::objectValue)); testInvalidAccountParam(Json::Value(Json::arrayValue)); } Account const alice{"alice"}; { // account_lines on an unfunded account. Json::Value params; params[jss::account] = alice.human(); auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == RPC::make_error(rpcACT_NOT_FOUND)[jss::error_message]); } env.fund(XRP(10000), alice); env.close(); LedgerHeader const ledger3Info = env.closed()->header(); BEAST_EXPECT(ledger3Info.seq == 3); { // alice is funded but has no lines. An empty array is returned. Json::Value params; params[jss::account] = alice.human(); auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT(lines[jss::result][jss::lines].isArray()); BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0); } { // Specify a ledger that doesn't exist. Json::Value params; params[jss::account] = alice.human(); params[jss::ledger_index] = "nonsense"; auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == "Invalid field 'ledger_index', not string or number."); } { // Specify a different ledger that doesn't exist. Json::Value params; params[jss::account] = alice.human(); params[jss::ledger_index] = 50000; auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == "ledgerNotFound"); } // Create trust lines to share with alice. Account const gw1{"gw1"}; env.fund(XRP(10000), gw1); std::vector gw1Currencies; for (char c = 0; c <= ('Z' - 'A'); ++c) { // gw1 currencies have names "YAA" -> "YAZ". gw1Currencies.push_back( gw1[std::string("YA") + static_cast('A' + c)]); IOU const& gw1Currency = gw1Currencies.back(); // Establish trust lines. env(trust(alice, gw1Currency(100 + c))); env(pay(gw1, alice, gw1Currency(50 + c))); } env.close(); LedgerHeader const ledger4Info = env.closed()->header(); BEAST_EXPECT(ledger4Info.seq == 4); // Add another set of trust lines in another ledger so we can see // differences in historic ledgers. Account const gw2{"gw2"}; env.fund(XRP(10000), gw2); // gw2 requires authorization. env(fset(gw2, asfRequireAuth)); env.close(); std::vector gw2Currencies; for (char c = 0; c <= ('Z' - 'A'); ++c) { // gw2 currencies have names "ZAA" -> "ZAZ". gw2Currencies.push_back( gw2[std::string("ZA") + static_cast('A' + c)]); IOU const& gw2Currency = gw2Currencies.back(); // Establish trust lines. env(trust(alice, gw2Currency(200 + c))); env(trust(gw2, gw2Currency(0), alice, tfSetfAuth)); env.close(); env(pay(gw2, alice, gw2Currency(100 + c))); env.close(); // Set flags on gw2 trust lines so we can look for them. env(trust( alice, gw2Currency(0), gw2, tfSetNoRipple | tfSetFreeze | tfSetDeepFreeze)); } env.close(); LedgerHeader const ledger58Info = env.closed()->header(); BEAST_EXPECT(ledger58Info.seq == 58); // A re-usable test for historic ledgers. auto testAccountLinesHistory = [this, &env]( Account const& account, LedgerHeader const& info, int count) { // Get account_lines by ledger index. Json::Value paramsSeq; paramsSeq[jss::account] = account.human(); paramsSeq[jss::ledger_index] = info.seq; auto const linesSeq = env.rpc("json", "account_lines", to_string(paramsSeq)); BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray()); BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count); // Get account_lines by ledger hash. Json::Value paramsHash; paramsHash[jss::account] = account.human(); paramsHash[jss::ledger_hash] = to_string(info.hash); auto const linesHash = env.rpc("json", "account_lines", to_string(paramsHash)); BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray()); BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count); }; // Alice should have no trust lines in ledger 3. testAccountLinesHistory(alice, ledger3Info, 0); // Alice should have 26 trust lines in ledger 4. testAccountLinesHistory(alice, ledger4Info, 26); // Alice should have 52 trust lines in ledger 58. testAccountLinesHistory(alice, ledger58Info, 52); { // Invalid to specify both index and hash Json::Value params; params[jss::account] = alice.human(); params[jss::ledger_hash] = to_string(ledger4Info.hash); params[jss::ledger_index] = ledger58Info.seq; auto const lines = env.rpc( "json", "account_lines", to_string(params))[jss::result]; BEAST_EXPECT(lines[jss::error] == "invalidParams"); BEAST_EXPECT( lines[jss::error_message] == "Exactly one of 'ledger_hash' or 'ledger_index' can be " "specified."); } { // Invalid index Json::Value params; params[jss::account] = alice.human(); params[jss::ledger_index] = Json::objectValue; auto const lines = env.rpc( "json", "account_lines", to_string(params))[jss::result]; BEAST_EXPECT(lines[jss::error] == "invalidParams"); BEAST_EXPECT( lines[jss::error_message] == "Invalid field 'ledger_index', not string or number."); } { // alice should have 52 trust lines in the current ledger. Json::Value params; params[jss::account] = alice.human(); auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT(lines[jss::result][jss::lines].isArray()); BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52); } { // alice should have 26 trust lines with gw1. Json::Value params; params[jss::account] = alice.human(); params[jss::peer] = gw1.human(); auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT(lines[jss::result][jss::lines].isArray()); BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26); // Check no ripple is not set for trustlines between alice and gw1 auto const& line = lines[jss::result][jss::lines][0u]; BEAST_EXPECT(!line[jss::no_ripple].isMember(jss::no_ripple)); } { // Use a malformed peer. Json::Value params; params[jss::account] = alice.human(); params[jss::peer] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"; auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == RPC::make_error(rpcACT_MALFORMED)[jss::error_message]); } { // A negative limit should fail. Json::Value params; params[jss::account] = alice.human(); params[jss::limit] = -1; auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == RPC::expected_field_message(jss::limit, "unsigned integer")); } { // Limit the response to 1 trust line. Json::Value paramsA; paramsA[jss::account] = alice.human(); paramsA[jss::limit] = 1; auto const linesA = env.rpc("json", "account_lines", to_string(paramsA)); BEAST_EXPECT(linesA[jss::result][jss::lines].isArray()); BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1); // Pick up from where the marker left off. We should get 51. auto marker = linesA[jss::result][jss::marker].asString(); Json::Value paramsB; paramsB[jss::account] = alice.human(); paramsB[jss::marker] = marker; auto const linesB = env.rpc("json", "account_lines", to_string(paramsB)); BEAST_EXPECT(linesB[jss::result][jss::lines].isArray()); BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51); // Go again from where the marker left off, but set a limit of 3. Json::Value paramsC; paramsC[jss::account] = alice.human(); paramsC[jss::limit] = 3; paramsC[jss::marker] = marker; auto const linesC = env.rpc("json", "account_lines", to_string(paramsC)); BEAST_EXPECT(linesC[jss::result][jss::lines].isArray()); BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3); // Mess with the marker so it becomes bad and check for the error. marker[5] = marker[5] == '7' ? '8' : '7'; Json::Value paramsD; paramsD[jss::account] = alice.human(); paramsD[jss::marker] = marker; auto const linesD = env.rpc("json", "account_lines", to_string(paramsD)); BEAST_EXPECT( linesD[jss::result][jss::error_message] == RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]); } { // A non-string marker should also fail. Json::Value params; params[jss::account] = alice.human(); params[jss::marker] = true; auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == RPC::expected_field_message(jss::marker, "string")); } { // Check that the flags we expect from alice to gw2 are present. Json::Value params; params[jss::account] = alice.human(); params[jss::limit] = 10; params[jss::peer] = gw2.human(); auto const lines = env.rpc("json", "account_lines", to_string(params)); auto const& line = lines[jss::result][jss::lines][0u]; BEAST_EXPECT(line[jss::freeze].asBool() == true); BEAST_EXPECT(line[jss::deep_freeze].asBool() == true); BEAST_EXPECT(line[jss::no_ripple].asBool() == true); BEAST_EXPECT(line[jss::peer_authorized].asBool() == true); } { // Check that the flags we expect from gw2 to alice are present. Json::Value paramsA; paramsA[jss::account] = gw2.human(); paramsA[jss::limit] = 1; paramsA[jss::peer] = alice.human(); auto const linesA = env.rpc("json", "account_lines", to_string(paramsA)); auto const& lineA = linesA[jss::result][jss::lines][0u]; BEAST_EXPECT(lineA[jss::freeze_peer].asBool() == true); BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true); BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true); BEAST_EXPECT(lineA[jss::authorized].asBool() == true); // Continue from the returned marker to make sure that works. BEAST_EXPECT(linesA[jss::result].isMember(jss::marker)); auto const marker = linesA[jss::result][jss::marker].asString(); Json::Value paramsB; paramsB[jss::account] = gw2.human(); paramsB[jss::limit] = 25; paramsB[jss::marker] = marker; paramsB[jss::peer] = alice.human(); auto const linesB = env.rpc("json", "account_lines", to_string(paramsB)); BEAST_EXPECT(linesB[jss::result][jss::lines].isArray()); BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25); BEAST_EXPECT(!linesB[jss::result].isMember(jss::marker)); } } void testAccountLinesMarker() { testcase("Entry pointed to by marker is not owned by account"); using namespace test::jtx; Env env(*this); // The goal of this test is observe account_lines RPC calls return an // error message when the SLE pointed to by the marker is not owned by // the Account being traversed. // // To start, we'll create an environment with some trust lines, offers // and a signers list. Account const alice{"alice"}; Account const becky{"becky"}; Account const gw1{"gw1"}; env.fund(XRP(10000), alice, becky, gw1); env.close(); // Give alice a SignerList. Account const bogie{"bogie"}; env(signers(alice, 2, {{bogie, 3}})); env.close(); auto const EUR = gw1["EUR"]; env(trust(alice, EUR(200))); env(trust(becky, EUR(200))); env.close(); // Get all account objects for alice and verify that her // signerlist is first. This is only a (reliable) coincidence of // object naming. So if any of alice's objects are renamed this // may fail. Json::Value aliceObjectsParams; aliceObjectsParams[jss::account] = alice.human(); aliceObjectsParams[jss::limit] = 10; Json::Value const aliceObjects = env.rpc("json", "account_objects", to_string(aliceObjectsParams)); Json::Value const& aliceSignerList = aliceObjects[jss::result][jss::account_objects][0u]; if (!(aliceSignerList[sfLedgerEntryType.jsonName] == jss::SignerList)) { fail( "alice's account objects are misordered. " "Please reorder the objects so the SignerList is first.", __FILE__, __LINE__); return; } // Get account_lines for alice. Limit at 1, so we get a marker // pointing to her SignerList. Json::Value aliceLines1Params; aliceLines1Params[jss::account] = alice.human(); aliceLines1Params[jss::limit] = 1; auto const aliceLines1 = env.rpc("json", "account_lines", to_string(aliceLines1Params)); BEAST_EXPECT(aliceLines1[jss::result].isMember(jss::marker)); // Verify that the marker points at the signer list. std::string const aliceMarker = aliceLines1[jss::result][jss::marker].asString(); std::string const markerIndex = aliceMarker.substr(0, aliceMarker.find(',')); BEAST_EXPECT(markerIndex == aliceSignerList[jss::index].asString()); // When we fetch Alice's remaining lines we should find one and no more. Json::Value aliceLines2Params; aliceLines2Params[jss::account] = alice.human(); aliceLines2Params[jss::marker] = aliceMarker; auto const aliceLines2 = env.rpc("json", "account_lines", to_string(aliceLines2Params)); BEAST_EXPECT(aliceLines2[jss::result][jss::lines].size() == 1); BEAST_EXPECT(!aliceLines2[jss::result].isMember(jss::marker)); // Get account lines for beckys account, using alices SignerList as a // marker. This should cause an error. Json::Value beckyLinesParams; beckyLinesParams[jss::account] = becky.human(); beckyLinesParams[jss::marker] = aliceMarker; auto const beckyLines = env.rpc("json", "account_lines", to_string(beckyLinesParams)); BEAST_EXPECT(beckyLines[jss::result].isMember(jss::error_message)); } void testAccountLineDelete() { testcase("Entry pointed to by marker is removed"); using namespace test::jtx; Env env(*this); // The goal here is to observe account_lines marker behavior if the // entry pointed at by a returned marker is removed from the ledger. // // It isn't easy to explicitly delete a trust line, so we do so in a // round-about fashion. It takes 4 actors: // o Gateway gw1 issues USD // o alice offers to buy 100 USD for 100 XRP. // o becky offers to sell 100 USD for 100 XRP. // There will now be an inferred trustline between alice and gw1. // o alice pays her 100 USD to cheri. // alice should now have no USD and no trustline to gw1. Account const alice{"alice"}; Account const becky{"becky"}; Account const cheri{"cheri"}; Account const gw1{"gw1"}; Account const gw2{"gw2"}; env.fund(XRP(10000), alice, becky, cheri, gw1, gw2); env.close(); auto const USD = gw1["USD"]; auto const AUD = gw1["AUD"]; auto const EUR = gw2["EUR"]; env(trust(alice, USD(200))); env(trust(alice, AUD(200))); env(trust(becky, EUR(200))); env(trust(cheri, EUR(200))); env.close(); // becky gets 100 USD from gw1. env(pay(gw2, becky, EUR(100))); env.close(); // alice offers to buy 100 EUR for 100 XRP. env(offer(alice, EUR(100), XRP(100))); env.close(); // becky offers to buy 100 XRP for 100 EUR. env(offer(becky, XRP(100), EUR(100))); env.close(); // Get account_lines for alice. Limit at 1, so we get a marker. Json::Value linesBegParams; linesBegParams[jss::account] = alice.human(); linesBegParams[jss::limit] = 2; auto const linesBeg = env.rpc("json", "account_lines", to_string(linesBegParams)); BEAST_EXPECT( linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD"); BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker)); // alice pays 100 EUR to cheri. env(pay(alice, cheri, EUR(100))); env.close(); // Since alice paid all her EUR to cheri, alice should no longer // have a trust line to gw1. So the old marker should now be invalid. Json::Value linesEndParams; linesEndParams[jss::account] = alice.human(); linesEndParams[jss::marker] = linesBeg[jss::result][jss::marker]; auto const linesEnd = env.rpc("json", "account_lines", to_string(linesEndParams)); BEAST_EXPECT( linesEnd[jss::result][jss::error_message] == RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]); } void testAccountLinesWalkMarkers() { testcase("Marker can point to any appropriate ledger entry type"); using namespace test::jtx; using namespace std::chrono_literals; Env env(*this); // The goal of this test is observe account_lines RPC calls return an // error message when the SLE pointed to by the marker is not owned by // the Account being traversed. // // To start, we'll create an environment with some trust lines, offers // and a signers list. Account const alice{"alice"}; Account const becky{"becky"}; Account const gw1{"gw1"}; env.fund(XRP(10000), alice, becky, gw1); env.close(); auto payChan = [](Account const& account, Account const& to, STAmount const& amount, NetClock::duration const& settleDelay, PublicKey const& pk) { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); jv["SettleDelay"] = settleDelay.count(); jv["PublicKey"] = strHex(pk.slice()); return jv; }; // Test all available object types. Not all of these objects will be // included in the search, nor found by `account_objects`. If that ever // changes for any reason, this test will help catch that. // // SignerList, for alice Account const bogie{"bogie"}; env(signers(alice, 2, {{bogie, 3}})); env.close(); // SignerList, includes alice env(signers(becky, 2, {{alice, 3}})); env.close(); // Trust lines auto const EUR = gw1["EUR"]; env(trust(alice, EUR(200))); env(trust(becky, EUR(200))); env.close(); // Escrow, in each direction env(escrow::create(alice, becky, XRP(1000)), escrow::finish_time(env.now() + 1s)); env(escrow::create(becky, alice, XRP(1000)), escrow::finish_time(env.now() + 1s)); // Pay channels, in each direction env(payChan(alice, becky, XRP(1000), 100s, alice.pk())); env(payChan(becky, alice, XRP(1000), 100s, becky.pk())); // Mint NFTs, for each account uint256 const aliceNFtokenID = token::getNextID(env, alice, 0, tfTransferable); env(token::mint(alice, 0), txflags(tfTransferable)); uint256 const beckyNFtokenID = token::getNextID(env, becky, 0, tfTransferable); env(token::mint(becky, 0), txflags(tfTransferable)); // NFT Offers, for each other's NFTs env(token::createOffer(alice, beckyNFtokenID, drops(1)), token::owner(becky)); env(token::createOffer(becky, aliceNFtokenID, drops(1)), token::owner(alice)); env(token::createOffer(becky, beckyNFtokenID, drops(1)), txflags(tfSellNFToken), token::destination(alice)); env(token::createOffer(alice, aliceNFtokenID, drops(1)), txflags(tfSellNFToken), token::destination(becky)); env(token::createOffer(gw1, beckyNFtokenID, drops(1)), token::owner(becky), token::destination(alice)); env(token::createOffer(gw1, aliceNFtokenID, drops(1)), token::owner(alice), token::destination(becky)); env(token::createOffer(becky, beckyNFtokenID, drops(1)), txflags(tfSellNFToken)); env(token::createOffer(alice, aliceNFtokenID, drops(1)), txflags(tfSellNFToken)); // Checks, in each direction env(check::create(alice, becky, XRP(50))); env(check::create(becky, alice, XRP(50))); // Deposit preauth, in each direction env(deposit::auth(alice, becky)); env(deposit::auth(becky, alice)); // Offers, one where alice is the owner, and one where alice is the // issuer auto const USDalice = alice["USD"]; env(offer(alice, EUR(10), XRP(100))); env(offer(becky, USDalice(10), XRP(100))); // Tickets env(ticket::create(alice, 2)); // Add another trustline for good measure auto const BTCbecky = becky["BTC"]; env(trust(alice, BTCbecky(200))); env.close(); { // Now make repeated calls to `account_lines` with a limit of 1. // That should iterate all of alice's relevant objects, even though // the list will be empty for most calls. auto getNextLine = [](Env& env, Account const& alice, std::optional const marker) { Json::Value params(Json::objectValue); params[jss::account] = alice.human(); params[jss::limit] = 1; if (marker) params[jss::marker] = *marker; return env.rpc("json", "account_lines", to_string(params)); }; auto aliceLines = getNextLine(env, alice, std::nullopt); constexpr std::size_t expectedIterations = 16; constexpr std::size_t expectedLines = 2; constexpr std::size_t expectedNFTs = 1; std::size_t foundLines = 0; auto hasMarker = [](auto const& aliceLines) { return aliceLines[jss::result].isMember(jss::marker); }; auto marker = [](auto const& aliceLines) { return aliceLines[jss::result][jss::marker].asString(); }; auto checkLines = [](auto const& aliceLines) { return aliceLines.isMember(jss::result) && !aliceLines[jss::result].isMember(jss::error_message) && aliceLines[jss::result].isMember(jss::lines) && aliceLines[jss::result][jss::lines].isArray() && aliceLines[jss::result][jss::lines].size() <= 1; }; BEAST_EXPECT(hasMarker(aliceLines)); BEAST_EXPECT(checkLines(aliceLines)); BEAST_EXPECT(aliceLines[jss::result][jss::lines].size() == 0); int iterations = 1; while (hasMarker(aliceLines)) { // Iterate through the markers aliceLines = getNextLine(env, alice, marker(aliceLines)); BEAST_EXPECT(checkLines(aliceLines)); foundLines += aliceLines[jss::result][jss::lines].size(); ++iterations; } BEAST_EXPECT(expectedLines == foundLines); Json::Value aliceObjectsParams2; aliceObjectsParams2[jss::account] = alice.human(); aliceObjectsParams2[jss::limit] = 200; Json::Value const aliceObjects = env.rpc( "json", "account_objects", to_string(aliceObjectsParams2)); BEAST_EXPECT(aliceObjects.isMember(jss::result)); BEAST_EXPECT( !aliceObjects[jss::result].isMember(jss::error_message)); BEAST_EXPECT( aliceObjects[jss::result].isMember(jss::account_objects)); BEAST_EXPECT( aliceObjects[jss::result][jss::account_objects].isArray()); // account_objects does not currently return NFTPages. If // that ever changes, without also changing account_lines, // this test will need to be updated. BEAST_EXPECT( aliceObjects[jss::result][jss::account_objects].size() == iterations + expectedNFTs); // If ledger object association ever changes, for whatever // reason, this test will need to be updated. BEAST_EXPECTS( iterations == expectedIterations, std::to_string(iterations)); // Get becky's objects just to confirm that they're symmetrical Json::Value beckyObjectsParams; beckyObjectsParams[jss::account] = becky.human(); beckyObjectsParams[jss::limit] = 200; Json::Value const beckyObjects = env.rpc( "json", "account_objects", to_string(beckyObjectsParams)); BEAST_EXPECT(beckyObjects.isMember(jss::result)); BEAST_EXPECT( !beckyObjects[jss::result].isMember(jss::error_message)); BEAST_EXPECT( beckyObjects[jss::result].isMember(jss::account_objects)); BEAST_EXPECT( beckyObjects[jss::result][jss::account_objects].isArray()); // becky should have the same number of objects as alice, except the // 2 tickets that only alice created. BEAST_EXPECT( beckyObjects[jss::result][jss::account_objects].size() == aliceObjects[jss::result][jss::account_objects].size() - 2); } } // test API V2 void testAccountLines2() { testcase("V2: account_lines"); using namespace test::jtx; Env env(*this); { // account_lines with mal-formed json2 (missing id field). Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); } { // account_lines with no account. Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; request[jss::id] = 5; auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == RPC::missing_field_error(jss::account)[jss::error_message]); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); } { // account_lines with a malformed account. Json::Value params; params[jss::account] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"; Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; request[jss::id] = 5; request[jss::params] = params; auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == RPC::make_error(rpcACT_MALFORMED)[jss::error_message]); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); } Account const alice{"alice"}; { // account_lines on an unfunded account. Json::Value params; params[jss::account] = alice.human(); Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; request[jss::id] = 5; request[jss::params] = params; auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == RPC::make_error(rpcACT_NOT_FOUND)[jss::error_message]); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); } env.fund(XRP(10000), alice); env.close(); LedgerHeader const ledger3Info = env.closed()->header(); BEAST_EXPECT(ledger3Info.seq == 3); { // alice is funded but has no lines. An empty array is returned. Json::Value params; params[jss::account] = alice.human(); Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; request[jss::id] = 5; request[jss::params] = params; auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT(lines[jss::result][jss::lines].isArray()); BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); } { // Specify a ledger that doesn't exist. Json::Value params; params[jss::account] = alice.human(); params[jss::ledger_index] = "nonsense"; Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; request[jss::id] = 5; request[jss::params] = params; auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == "Invalid field 'ledger_index', not string or number."); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); } { // Specify a different ledger that doesn't exist. Json::Value params; params[jss::account] = alice.human(); params[jss::ledger_index] = 50000; Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; request[jss::id] = 5; request[jss::params] = params; auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT(lines[jss::error][jss::message] == "ledgerNotFound"); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); } // Create trust lines to share with alice. Account const gw1{"gw1"}; env.fund(XRP(10000), gw1); std::vector gw1Currencies; for (char c = 0; c <= ('Z' - 'A'); ++c) { // gw1 currencies have names "YAA" -> "YAZ". gw1Currencies.push_back( gw1[std::string("YA") + static_cast('A' + c)]); IOU const& gw1Currency = gw1Currencies.back(); // Establish trust lines. env(trust(alice, gw1Currency(100 + c))); env(pay(gw1, alice, gw1Currency(50 + c))); } env.close(); LedgerHeader const ledger4Info = env.closed()->header(); BEAST_EXPECT(ledger4Info.seq == 4); // Add another set of trust lines in another ledger so we can see // differences in historic ledgers. Account const gw2{"gw2"}; env.fund(XRP(10000), gw2); // gw2 requires authorization. env(fset(gw2, asfRequireAuth)); env.close(); std::vector gw2Currencies; for (char c = 0; c <= ('Z' - 'A'); ++c) { // gw2 currencies have names "ZAA" -> "ZAZ". gw2Currencies.push_back( gw2[std::string("ZA") + static_cast('A' + c)]); IOU const& gw2Currency = gw2Currencies.back(); // Establish trust lines. env(trust(alice, gw2Currency(200 + c))); env(trust(gw2, gw2Currency(0), alice, tfSetfAuth)); env.close(); env(pay(gw2, alice, gw2Currency(100 + c))); env.close(); // Set flags on gw2 trust lines so we can look for them. env(trust( alice, gw2Currency(0), gw2, tfSetNoRipple | tfSetFreeze | tfSetDeepFreeze)); } env.close(); LedgerHeader const ledger58Info = env.closed()->header(); BEAST_EXPECT(ledger58Info.seq == 58); // A re-usable test for historic ledgers. auto testAccountLinesHistory = [this, &env]( Account const& account, LedgerHeader const& info, int count) { // Get account_lines by ledger index. Json::Value paramsSeq; paramsSeq[jss::account] = account.human(); paramsSeq[jss::ledger_index] = info.seq; Json::Value requestSeq; requestSeq[jss::method] = "account_lines"; requestSeq[jss::jsonrpc] = "2.0"; requestSeq[jss::ripplerpc] = "2.0"; requestSeq[jss::id] = 5; requestSeq[jss::params] = paramsSeq; auto const linesSeq = env.rpc("json2", to_string(requestSeq)); BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray()); BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count); BEAST_EXPECT( linesSeq.isMember(jss::jsonrpc) && linesSeq[jss::jsonrpc] == "2.0"); BEAST_EXPECT( linesSeq.isMember(jss::ripplerpc) && linesSeq[jss::ripplerpc] == "2.0"); BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5); // Get account_lines by ledger hash. Json::Value paramsHash; paramsHash[jss::account] = account.human(); paramsHash[jss::ledger_hash] = to_string(info.hash); Json::Value requestHash; requestHash[jss::method] = "account_lines"; requestHash[jss::jsonrpc] = "2.0"; requestHash[jss::ripplerpc] = "2.0"; requestHash[jss::id] = 5; requestHash[jss::params] = paramsHash; auto const linesHash = env.rpc("json2", to_string(requestHash)); BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray()); BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count); BEAST_EXPECT( linesHash.isMember(jss::jsonrpc) && linesHash[jss::jsonrpc] == "2.0"); BEAST_EXPECT( linesHash.isMember(jss::ripplerpc) && linesHash[jss::ripplerpc] == "2.0"); BEAST_EXPECT( linesHash.isMember(jss::id) && linesHash[jss::id] == 5); }; // Alice should have no trust lines in ledger 3. testAccountLinesHistory(alice, ledger3Info, 0); // Alice should have 26 trust lines in ledger 4. testAccountLinesHistory(alice, ledger4Info, 26); // Alice should have 52 trust lines in ledger 58. testAccountLinesHistory(alice, ledger58Info, 52); { // Surprisingly, it's valid to specify both index and hash, in // which case the hash wins. Json::Value params; params[jss::account] = alice.human(); params[jss::ledger_hash] = to_string(ledger4Info.hash); params[jss::ledger_index] = ledger58Info.seq; Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; request[jss::id] = 5; request[jss::params] = params; auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams"); BEAST_EXPECT( lines[jss::error][jss::message] == "Exactly one of 'ledger_hash' or 'ledger_index' can be " "specified."); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); } { // alice should have 52 trust lines in the current ledger. Json::Value params; params[jss::account] = alice.human(); Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; request[jss::id] = 5; request[jss::params] = params; auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT(lines[jss::result][jss::lines].isArray()); BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); } { // alice should have 26 trust lines with gw1. Json::Value params; params[jss::account] = alice.human(); params[jss::peer] = gw1.human(); Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; request[jss::id] = 5; request[jss::params] = params; auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT(lines[jss::result][jss::lines].isArray()); BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); } { // Use a malformed peer. Json::Value params; params[jss::account] = alice.human(); params[jss::peer] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj"; Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; request[jss::id] = 5; request[jss::params] = params; auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == RPC::make_error(rpcACT_MALFORMED)[jss::error_message]); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); } { // A negative limit should fail. Json::Value params; params[jss::account] = alice.human(); params[jss::limit] = -1; Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; request[jss::id] = 5; request[jss::params] = params; auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == RPC::expected_field_message(jss::limit, "unsigned integer")); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); } { // Limit the response to 1 trust line. Json::Value paramsA; paramsA[jss::account] = alice.human(); paramsA[jss::limit] = 1; Json::Value requestA; requestA[jss::method] = "account_lines"; requestA[jss::jsonrpc] = "2.0"; requestA[jss::ripplerpc] = "2.0"; requestA[jss::id] = 5; requestA[jss::params] = paramsA; auto const linesA = env.rpc("json2", to_string(requestA)); BEAST_EXPECT(linesA[jss::result][jss::lines].isArray()); BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1); BEAST_EXPECT( linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0"); BEAST_EXPECT( linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0"); BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5); // Pick up from where the marker left off. We should get 51. auto marker = linesA[jss::result][jss::marker].asString(); Json::Value paramsB; paramsB[jss::account] = alice.human(); paramsB[jss::marker] = marker; Json::Value requestB; requestB[jss::method] = "account_lines"; requestB[jss::jsonrpc] = "2.0"; requestB[jss::ripplerpc] = "2.0"; requestB[jss::id] = 5; requestB[jss::params] = paramsB; auto const linesB = env.rpc("json2", to_string(requestB)); BEAST_EXPECT(linesB[jss::result][jss::lines].isArray()); BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51); BEAST_EXPECT( linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0"); BEAST_EXPECT( linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0"); BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5); // Go again from where the marker left off, but set a limit of 3. Json::Value paramsC; paramsC[jss::account] = alice.human(); paramsC[jss::limit] = 3; paramsC[jss::marker] = marker; Json::Value requestC; requestC[jss::method] = "account_lines"; requestC[jss::jsonrpc] = "2.0"; requestC[jss::ripplerpc] = "2.0"; requestC[jss::id] = 5; requestC[jss::params] = paramsC; auto const linesC = env.rpc("json2", to_string(requestC)); BEAST_EXPECT(linesC[jss::result][jss::lines].isArray()); BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3); BEAST_EXPECT( linesC.isMember(jss::jsonrpc) && linesC[jss::jsonrpc] == "2.0"); BEAST_EXPECT( linesC.isMember(jss::ripplerpc) && linesC[jss::ripplerpc] == "2.0"); BEAST_EXPECT(linesC.isMember(jss::id) && linesC[jss::id] == 5); // Mess with the marker so it becomes bad and check for the error. marker[5] = marker[5] == '7' ? '8' : '7'; Json::Value paramsD; paramsD[jss::account] = alice.human(); paramsD[jss::marker] = marker; Json::Value requestD; requestD[jss::method] = "account_lines"; requestD[jss::jsonrpc] = "2.0"; requestD[jss::ripplerpc] = "2.0"; requestD[jss::id] = 5; requestD[jss::params] = paramsD; auto const linesD = env.rpc("json2", to_string(requestD)); BEAST_EXPECT( linesD[jss::error][jss::message] == RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]); BEAST_EXPECT( linesD.isMember(jss::jsonrpc) && linesD[jss::jsonrpc] == "2.0"); BEAST_EXPECT( linesD.isMember(jss::ripplerpc) && linesD[jss::ripplerpc] == "2.0"); BEAST_EXPECT(linesD.isMember(jss::id) && linesD[jss::id] == 5); } { // A non-string marker should also fail. Json::Value params; params[jss::account] = alice.human(); params[jss::marker] = true; Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; request[jss::id] = 5; request[jss::params] = params; auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == RPC::expected_field_message(jss::marker, "string")); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); } { // Check that the flags we expect from alice to gw2 are present. Json::Value params; params[jss::account] = alice.human(); params[jss::limit] = 10; params[jss::peer] = gw2.human(); Json::Value request; request[jss::method] = "account_lines"; request[jss::jsonrpc] = "2.0"; request[jss::ripplerpc] = "2.0"; request[jss::id] = 5; request[jss::params] = params; auto const lines = env.rpc("json2", to_string(request)); auto const& line = lines[jss::result][jss::lines][0u]; BEAST_EXPECT(line[jss::freeze].asBool() == true); BEAST_EXPECT(line[jss::deep_freeze].asBool() == true); BEAST_EXPECT(line[jss::no_ripple].asBool() == true); BEAST_EXPECT(line[jss::peer_authorized].asBool() == true); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); } { // Check that the flags we expect from gw2 to alice are present. Json::Value paramsA; paramsA[jss::account] = gw2.human(); paramsA[jss::limit] = 1; paramsA[jss::peer] = alice.human(); Json::Value requestA; requestA[jss::method] = "account_lines"; requestA[jss::jsonrpc] = "2.0"; requestA[jss::ripplerpc] = "2.0"; requestA[jss::id] = 5; requestA[jss::params] = paramsA; auto const linesA = env.rpc("json2", to_string(requestA)); auto const& lineA = linesA[jss::result][jss::lines][0u]; BEAST_EXPECT(lineA[jss::freeze_peer].asBool() == true); BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true); BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true); BEAST_EXPECT(lineA[jss::authorized].asBool() == true); BEAST_EXPECT( linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0"); BEAST_EXPECT( linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0"); BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5); // Continue from the returned marker to make sure that works. BEAST_EXPECT(linesA[jss::result].isMember(jss::marker)); auto const marker = linesA[jss::result][jss::marker].asString(); Json::Value paramsB; paramsB[jss::account] = gw2.human(); paramsB[jss::limit] = 25; paramsB[jss::marker] = marker; paramsB[jss::peer] = alice.human(); Json::Value requestB; requestB[jss::method] = "account_lines"; requestB[jss::jsonrpc] = "2.0"; requestB[jss::ripplerpc] = "2.0"; requestB[jss::id] = 5; requestB[jss::params] = paramsB; auto const linesB = env.rpc("json2", to_string(requestB)); BEAST_EXPECT(linesB[jss::result][jss::lines].isArray()); BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25); BEAST_EXPECT(!linesB[jss::result].isMember(jss::marker)); BEAST_EXPECT( linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0"); BEAST_EXPECT( linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0"); BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5); } } // test API V2 void testAccountLineDelete2() { testcase("V2: account_lines with removed marker"); using namespace test::jtx; Env env(*this); // The goal here is to observe account_lines marker behavior if the // entry pointed at by a returned marker is removed from the ledger. // // It isn't easy to explicitly delete a trust line, so we do so in a // round-about fashion. It takes 4 actors: // o Gateway gw1 issues EUR // o alice offers to buy 100 EUR for 100 XRP. // o becky offers to sell 100 EUR for 100 XRP. // There will now be an inferred trustline between alice and gw2. // o alice pays her 100 EUR to cheri. // alice should now have no EUR and no trustline to gw2. Account const alice{"alice"}; Account const becky{"becky"}; Account const cheri{"cheri"}; Account const gw1{"gw1"}; Account const gw2{"gw2"}; env.fund(XRP(10000), alice, becky, cheri, gw1, gw2); env.close(); auto const USD = gw1["USD"]; auto const AUD = gw1["AUD"]; auto const EUR = gw2["EUR"]; env(trust(alice, USD(200))); env(trust(alice, AUD(200))); env(trust(becky, EUR(200))); env(trust(cheri, EUR(200))); env.close(); // becky gets 100 EUR from gw1. env(pay(gw2, becky, EUR(100))); env.close(); // alice offers to buy 100 EUR for 100 XRP. env(offer(alice, EUR(100), XRP(100))); env.close(); // becky offers to buy 100 XRP for 100 EUR. env(offer(becky, XRP(100), EUR(100))); env.close(); // Get account_lines for alice. Limit at 1, so we get a marker. Json::Value linesBegParams; linesBegParams[jss::account] = alice.human(); linesBegParams[jss::limit] = 2; Json::Value linesBegRequest; linesBegRequest[jss::method] = "account_lines"; linesBegRequest[jss::jsonrpc] = "2.0"; linesBegRequest[jss::ripplerpc] = "2.0"; linesBegRequest[jss::id] = 5; linesBegRequest[jss::params] = linesBegParams; auto const linesBeg = env.rpc("json2", to_string(linesBegRequest)); BEAST_EXPECT( linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD"); BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker)); BEAST_EXPECT( linesBeg.isMember(jss::jsonrpc) && linesBeg[jss::jsonrpc] == "2.0"); BEAST_EXPECT( linesBeg.isMember(jss::ripplerpc) && linesBeg[jss::ripplerpc] == "2.0"); BEAST_EXPECT(linesBeg.isMember(jss::id) && linesBeg[jss::id] == 5); // alice pays 100 USD to cheri. env(pay(alice, cheri, EUR(100))); env.close(); // Since alice paid all her EUR to cheri, alice should no longer // have a trust line to gw1. So the old marker should now be invalid. Json::Value linesEndParams; linesEndParams[jss::account] = alice.human(); linesEndParams[jss::marker] = linesBeg[jss::result][jss::marker]; Json::Value linesEndRequest; linesEndRequest[jss::method] = "account_lines"; linesEndRequest[jss::jsonrpc] = "2.0"; linesEndRequest[jss::ripplerpc] = "2.0"; linesEndRequest[jss::id] = 5; linesEndRequest[jss::params] = linesEndParams; auto const linesEnd = env.rpc("json2", to_string(linesEndRequest)); BEAST_EXPECT( linesEnd[jss::error][jss::message] == RPC::make_error(rpcINVALID_PARAMS)[jss::error_message]); BEAST_EXPECT( linesEnd.isMember(jss::jsonrpc) && linesEnd[jss::jsonrpc] == "2.0"); BEAST_EXPECT( linesEnd.isMember(jss::ripplerpc) && linesEnd[jss::ripplerpc] == "2.0"); BEAST_EXPECT(linesEnd.isMember(jss::id) && linesEnd[jss::id] == 5); } void run() override { testAccountLines(); testAccountLinesMarker(); testAccountLineDelete(); testAccountLinesWalkMarkers(); testAccountLines2(); testAccountLineDelete2(); } }; BEAST_DEFINE_TESTSUITE(AccountLines, rpc, ripple); } // namespace RPC } // namespace ripple