Make Clio RPCs more consistent with rippled (#110)

* parse ledger_index as number

* allow websocket to use "command" or "method"

* mark all non-forwarded responses as validated

* dont mark forwarded errors as successful

* reduce forwarding Websocket expiration 30->3 seconds

* fix merge conflict in test.py

* adds ledger_current and ledger_closed

* deserialize `taker_gets_funded` into amount json

* limit account RPCs by number of objects traversed

* assign result correctly
This commit is contained in:
Nathan Nichols
2022-03-21 18:28:03 -05:00
committed by GitHub
parent 945222840b
commit 1d5c482d9c
17 changed files with 394 additions and 113 deletions

View File

@@ -1055,7 +1055,7 @@ ETLSourceImpl<Derived>::forwardToRippled(
if (ec)
return {};
ws->next_layer().expires_after(std::chrono::seconds(30));
ws->next_layer().expires_after(std::chrono::seconds(3));
BOOST_LOG_TRIVIAL(debug) << "Connecting websocket";
// Make the connection on the IP address we get from a lookup

View File

@@ -1,4 +1,5 @@
#include <rpc/Counters.h>
#include <rpc/RPC.h>
namespace RPC {
@@ -19,6 +20,9 @@ Counters::initializeCounter(std::string const& method)
void
Counters::rpcErrored(std::string const& method)
{
if (!validHandler(method))
return;
initializeCounter(method);
std::shared_lock lk(mutex_);
@@ -32,6 +36,9 @@ Counters::rpcComplete(
std::string const& method,
std::chrono::microseconds const& rpcDuration)
{
if (!validHandler(method))
return;
initializeCounter(method);
std::shared_lock lk(mutex_);
@@ -44,6 +51,9 @@ Counters::rpcComplete(
void
Counters::rpcForwarded(std::string const& method)
{
if (!validHandler(method))
return;
initializeCounter(method);
std::shared_lock lk(mutex_);

View File

@@ -18,10 +18,16 @@ make_WsContext(
Counters& counters,
std::string const& clientIp)
{
if (!request.contains("command"))
boost::json::value commandValue = nullptr;
if (!request.contains("command") && request.contains("method"))
commandValue = request.at("method");
else if (request.contains("command") && !request.contains("method"))
commandValue = request.at("command");
if (!commandValue.is_string())
return {};
std::string command = request.at("command").as_string().c_str();
std::string command = commandValue.as_string().c_str();
return Context{
yc,
@@ -142,10 +148,17 @@ static std::unordered_set<std::string> forwardCommands{
"submit",
"submit_multisigned",
"fee",
"path_find",
"ledger_closed",
"ledger_current",
"ripple_path_find",
"manifest"};
bool
validHandler(std::string const& method)
{
return handlerTable.contains(method) || forwardCommands.contains(method);
}
bool
shouldForwardToRippled(Context const& ctx)
{
@@ -203,7 +216,12 @@ buildResponse(Context const& ctx)
try
{
return method(ctx);
auto v = method(ctx);
if (auto object = std::get_if<boost::json::object>(&v))
(*object)["validated"] = true;
return v;
}
catch (InvalidParamsError const& err)
{

View File

@@ -75,6 +75,24 @@ struct Context
};
using Error = ripple::error_code_i;
struct AccountCursor
{
ripple::uint256 index;
std::uint32_t hint;
std::string
toString()
{
return ripple::strHex(index) + "," + std::to_string(hint);
}
bool
isNonZero()
{
return index.isNonZero() || hint != 0;
}
};
struct Status
{
Error error = Error::rpcSUCCESS;
@@ -169,6 +187,9 @@ make_HttpContext(
Result
buildResponse(Context const& ctx);
bool
validHandler(std::string const& method);
} // namespace RPC
#endif // REPORTING_RPC_H_INCLUDED

View File

@@ -67,6 +67,81 @@ getRequiredUInt(boost::json::object const& request, std::string const& field)
throw InvalidParamsError("Missing field " + field);
}
bool
isOwnedByAccount(ripple::SLE const& sle, ripple::AccountID const& accountID)
{
if (sle.getType() == ripple::ltRIPPLE_STATE)
{
return (sle.getFieldAmount(ripple::sfLowLimit).getIssuer() ==
accountID) ||
(sle.getFieldAmount(ripple::sfHighLimit).getIssuer() == accountID);
}
else if (sle.isFieldPresent(ripple::sfAccount))
{
return sle.getAccountID(ripple::sfAccount) == accountID;
}
else if (sle.getType() == ripple::ltSIGNER_LIST)
{
ripple::Keylet const accountSignerList =
ripple::keylet::signers(accountID);
return sle.key() == accountSignerList.key;
}
return false;
}
std::optional<AccountCursor>
parseAccountCursor(
BackendInterface const& backend,
std::uint32_t seq,
std::optional<std::string> jsonCursor,
ripple::AccountID const& accountID,
boost::asio::yield_context& yield)
{
ripple::uint256 cursorIndex = beast::zero;
std::uint64_t startHint = 0;
if (!jsonCursor)
return AccountCursor({cursorIndex, startHint});
// Cursor is composed of a comma separated index and start hint. The
// former will be read as hex, and the latter using boost lexical cast.
std::stringstream cursor(*jsonCursor);
std::string value;
if (!std::getline(cursor, value, ','))
return {};
if (!cursorIndex.parseHex(value))
return {};
if (!std::getline(cursor, value, ','))
return {};
try
{
startHint = boost::lexical_cast<std::uint64_t>(value);
}
catch (boost::bad_lexical_cast&)
{
return {};
}
// We then must check if the object pointed to by the marker is actually
// owned by the account in the request.
auto const ownedNode = backend.fetchLedgerObject(cursorIndex, seq, yield);
if (!ownedNode)
return {};
ripple::SerialIter it{ownedNode->data(), ownedNode->size()};
ripple::SLE sle{it, cursorIndex};
if (!isOwnedByAccount(sle, accountID))
return {};
return AccountCursor({cursorIndex, startHint});
}
std::optional<std::string>
getString(boost::json::object const& request, std::string const& field)
{
@@ -341,18 +416,28 @@ toJson(ripple::LedgerInfo const& lgrInfo)
return header;
}
std::optional<std::uint32_t>
parseStringAsUInt(std::string const& value)
{
std::optional<std::uint32_t> index = {};
try
{
index = boost::lexical_cast<std::uint32_t>(value);
}
catch (boost::bad_lexical_cast const&)
{
}
return index;
}
std::variant<Status, ripple::LedgerInfo>
ledgerInfoFromRequest(Context const& ctx)
{
auto indexValue = ctx.params.contains("ledger_index")
? ctx.params.at("ledger_index")
: nullptr;
auto hashValue = ctx.params.contains("ledger_hash")
? ctx.params.at("ledger_hash")
: nullptr;
std::optional<ripple::LedgerInfo> lgrInfo;
if (!hashValue.is_null())
{
if (!hashValue.is_string())
@@ -362,26 +447,38 @@ ledgerInfoFromRequest(Context const& ctx)
if (!ledgerHash.parseHex(hashValue.as_string().c_str()))
return Status{Error::rpcINVALID_PARAMS, "ledgerHashMalformed"};
lgrInfo = ctx.backend->fetchLedgerByHash(ledgerHash, ctx.yield);
auto lgrInfo = ctx.backend->fetchLedgerByHash(ledgerHash, ctx.yield);
}
else if (!indexValue.is_null())
{
std::uint32_t ledgerSequence;
if (indexValue.is_string() && indexValue.as_string() == "validated")
ledgerSequence = ctx.range.maxSequence;
else if (!indexValue.is_string() && indexValue.is_int64())
ledgerSequence = indexValue.as_int64();
else
return Status{Error::rpcINVALID_PARAMS, "ledgerIndexMalformed"};
lgrInfo = ctx.backend->fetchLedgerBySequence(ledgerSequence, ctx.yield);
auto indexValue = ctx.params.contains("ledger_index")
? ctx.params.at("ledger_index")
: nullptr;
std::optional<std::uint32_t> ledgerSequence = {};
if (!indexValue.is_null())
{
if (indexValue.is_string())
{
boost::json::string const& stringIndex = indexValue.as_string();
if (stringIndex == "validated")
ledgerSequence = ctx.range.maxSequence;
else
ledgerSequence = parseStringAsUInt(stringIndex.c_str());
}
else if (indexValue.is_int64())
ledgerSequence = indexValue.as_int64();
}
else
{
lgrInfo = ctx.backend->fetchLedgerBySequence(
ctx.range.maxSequence, ctx.yield);
ledgerSequence = ctx.range.maxSequence;
}
if (!ledgerSequence)
return Status{Error::rpcLGR_NOT_FOUND, "ledgerIndexMalformed"};
auto lgrInfo =
ctx.backend->fetchLedgerBySequence(*ledgerSequence, ctx.yield);
if (!lgrInfo)
return Status{Error::rpcLGR_NOT_FOUND, "ledgerNotFound"};
@@ -406,50 +503,155 @@ ledgerInfoToBlob(ripple::LedgerInfo const& info, bool includeHash)
return s.peekData();
}
std::optional<ripple::uint256>
std::uint64_t
getStartHint(ripple::SLE const& sle, ripple::AccountID const& accountID)
{
if (sle.getType() == ripple::ltRIPPLE_STATE)
{
if (sle.getFieldAmount(ripple::sfLowLimit).getIssuer() == accountID)
return sle.getFieldU64(ripple::sfLowNode);
else if (
sle.getFieldAmount(ripple::sfHighLimit).getIssuer() == accountID)
return sle.getFieldU64(ripple::sfHighNode);
}
if (!sle.isFieldPresent(ripple::sfOwnerNode))
return 0;
return sle.getFieldU64(ripple::sfOwnerNode);
}
std::variant<Status, AccountCursor>
traverseOwnedNodes(
BackendInterface const& backend,
ripple::AccountID const& accountID,
std::uint32_t sequence,
ripple::uint256 const& cursor,
std::uint32_t limit,
std::optional<std::string> jsonCursor,
boost::asio::yield_context& yield,
std::function<bool(ripple::SLE)> atOwnedNode)
std::function<void(ripple::SLE)> atOwnedNode)
{
if (!backend.fetchLedgerObject(
ripple::keylet::account(accountID).key, sequence, yield))
throw AccountNotFoundError(ripple::toBase58(accountID));
auto parsedCursor =
parseAccountCursor(backend, sequence, jsonCursor, accountID, yield);
if (!parsedCursor)
return Status(ripple::rpcINVALID_PARAMS, "Malformed cursor");
auto cursor = AccountCursor({beast::zero, 0});
auto [hexCursor, startHint] = *parsedCursor;
auto const rootIndex = ripple::keylet::ownerDir(accountID);
auto currentIndex = rootIndex;
std::vector<ripple::uint256> keys;
std::optional<ripple::uint256> nextCursor = {};
keys.reserve(limit);
auto start = std::chrono::system_clock::now();
// If startAfter is not zero try jumping to that page using the hint
if (hexCursor.isNonZero())
{
auto const hintIndex = ripple::keylet::page(rootIndex, startHint);
auto hintDir =
backend.fetchLedgerObject(hintIndex.key, sequence, yield);
if (hintDir)
{
ripple::SerialIter it{hintDir->data(), hintDir->size()};
ripple::SLE sle{it, hintIndex.key};
for (auto const& key : sle.getFieldV256(ripple::sfIndexes))
{
if (key == hexCursor)
{
// We found the hint, we can start here
currentIndex = hintIndex;
break;
}
}
}
bool found = false;
for (;;)
{
auto ownedNode =
auto const ownerDir =
backend.fetchLedgerObject(currentIndex.key, sequence, yield);
if (!ownedNode)
if (!ownerDir)
return Status(
ripple::rpcINVALID_PARAMS, "Owner directory not found");
ripple::SerialIter it{ownerDir->data(), ownerDir->size()};
ripple::SLE sle{it, currentIndex.key};
for (auto const& key : sle.getFieldV256(ripple::sfIndexes))
{
if (!found)
{
if (key == hexCursor)
found = true;
}
else
{
keys.push_back(key);
if (--limit == 0)
{
break;
}
ripple::SerialIter it{ownedNode->data(), ownedNode->size()};
ripple::SLE dir{it, currentIndex.key};
for (auto const& key : dir.getFieldV256(ripple::sfIndexes))
{
if (key >= cursor)
keys.push_back(key);
}
}
auto const uNodeNext = sle.getFieldU64(ripple::sfIndexNext);
if (limit == 0)
{
cursor = AccountCursor({keys.back(), uNodeNext});
break;
}
auto const uNodeNext = dir.getFieldU64(ripple::sfIndexNext);
if (uNodeNext == 0)
break;
currentIndex = ripple::keylet::page(rootIndex, uNodeNext);
}
}
else
{
for (;;)
{
auto const ownerDir =
backend.fetchLedgerObject(currentIndex.key, sequence, yield);
if (!ownerDir)
return Status(ripple::rpcACT_NOT_FOUND);
ripple::SerialIter it{ownerDir->data(), ownerDir->size()};
ripple::SLE sle{it, currentIndex.key};
for (auto const& key : sle.getFieldV256(ripple::sfIndexes))
{
keys.push_back(key);
if (--limit == 0)
break;
}
auto const uNodeNext = sle.getFieldU64(ripple::sfIndexNext);
if (limit == 0)
{
cursor = AccountCursor({keys.back(), uNodeNext});
break;
}
if (uNodeNext == 0)
break;
currentIndex = ripple::keylet::page(rootIndex, uNodeNext);
}
}
auto end = std::chrono::system_clock::now();
BOOST_LOG_TRIVIAL(debug) << "Time loading owned directories: "
@@ -466,14 +668,14 @@ traverseOwnedNodes(
{
ripple::SerialIter it{objects[i].data(), objects[i].size()};
ripple::SLE sle(it, keys[i]);
if (!atOwnedNode(sle))
{
nextCursor = keys[i + 1];
break;
}
atOwnedNode(sle);
}
return nextCursor;
if (limit == 0)
return cursor;
return AccountCursor({beast::zero, 0});
}
std::optional<ripple::Seed>
@@ -934,7 +1136,8 @@ postProcessOrderBook(
else
{
saTakerGetsFunded = saOwnerFundsLimit;
offerJson["taker_gets_funded"] = saTakerGetsFunded.getText();
offerJson["taker_gets_funded"] = toBoostJson(
saTakerGetsFunded.getJson(ripple::JsonOptions::none));
offerJson["taker_pays_funded"] = toBoostJson(
std::min(
saTakerPays,

View File

@@ -20,6 +20,20 @@ accountFromStringStrict(std::string const& account);
std::optional<ripple::AccountID>
accountFromSeed(std::string const& account);
bool
isOwnedByAccount(ripple::SLE const& sle, ripple::AccountID const& accountID);
std::uint64_t
getStartHint(ripple::SLE const& sle, ripple::AccountID const& accountID);
std::optional<AccountCursor>
parseAccountCursor(
BackendInterface const& backend,
std::uint32_t seq,
std::optional<std::string> jsonCursor,
ripple::AccountID const& accountID,
boost::asio::yield_context& yield);
// TODO this function should probably be in a different file and namespace
std::pair<
std::shared_ptr<ripple::STTx const>,
@@ -69,14 +83,15 @@ generatePubLedgerMessage(
std::variant<Status, ripple::LedgerInfo>
ledgerInfoFromRequest(Context const& ctx);
std::optional<ripple::uint256>
std::variant<Status, AccountCursor>
traverseOwnedNodes(
BackendInterface const& backend,
ripple::AccountID const& accountID,
std::uint32_t sequence,
ripple::uint256 const& cursor,
std::uint32_t limit,
std::optional<std::string> jsonCursor,
boost::asio::yield_context& yield,
std::function<bool(ripple::SLE)> atOwnedNode);
std::function<void(ripple::SLE)> atOwnedNode);
std::variant<Status, std::pair<ripple::PublicKey, ripple::SecretKey>>
keypairFromRequst(boost::json::object const& request);

View File

@@ -90,14 +90,13 @@ doAccountChannels(Context const& context)
return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"};
}
ripple::uint256 marker;
std::optional<std::string> marker = {};
if (request.contains("marker"))
{
if (!request.at("marker").is_string())
return Status{Error::rpcINVALID_PARAMS, "markerNotString"};
if (!marker.parseHex(request.at("marker").as_string().c_str()))
return Status{Error::rpcINVALID_PARAMS, "malformedCursor"};
marker = request.at("marker").as_string().c_str();
}
response["account"] = ripple::to_string(*accountID);
@@ -121,18 +120,25 @@ doAccountChannels(Context const& context)
return true;
};
auto nextCursor = traverseOwnedNodes(
auto next = traverseOwnedNodes(
*context.backend,
*accountID,
lgrInfo.seq,
limit,
marker,
context.yield,
addToResponse);
response["ledger_hash"] = ripple::strHex(lgrInfo.hash);
response["ledger_index"] = lgrInfo.seq;
if (nextCursor)
response["marker"] = ripple::strHex(*nextCursor);
if (auto status = std::get_if<RPC::Status>(&next))
return *status;
auto nextCursor = std::get<RPC::AccountCursor>(next);
if (nextCursor.isNonZero())
response["marker"] = nextCursor.toString();
return response;
}

View File

@@ -63,7 +63,8 @@ doAccountCurrencies(Context const& context)
*context.backend,
*accountID,
lgrInfo.seq,
beast::zero,
std::numeric_limits<std::uint32_t>::max(),
{},
context.yield,
addToResponse);

View File

@@ -135,14 +135,13 @@ doAccountLines(Context const& context)
return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"};
}
ripple::uint256 cursor;
if (request.contains("cursor"))
std::optional<std::string> cursor = {};
if (request.contains("marker"))
{
if (!request.at("cursor").is_string())
return Status{Error::rpcINVALID_PARAMS, "cursorNotString"};
if (!request.at("marker").is_string())
return Status{Error::rpcINVALID_PARAMS, "markerNotString"};
if (!cursor.parseHex(request.at("cursor").as_string().c_str()))
return Status{Error::rpcINVALID_PARAMS, "malformedCursor"};
cursor = request.at("marker").as_string().c_str();
}
response["account"] = ripple::to_string(*accountID);
@@ -151,30 +150,29 @@ doAccountLines(Context const& context)
response["lines"] = boost::json::value(boost::json::array_kind);
boost::json::array& jsonLines = response.at("lines").as_array();
auto const addToResponse = [&](ripple::SLE const& sle) {
auto const addToResponse = [&](ripple::SLE const& sle) -> void {
if (sle.getType() == ripple::ltRIPPLE_STATE)
{
if (limit-- == 0)
{
return false;
}
addLine(jsonLines, sle, *accountID, peerAccount);
}
return true;
};
auto nextCursor = traverseOwnedNodes(
auto next = traverseOwnedNodes(
*context.backend,
*accountID,
lgrInfo.seq,
limit,
cursor,
context.yield,
addToResponse);
if (nextCursor)
response["marker"] = ripple::strHex(*nextCursor);
if (auto status = std::get_if<RPC::Status>(&next))
return *status;
auto nextCursor = std::get<RPC::AccountCursor>(next);
if (nextCursor.isNonZero())
response["marker"] = nextCursor.toString();
return response;
}

View File

@@ -60,14 +60,13 @@ doAccountObjects(Context const& context)
return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"};
}
ripple::uint256 cursor;
std::optional<std::string> cursor = {};
if (request.contains("marker"))
{
if (!request.at("marker").is_string())
return Status{Error::rpcINVALID_PARAMS, "markerNotString"};
if (!cursor.parseHex(request.at("marker").as_string().c_str()))
return Status{Error::rpcINVALID_PARAMS, "malformedCursor"};
cursor = request.at("marker").as_string().c_str();
}
std::optional<ripple::LedgerEntryType> objectType = {};
@@ -90,21 +89,15 @@ doAccountObjects(Context const& context)
auto const addToResponse = [&](ripple::SLE const& sle) {
if (!objectType || objectType == sle.getType())
{
if (limit-- == 0)
{
return false;
}
jsonObjects.push_back(toJson(sle));
}
return true;
};
auto nextCursor = traverseOwnedNodes(
auto next = traverseOwnedNodes(
*context.backend,
*accountID,
lgrInfo.seq,
limit,
cursor,
context.yield,
addToResponse);
@@ -112,8 +105,13 @@ doAccountObjects(Context const& context)
response["ledger_hash"] = ripple::strHex(lgrInfo.hash);
response["ledger_index"] = lgrInfo.seq;
if (nextCursor)
response["marker"] = ripple::strHex(*nextCursor);
if (auto status = std::get_if<RPC::Status>(&next))
return *status;
auto nextCursor = std::get<RPC::AccountCursor>(next);
if (nextCursor.isNonZero())
response["marker"] = nextCursor.toString();
return response;
}

View File

@@ -97,14 +97,13 @@ doAccountOffers(Context const& context)
return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"};
}
ripple::uint256 cursor;
if (request.contains("cursor"))
std::optional<std::string> cursor = {};
if (request.contains("marker"))
{
if (!request.at("cursor").is_string())
return Status{Error::rpcINVALID_PARAMS, "cursorNotString"};
if (!request.at("marker").is_string())
return Status{Error::rpcINVALID_PARAMS, "markerNotString"};
if (!cursor.parseHex(request.at("cursor").as_string().c_str()))
return Status{Error::rpcINVALID_PARAMS, "malformedCursor"};
cursor = request.at("marker").as_string().c_str();
}
response["account"] = ripple::to_string(*accountID);
@@ -127,16 +126,22 @@ doAccountOffers(Context const& context)
return true;
};
auto nextCursor = traverseOwnedNodes(
auto next = traverseOwnedNodes(
*context.backend,
*accountID,
lgrInfo.seq,
limit,
cursor,
context.yield,
addToResponse);
if (nextCursor)
response["marker"] = ripple::strHex(*nextCursor);
if (auto status = std::get_if<RPC::Status>(&next))
return *status;
auto nextCursor = std::get<RPC::AccountCursor>(next);
if (nextCursor.isNonZero())
response["marker"] = nextCursor.toString();
return response;
}

View File

@@ -219,8 +219,6 @@ doAccountTx(Context const& context)
obj["date"] = txnPlusMeta.date;
}
obj["validated"] = true;
txns.push_back(obj);
if (!minReturnedIndex || txnPlusMeta.ledgerSequence < *minReturnedIndex)
minReturnedIndex = txnPlusMeta.ledgerSequence;

View File

@@ -150,7 +150,8 @@ doGatewayBalances(Context const& context)
*context.backend,
*accountID,
lgrInfo.seq,
beast::zero,
std::numeric_limits<std::uint32_t>::max(),
{},
context.yield,
addToResponse);

View File

@@ -90,6 +90,7 @@ doNoRippleCheck(Context const& context)
*context.backend,
*accountID,
lgrInfo.seq,
std::numeric_limits<std::uint32_t>::max(),
{},
context.yield,
[roleGateway,

View File

@@ -358,10 +358,14 @@ handle_request(
}
else
{
// This can still technically be an error. Clio counts forwarded
// requests as successful.
counters.rpcComplete(context->method, us);
result = std::get<boost::json::object>(v);
if (!result.contains("error"))
result["status"] = "success";
result["validated"] = true;
responseStr = boost::json::serialize(response);
}

View File

@@ -246,7 +246,6 @@ public:
auto id = request.contains("id") ? request.at("id") : nullptr;
response = getDefaultWsResponse(id);
boost::json::object& result = response["result"].as_object();
auto start = std::chrono::system_clock::now();
auto v = RPC::buildResponse(*context);
@@ -262,6 +261,7 @@ public:
if (!id.is_null())
error["id"] = id;
error["request"] = request;
response = error;
}
@@ -269,7 +269,7 @@ public:
{
counters_.rpcComplete(context->method, us);
result = std::get<boost::json::object>(v);
response["result"] = std::get<boost::json::object>(v);
}
}
catch (Backend::DatabaseTimeout const& t)

10
test.py
View File

@@ -1,5 +1,6 @@
#!/usr/bin/python3
from ast import parse
import websockets
import asyncio
import json
@@ -486,7 +487,7 @@ def writeLedgerData(data,filename):
f.write('\n')
async def ledger_data_full(ip, port, ledger, binary, limit, typ=None, count=-1):
async def ledger_data_full(ip, port, ledger, binary, limit, typ=None, count=-1, marker = None):
address = 'ws://' + str(ip) + ':' + str(port)
try:
blobs = []
@@ -494,7 +495,6 @@ async def ledger_data_full(ip, port, ledger, binary, limit, typ=None, count=-1):
async with websockets.connect(address,max_size=1000000000) as ws:
if int(limit) < 2048:
limit = 2048
marker = None
while True:
res = {}
if marker is None:
@@ -974,7 +974,8 @@ parser = argparse.ArgumentParser(description='test script for xrpl-reporting')
parser.add_argument('action', choices=["account_info", "tx", "txs","account_tx", "account_tx_full","ledger_data", "ledger_data_full", "book_offers","ledger","ledger_range","ledger_entry", "ledgers", "ledger_entries","account_txs","account_infos","account_txs_full","book_offerses","ledger_diff","perf","fee","server_info", "gaps","subscribe","verify_subscribe","call"])
parser.add_argument('--ip', default='127.0.0.1')
parser.add_argument('--port', default='51233')
parser.add_argument('--port', default='8080')
parser.add_argument('--marker')
parser.add_argument('--hash')
parser.add_argument('--account')
parser.add_argument('--ledger')
@@ -993,6 +994,7 @@ parser.add_argument('--transactions',default=False)
parser.add_argument('--minLedger',default=-1)
parser.add_argument('--maxLedger',default=-1)
parser.add_argument('--filename',default=None)
parser.add_argument('--ledgerIndex', default=-1)
parser.add_argument('--index')
parser.add_argument('--numPages',default=3)
parser.add_argument('--base')
@@ -1260,7 +1262,7 @@ def run(args):
args.filename = str(args.port) + "." + str(args.ledger)
res = asyncio.get_event_loop().run_until_complete(
ledger_data_full(args.ip, args.port, args.ledger, bool(args.binary), args.limit,args.type, int(args.count)))
ledger_data_full(args.ip, args.port, args.ledger, bool(args.binary), args.limit,args.type, int(args.count), args.marker))
print(len(res[0]))
if args.verify:
writeLedgerData(res,args.filename)