mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-21 20:25:52 +00:00
Add NFT-specific data stores and add nft_info API (#98)
This commit is contained in:
146
src/rpc/handlers/NFTInfo.cpp
Normal file
146
src/rpc/handlers/NFTInfo.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include <ripple/app/tx/impl/details/NFTokenUtils.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include <backend/BackendInterface.h>
|
||||
#include <rpc/RPCHelpers.h>
|
||||
|
||||
// {
|
||||
// nft_id: <ident>
|
||||
// ledger_hash: <ledger>
|
||||
// ledger_index: <ledger_index>
|
||||
// }
|
||||
|
||||
namespace RPC {
|
||||
|
||||
std::variant<std::monostate, std::string, Status>
|
||||
getURI(Backend::NFT const& dbResponse, Context const& context)
|
||||
{
|
||||
// Fetch URI from ledger
|
||||
// The correct page will be > bookmark and <= last. We need to calculate
|
||||
// the first possible page however, since bookmark is not guaranteed to
|
||||
// exist.
|
||||
auto const bookmark = ripple::keylet::nftpage(
|
||||
ripple::keylet::nftpage_min(dbResponse.owner), dbResponse.tokenID);
|
||||
auto const last = ripple::keylet::nftpage_max(dbResponse.owner);
|
||||
|
||||
ripple::uint256 nextKey = last.key;
|
||||
std::optional<ripple::STLedgerEntry> sle;
|
||||
|
||||
// when this loop terminates, `sle` will contain the correct page for
|
||||
// this NFT.
|
||||
//
|
||||
// 1) We start at the last NFTokenPage, which is guaranteed to exist,
|
||||
// grab the object from the DB and deserialize it.
|
||||
//
|
||||
// 2) If that NFTokenPage has a PreviousPageMin value and the
|
||||
// PreviousPageMin value is > bookmark, restart loop. Otherwise
|
||||
// terminate and use the `sle` from this iteration.
|
||||
do
|
||||
{
|
||||
auto const blob = context.backend->fetchLedgerObject(
|
||||
ripple::Keylet(ripple::ltNFTOKEN_PAGE, nextKey).key,
|
||||
dbResponse.ledgerSequence,
|
||||
context.yield);
|
||||
|
||||
if (!blob || blob->size() == 0)
|
||||
return Status{
|
||||
Error::rpcINTERNAL, "Cannot find NFTokenPage for this NFT"};
|
||||
|
||||
sle = ripple::STLedgerEntry(
|
||||
ripple::SerialIter{blob->data(), blob->size()}, nextKey);
|
||||
|
||||
if (sle->isFieldPresent(ripple::sfPreviousPageMin))
|
||||
nextKey = sle->getFieldH256(ripple::sfPreviousPageMin);
|
||||
|
||||
} while (sle && sle->key() != nextKey && nextKey > bookmark.key);
|
||||
|
||||
if (!sle)
|
||||
return Status{
|
||||
Error::rpcINTERNAL, "Cannot find NFTokenPage for this NFT"};
|
||||
|
||||
auto const nfts = sle->getFieldArray(ripple::sfNFTokens);
|
||||
auto const nft = std::find_if(
|
||||
nfts.begin(),
|
||||
nfts.end(),
|
||||
[&dbResponse](ripple::STObject const& candidate) {
|
||||
return candidate.getFieldH256(ripple::sfNFTokenID) ==
|
||||
dbResponse.tokenID;
|
||||
});
|
||||
|
||||
if (nft == nfts.end())
|
||||
return Status{
|
||||
Error::rpcINTERNAL, "Cannot find NFTokenPage for this NFT"};
|
||||
|
||||
ripple::Blob const uriField = nft->getFieldVL(ripple::sfURI);
|
||||
|
||||
// NOTE this cannot use a ternary or value_or because then the
|
||||
// expression's type is unclear. We want to explicitly set the `uri`
|
||||
// field to null when not present to avoid any confusion.
|
||||
if (std::string const uri = std::string(uriField.begin(), uriField.end());
|
||||
uri.size() > 0)
|
||||
return uri;
|
||||
return std::monostate{};
|
||||
}
|
||||
|
||||
Result
|
||||
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))
|
||||
return *status;
|
||||
ripple::LedgerInfo lgrInfo = std::get<ripple::LedgerInfo>(v);
|
||||
|
||||
std::optional<Backend::NFT> dbResponse =
|
||||
context.backend->fetchNFT(tokenID, lgrInfo.seq, context.yield);
|
||||
if (!dbResponse)
|
||||
return Status{Error::rpcOBJECT_NOT_FOUND, "NFT not found"};
|
||||
|
||||
response["nft_id"] = ripple::strHex(dbResponse->tokenID);
|
||||
response["ledger_index"] = dbResponse->ledgerSequence;
|
||||
response["owner"] = ripple::toBase58(dbResponse->owner);
|
||||
response["is_burned"] = dbResponse->isBurned;
|
||||
|
||||
response["flags"] = ripple::nft::getFlags(dbResponse->tokenID);
|
||||
response["transfer_fee"] = ripple::nft::getTransferFee(dbResponse->tokenID);
|
||||
response["issuer"] =
|
||||
ripple::toBase58(ripple::nft::getIssuer(dbResponse->tokenID));
|
||||
response["nft_taxon"] =
|
||||
ripple::nft::toUInt32(ripple::nft::getTaxon(dbResponse->tokenID));
|
||||
response["nft_sequence"] = ripple::nft::getSerial(dbResponse->tokenID);
|
||||
|
||||
if (!dbResponse->isBurned)
|
||||
{
|
||||
auto const maybeURI = getURI(*dbResponse, context);
|
||||
// An error occurred
|
||||
if (Status const* status = std::get_if<Status>(&maybeURI))
|
||||
return *status;
|
||||
// A URI was found
|
||||
if (std::string const* uri = std::get_if<std::string>(&maybeURI))
|
||||
response["uri"] = *uri;
|
||||
// A URI was not found, explicitly set to null
|
||||
else
|
||||
response["uri"] = nullptr;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
Reference in New Issue
Block a user