Files
rippled/src/test/rpc/AccountLines_test.cpp
Mayukha Vadari 62efecbfb1 refactor: rename info() to header() (#6138)
This change renames all the `info()` functions to `header()`, since they return `LedgerHeader` structs. It also renames the underlying variables from `info_` to `header_`.
2025-12-10 16:04:37 -05:00

1407 lines
59 KiB
C++

#include <test/jtx.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
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<IOU> gw1Currencies;
for (char c = 0; c <= ('Z' - 'A'); ++c)
{
// gw1 currencies have names "YAA" -> "YAZ".
gw1Currencies.push_back(
gw1[std::string("YA") + static_cast<char>('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<IOU> gw2Currencies;
for (char c = 0; c <= ('Z' - 'A'); ++c)
{
// gw2 currencies have names "ZAA" -> "ZAZ".
gw2Currencies.push_back(
gw2[std::string("ZA") + static_cast<char>('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<std::string> 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<IOU> gw1Currencies;
for (char c = 0; c <= ('Z' - 'A'); ++c)
{
// gw1 currencies have names "YAA" -> "YAZ".
gw1Currencies.push_back(
gw1[std::string("YA") + static_cast<char>('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<IOU> gw2Currencies;
for (char c = 0; c <= ('Z' - 'A'); ++c)
{
// gw2 currencies have names "ZAA" -> "ZAZ".
gw2Currencies.push_back(
gw2[std::string("ZA") + static_cast<char>('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