add nft_history and mark certain APIs as clio-only to improve error (#255)

This commit is contained in:
ledhed2222
2022-09-15 21:11:29 -04:00
committed by GitHub
parent 777ae24f62
commit 1764f3524e
12 changed files with 346 additions and 240 deletions

View File

@@ -651,6 +651,9 @@ CassandraBackend::fetchNFTTransactions(
static_cast<std::uint32_t>(lgrSeq),
static_cast<std::uint32_t>(txnIdx)};
// Only modify if forward because forward query
// (selectNFTTxForward_) orders by ledger/tx sequence >= whereas
// reverse query (selectNFTTx_) orders by ledger/tx sequence <.
if (forward)
++cursor->transactionIndex;
}
@@ -732,6 +735,9 @@ CassandraBackend::fetchAccountTransactions(
static_cast<std::uint32_t>(lgrSeq),
static_cast<std::uint32_t>(txnIdx)};
// Only modify if forward because forward query
// (selectAccountTxForward_) orders by ledger/tx sequence >= whereas
// reverse query (selectAccountTx_) orders by ledger/tx sequence <.
if (forward)
++cursor->transactionIndex;
}

View File

@@ -61,6 +61,9 @@ doNFTSellOffers(Context const& context);
Result
doNFTInfo(Context const& context);
Result
doNFTHistory(Context const& context);
// ledger methods
Result
doLedger(Context const& context);

View File

@@ -168,6 +168,7 @@ struct Handler
std::string method;
std::function<Result(Context const&)> handler;
std::optional<LimitRange> limit;
bool isClioOnly = false;
};
class HandlerTable
@@ -206,6 +207,12 @@ public:
return handlerMap_[command].handler;
}
bool
isClioOnly(std::string const& command)
{
return handlerMap_.contains(command) && handlerMap_[command].isClioOnly;
}
};
static HandlerTable handlerTable{
@@ -224,7 +231,8 @@ static HandlerTable handlerTable{
{"ledger", &doLedger, {}},
{"ledger_data", &doLedgerData, LimitRange{1, 100, 2048}},
{"nft_buy_offers", &doNFTBuyOffers, LimitRange{1, 50, 100}},
{"nft_info", &doNFTInfo},
{"nft_history", &doNFTHistory, LimitRange{1, 50, 100}, true},
{"nft_info", &doNFTInfo, {}, true},
{"nft_sell_offers", &doNFTSellOffers, LimitRange{1, 50, 100}},
{"ledger_entry", &doLedgerEntry, {}},
{"ledger_range", &doLedgerRange, {}},
@@ -252,6 +260,12 @@ validHandler(std::string const& method)
return handlerTable.contains(method) || forwardCommands.contains(method);
}
bool
isClioOnly(std::string const& method)
{
return handlerTable.isClioOnly(method);
}
Status
getLimit(RPC::Context const& context, std::uint32_t& limit)
{
@@ -287,6 +301,9 @@ shouldForwardToRippled(Context const& ctx)
{
auto request = ctx.params;
if (isClioOnly(ctx.method))
return false;
if (forwardCommands.find(ctx.method) != forwardCommands.end())
return true;

View File

@@ -226,6 +226,9 @@ buildResponse(Context const& ctx);
bool
validHandler(std::string const& method);
bool
isClioOnly(std::string const& method);
Status
getLimit(RPC::Context const& context, std::uint32_t& limit);

View File

@@ -268,7 +268,7 @@ getTaker(boost::json::object const& request, ripple::AccountID& takerID)
if (request.contains(JS(taker)))
{
auto parsed = parseTaker(request.at(JS(taker)));
if (auto status = std::get_if<Status>(&parsed))
if (auto status = std::get_if<Status>(&parsed); status)
return *status;
else
takerID = std::get<ripple::AccountID>(parsed);
@@ -808,15 +808,21 @@ traverseOwnedNodes(
}
auto end = std::chrono::system_clock::now();
BOOST_LOG_TRIVIAL(debug) << "Time loading owned directories: "
<< ((end - start).count() / 1000000000.0);
BOOST_LOG_TRIVIAL(debug)
<< "Time loading owned directories: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
.count()
<< " milliseconds";
start = std::chrono::system_clock::now();
auto objects = backend.fetchLedgerObjects(keys, sequence, yield);
end = std::chrono::system_clock::now();
BOOST_LOG_TRIVIAL(debug) << "Time loading owned entries: "
<< ((end - start).count() / 1000000000.0);
BOOST_LOG_TRIVIAL(debug)
<< "Time loading owned entries: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
.count()
<< " milliseconds";
for (auto i = 0; i < objects.size(); ++i)
{
@@ -1480,4 +1486,211 @@ specifiesCurrentOrClosedLedger(boost::json::object const& request)
return false;
}
std::variant<ripple::uint256, Status>
getNFTID(boost::json::object const& request)
{
if (!request.contains(JS(nft_id)))
return Status{Error::rpcINVALID_PARAMS, "missingTokenID"};
if (!request.at(JS(nft_id)).is_string())
return Status{Error::rpcINVALID_PARAMS, "tokenIDNotString"};
ripple::uint256 tokenid;
if (!tokenid.parseHex(request.at(JS(nft_id)).as_string().c_str()))
return Status{Error::rpcINVALID_PARAMS, "malformedTokenID"};
return tokenid;
}
// TODO - this function is long and shouldn't be responsible for as much as it
// is. Split it out into some helper functions.
std::variant<Status, boost::json::object>
traverseTransactions(
Context const& context,
std::function<Backend::TransactionsAndCursor(
std::shared_ptr<Backend::BackendInterface const> const& backend,
std::uint32_t const,
bool const,
std::optional<Backend::TransactionsCursor> const&,
boost::asio::yield_context& yield)> transactionFetcher)
{
auto request = context.params;
boost::json::object response = {};
bool const binary = getBool(request, JS(binary), false);
bool const forward = getBool(request, JS(forward), false);
std::optional<Backend::TransactionsCursor> cursor;
if (request.contains(JS(marker)))
{
if (!request.at(JS(marker)).is_object())
return Status{Error::rpcINVALID_PARAMS, "invalidMarker"};
auto const& obj = request.at(JS(marker)).as_object();
std::optional<std::uint32_t> transactionIndex = {};
if (obj.contains(JS(seq)))
{
if (!obj.at(JS(seq)).is_int64())
return Status{
Error::rpcINVALID_PARAMS, "transactionIndexNotInt"};
transactionIndex =
boost::json::value_to<std::uint32_t>(obj.at(JS(seq)));
}
std::optional<std::uint32_t> ledgerIndex = {};
if (obj.contains(JS(ledger)))
{
if (!obj.at(JS(ledger)).is_int64())
return Status{Error::rpcINVALID_PARAMS, "ledgerIndexNotInt"};
ledgerIndex =
boost::json::value_to<std::uint32_t>(obj.at(JS(ledger)));
}
if (!transactionIndex || !ledgerIndex)
return Status{Error::rpcINVALID_PARAMS, "missingLedgerOrSeq"};
cursor = {*ledgerIndex, *transactionIndex};
}
auto minIndex = context.range.minSequence;
if (request.contains(JS(ledger_index_min)))
{
auto& min = request.at(JS(ledger_index_min));
if (!min.is_int64())
return Status{Error::rpcINVALID_PARAMS, "ledgerSeqMinNotNumber"};
if (min.as_int64() != -1)
{
if (context.range.maxSequence < min.as_int64() ||
context.range.minSequence > min.as_int64())
return Status{
Error::rpcINVALID_PARAMS, "ledgerSeqMinOutOfRange"};
else
minIndex = boost::json::value_to<std::uint32_t>(min);
}
if (forward && !cursor)
cursor = {minIndex, 0};
}
auto maxIndex = context.range.maxSequence;
if (request.contains(JS(ledger_index_max)))
{
auto& max = request.at(JS(ledger_index_max));
if (!max.is_int64())
return Status{Error::rpcINVALID_PARAMS, "ledgerSeqMaxNotNumber"};
if (max.as_int64() != -1)
{
if (context.range.maxSequence < max.as_int64() ||
context.range.minSequence > max.as_int64())
return Status{
Error::rpcINVALID_PARAMS, "ledgerSeqMaxOutOfRange"};
else
maxIndex = boost::json::value_to<std::uint32_t>(max);
}
if (minIndex > maxIndex)
return Status{Error::rpcINVALID_PARAMS, "invalidIndex"};
if (!forward && !cursor)
cursor = {maxIndex, INT32_MAX};
}
if (request.contains(JS(ledger_index)) || request.contains(JS(ledger_hash)))
{
if (request.contains(JS(ledger_index_max)) ||
request.contains(JS(ledger_index_min)))
return Status{
Error::rpcINVALID_PARAMS, "containsLedgerSpecifierAndRange"};
auto v = ledgerInfoFromRequest(context);
if (auto status = std::get_if<Status>(&v); status)
return *status;
maxIndex = minIndex = std::get<ripple::LedgerInfo>(v).seq;
}
if (!cursor)
{
if (forward)
cursor = {minIndex, 0};
else
cursor = {maxIndex, INT32_MAX};
}
std::uint32_t limit;
if (auto const status = getLimit(context, limit); status)
return status;
if (request.contains(JS(limit)))
response[JS(limit)] = limit;
boost::json::array txns;
auto [blobs, retCursor] = transactionFetcher(
context.backend, limit, forward, cursor, context.yield);
auto serializationStart = std::chrono::system_clock::now();
if (retCursor)
{
boost::json::object cursorJson;
cursorJson[JS(ledger)] = retCursor->ledgerSequence;
cursorJson[JS(seq)] = retCursor->transactionIndex;
response[JS(marker)] = cursorJson;
}
for (auto const& txnPlusMeta : blobs)
{
if (txnPlusMeta.ledgerSequence < minIndex ||
txnPlusMeta.ledgerSequence > maxIndex)
{
BOOST_LOG_TRIVIAL(debug)
<< __func__
<< " skipping over transactions from incomplete ledger";
continue;
}
boost::json::object obj;
if (!binary)
{
auto [txn, meta] = toExpandedJson(txnPlusMeta);
obj[JS(meta)] = meta;
obj[JS(tx)] = txn;
obj[JS(tx)].as_object()[JS(ledger_index)] =
txnPlusMeta.ledgerSequence;
obj[JS(tx)].as_object()[JS(date)] = txnPlusMeta.date;
}
else
{
obj[JS(meta)] = ripple::strHex(txnPlusMeta.metadata);
obj[JS(tx_blob)] = ripple::strHex(txnPlusMeta.transaction);
obj[JS(ledger_index)] = txnPlusMeta.ledgerSequence;
obj[JS(date)] = txnPlusMeta.date;
}
obj[JS(validated)] = true;
txns.push_back(obj);
}
response[JS(ledger_index_min)] = minIndex;
response[JS(ledger_index_max)] = maxIndex;
response[JS(transactions)] = txns;
BOOST_LOG_TRIVIAL(info)
<< __func__ << " serialization took "
<< std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now() - serializationStart)
.count()
<< " milliseconds";
return response;
}
} // namespace RPC

View File

@@ -24,6 +24,7 @@
namespace RPC {
std::optional<ripple::AccountID>
accountFromStringStrict(std::string const& account);
std::optional<ripple::AccountID>
accountFromSeed(std::string const& account);
@@ -254,5 +255,20 @@ getChannelId(boost::json::object const& request, ripple::uint256& channelId);
bool
specifiesCurrentOrClosedLedger(boost::json::object const& request);
std::variant<ripple::uint256, Status>
getNFTID(boost::json::object const& request);
// This function is the driver for both `account_tx` and `nft_tx` and should
// be used for any future transaction enumeration APIs.
std::variant<Status, boost::json::object>
traverseTransactions(
Context const& context,
std::function<Backend::TransactionsAndCursor(
std::shared_ptr<Backend::BackendInterface const> const& backend,
std::uint32_t const,
bool const,
std::optional<Backend::TransactionsCursor> const&,
boost::asio::yield_context& yield)> transactionFetcher);
} // namespace RPC
#endif

View File

@@ -1,212 +1,41 @@
#include <backend/BackendInterface.h>
#include <backend/Pg.h>
#include <rpc/RPCHelpers.h>
namespace RPC {
using boost::json::value_to;
Result
doAccountTx(Context const& context)
{
auto request = context.params;
boost::json::object response = {};
ripple::AccountID accountID;
if (auto const status = getAccount(request, accountID); status)
if (auto const status = getAccount(context.params, accountID); status)
return status;
bool const binary = getBool(request, JS(binary), false);
bool const forward = getBool(request, JS(forward), false);
constexpr std::string_view outerFuncName = __func__;
auto const maybeResponse = traverseTransactions(
context,
[&accountID, &outerFuncName](
std::shared_ptr<Backend::BackendInterface const> const& backend,
std::uint32_t const limit,
bool const forward,
std::optional<Backend::TransactionsCursor> const& cursorIn,
boost::asio::yield_context& yield) {
auto const start = std::chrono::system_clock::now();
auto const txnsAndCursor = backend->fetchAccountTransactions(
accountID, limit, forward, cursorIn, yield);
BOOST_LOG_TRIVIAL(info)
<< outerFuncName << " db fetch took "
<< std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now() - start)
.count()
<< " milliseconds - num blobs = " << txnsAndCursor.txns.size();
return txnsAndCursor;
});
std::optional<Backend::TransactionsCursor> cursor;
if (request.contains(JS(marker)))
{
auto const& obj = request.at(JS(marker)).as_object();
std::optional<std::uint32_t> transactionIndex = {};
if (obj.contains(JS(seq)))
{
if (!obj.at(JS(seq)).is_int64())
return Status{
Error::rpcINVALID_PARAMS, "transactionIndexNotInt"};
transactionIndex =
boost::json::value_to<std::uint32_t>(obj.at(JS(seq)));
}
std::optional<std::uint32_t> ledgerIndex = {};
if (obj.contains(JS(ledger)))
{
if (!obj.at(JS(ledger)).is_int64())
return Status{Error::rpcINVALID_PARAMS, "ledgerIndexNotInt"};
ledgerIndex =
boost::json::value_to<std::uint32_t>(obj.at(JS(ledger)));
}
if (!transactionIndex || !ledgerIndex)
return Status{Error::rpcINVALID_PARAMS, "missingLedgerOrSeq"};
cursor = {*ledgerIndex, *transactionIndex};
}
auto minIndex = context.range.minSequence;
if (request.contains(JS(ledger_index_min)))
{
auto& min = request.at(JS(ledger_index_min));
if (!min.is_int64())
return Status{Error::rpcINVALID_PARAMS, "ledgerSeqMinNotNumber"};
if (min.as_int64() != -1)
{
if (context.range.maxSequence < min.as_int64() ||
context.range.minSequence > min.as_int64())
return Status{
Error::rpcINVALID_PARAMS, "ledgerSeqMinOutOfRange"};
else
minIndex = value_to<std::uint32_t>(min);
}
if (forward && !cursor)
cursor = {minIndex, 0};
}
auto maxIndex = context.range.maxSequence;
if (request.contains(JS(ledger_index_max)))
{
auto& max = request.at(JS(ledger_index_max));
if (!max.is_int64())
return Status{Error::rpcINVALID_PARAMS, "ledgerSeqMaxNotNumber"};
if (max.as_int64() != -1)
{
if (context.range.maxSequence < max.as_int64() ||
context.range.minSequence > max.as_int64())
return Status{
Error::rpcINVALID_PARAMS, "ledgerSeqMaxOutOfRange"};
else
maxIndex = value_to<std::uint32_t>(max);
}
if (minIndex > maxIndex)
return Status{Error::rpcINVALID_PARAMS, "invalidIndex"};
if (!forward && !cursor)
cursor = {maxIndex, INT32_MAX};
}
if (request.contains(JS(ledger_index)) || request.contains(JS(ledger_hash)))
{
if (request.contains(JS(ledger_index_max)) ||
request.contains(JS(ledger_index_min)))
return Status{
Error::rpcINVALID_PARAMS, "containsLedgerSpecifierAndRange"};
auto v = ledgerInfoFromRequest(context);
if (auto status = std::get_if<Status>(&v))
return *status;
maxIndex = minIndex = std::get<ripple::LedgerInfo>(v).seq;
}
if (!cursor)
{
if (forward)
cursor = {minIndex, 0};
else
cursor = {maxIndex, INT32_MAX};
}
std::uint32_t limit;
if (auto const status = getLimit(context, limit); status)
return status;
if (request.contains(JS(limit)))
response[JS(limit)] = limit;
boost::json::array txns;
auto start = std::chrono::system_clock::now();
auto [blobs, retCursor] = context.backend->fetchAccountTransactions(
accountID, limit, forward, cursor, context.yield);
auto end = std::chrono::system_clock::now();
BOOST_LOG_TRIVIAL(info) << __func__ << " db fetch took "
<< ((end - start).count() / 1000000000.0)
<< " num blobs = " << blobs.size();
if (auto const status = std::get_if<Status>(&maybeResponse); status)
return *status;
auto response = std::get<boost::json::object>(maybeResponse);
response[JS(account)] = ripple::to_string(accountID);
if (retCursor)
{
boost::json::object cursorJson;
cursorJson[JS(ledger)] = retCursor->ledgerSequence;
cursorJson[JS(seq)] = retCursor->transactionIndex;
response[JS(marker)] = cursorJson;
}
std::optional<size_t> maxReturnedIndex;
std::optional<size_t> minReturnedIndex;
for (auto const& txnPlusMeta : blobs)
{
if (txnPlusMeta.ledgerSequence < minIndex ||
txnPlusMeta.ledgerSequence > maxIndex)
{
BOOST_LOG_TRIVIAL(debug)
<< __func__
<< " skipping over transactions from incomplete ledger";
continue;
}
boost::json::object obj;
if (!binary)
{
auto [txn, meta] = toExpandedJson(txnPlusMeta);
obj[JS(meta)] = meta;
obj[JS(tx)] = txn;
obj[JS(tx)].as_object()[JS(ledger_index)] =
txnPlusMeta.ledgerSequence;
obj[JS(tx)].as_object()[JS(date)] = txnPlusMeta.date;
}
else
{
obj[JS(meta)] = ripple::strHex(txnPlusMeta.metadata);
obj[JS(tx_blob)] = ripple::strHex(txnPlusMeta.transaction);
obj[JS(ledger_index)] = txnPlusMeta.ledgerSequence;
obj[JS(date)] = txnPlusMeta.date;
}
obj[JS(validated)] = true;
txns.push_back(obj);
if (!minReturnedIndex || txnPlusMeta.ledgerSequence < *minReturnedIndex)
minReturnedIndex = txnPlusMeta.ledgerSequence;
if (!maxReturnedIndex || txnPlusMeta.ledgerSequence > *maxReturnedIndex)
maxReturnedIndex = txnPlusMeta.ledgerSequence;
}
assert(cursor);
if (!forward)
{
response[JS(ledger_index_min)] = cursor->ledgerSequence;
response[JS(ledger_index_max)] = maxIndex;
}
else
{
response[JS(ledger_index_max)] = cursor->ledgerSequence;
response[JS(ledger_index_min)] = minIndex;
}
response[JS(transactions)] = txns;
auto end2 = std::chrono::system_clock::now();
BOOST_LOG_TRIVIAL(info) << __func__ << " serialization took "
<< ((end2 - end).count() / 1000000000.0);
return response;
} // namespace RPC
}
} // namespace RPC

View File

@@ -0,0 +1,43 @@
#include <rpc/RPCHelpers.h>
namespace RPC {
Result
doNFTHistory(Context const& context)
{
auto const maybeTokenID = getNFTID(context.params);
if (auto const status = std::get_if<Status>(&maybeTokenID); status)
return *status;
auto const tokenID = std::get<ripple::uint256>(maybeTokenID);
constexpr std::string_view outerFuncName = __func__;
auto const maybeResponse = traverseTransactions(
context,
[&tokenID, &outerFuncName](
std::shared_ptr<Backend::BackendInterface const> const& backend,
std::uint32_t const limit,
bool const forward,
std::optional<Backend::TransactionsCursor> const& cursorIn,
boost::asio::yield_context& yield)
-> Backend::TransactionsAndCursor {
auto const start = std::chrono::system_clock::now();
auto const txnsAndCursor = backend->fetchNFTTransactions(
tokenID, limit, forward, cursorIn, yield);
BOOST_LOG_TRIVIAL(info)
<< outerFuncName << " db fetch took "
<< std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now() - start)
.count()
<< " milliseconds - num blobs = " << txnsAndCursor.txns.size();
return txnsAndCursor;
});
if (auto const status = std::get_if<Status>(&maybeResponse); status)
return *status;
auto response = std::get<boost::json::object>(maybeResponse);
response[JS(nft_id)] = ripple::to_string(tokenID);
return response;
}
} // namespace RPC

View File

@@ -89,24 +89,15 @@ doNFTInfo(Context const& context)
auto request = context.params;
boost::json::object response = {};
if (!request.contains("nft_id"))
return Status{Error::rpcINVALID_PARAMS, "Missing nft_id"};
auto const& jsonTokenID = request.at("nft_id");
if (!jsonTokenID.is_string())
return Status{Error::rpcINVALID_PARAMS, "nft_id is not a string"};
ripple::uint256 tokenID;
if (!tokenID.parseHex(jsonTokenID.as_string().c_str()))
return Status{Error::rpcINVALID_PARAMS, "Malformed nft_id"};
// We only need to fetch the ledger header because the ledger hash is
// supposed to be included in the response. The ledger sequence is specified
// in the request
auto v = ledgerInfoFromRequest(context);
if (auto status = std::get_if<Status>(&v))
auto const maybeTokenID = getNFTID(request);
if (auto const status = std::get_if<Status>(&maybeTokenID); status)
return *status;
ripple::LedgerInfo lgrInfo = std::get<ripple::LedgerInfo>(v);
auto const tokenID = std::get<ripple::uint256>(maybeTokenID);
auto const maybeLedgerInfo = ledgerInfoFromRequest(context);
if (auto status = std::get_if<Status>(&maybeLedgerInfo); status)
return *status;
auto const lgrInfo = std::get<ripple::LedgerInfo>(maybeLedgerInfo);
std::optional<Backend::NFT> dbResponse =
context.backend->fetchNFT(tokenID, lgrInfo.seq, context.yield);
@@ -130,10 +121,10 @@ doNFTInfo(Context const& context)
{
auto const maybeURI = getURI(*dbResponse, context);
// An error occurred
if (Status const* status = std::get_if<Status>(&maybeURI))
if (Status const* status = std::get_if<Status>(&maybeURI); status)
return *status;
// A URI was found
if (std::string const* uri = std::get_if<std::string>(&maybeURI))
if (std::string const* uri = std::get_if<std::string>(&maybeURI); uri)
response["uri"] = *uri;
// A URI was not found, explicitly set to null
else

View File

@@ -129,26 +129,10 @@ enumerateNFTOffers(
return response;
}
std::variant<ripple::uint256, Status>
getTokenid(boost::json::object const& request)
{
if (!request.contains(JS(nft_id)))
return Status{Error::rpcINVALID_PARAMS, "missingTokenid"};
if (!request.at(JS(nft_id)).is_string())
return Status{Error::rpcINVALID_PARAMS, "tokenidNotString"};
ripple::uint256 tokenid;
if (!tokenid.parseHex(request.at(JS(nft_id)).as_string().c_str()))
return Status{Error::rpcINVALID_PARAMS, "malformedCursor"};
return tokenid;
}
Result
doNFTOffers(Context const& context, bool sells)
{
auto const v = getTokenid(context.params);
auto const v = getNFTID(context.params);
if (auto const status = std::get_if<Status>(&v))
return *status;