Write NFT URIs to nf_token_uris table and pull from it for nft_info API (#313)

Fixes #308
This commit is contained in:
ledhed2222
2023-03-20 13:43:31 -04:00
committed by GitHub
parent 9d10cff873
commit b25ac5d707
12 changed files with 338 additions and 147 deletions

View File

@@ -17,12 +17,13 @@
*/
//==============================================================================
#include <ripple/app/tx/impl/details/NFTokenUtils.h>
#include <backend/CassandraBackend.h>
#include <backend/DBHelpers.h>
#include <log/Logger.h>
#include <util/Profiler.h>
#include <ripple/app/tx/impl/details/NFTokenUtils.h>
#include <functional>
#include <unordered_map>
@@ -405,6 +406,13 @@ CassandraBackend::writeNFTs(std::vector<NFTsData>&& data)
},
"nf_tokens");
// If `uri` is set (and it can be set to an empty uri), we know this
// is a net-new NFT. That is, this NFT has not been seen before by us
// _OR_ it is in the extreme edge case of a re-minted NFT ID with the
// same NFT ID as an already-burned token. In this case, we need to
// record the URI and link to the issuer_nf_tokens table.
if (record.uri)
{
makeAndExecuteAsyncWrite(
this,
std::make_tuple(record.tokenID),
@@ -412,10 +420,27 @@ CassandraBackend::writeNFTs(std::vector<NFTsData>&& data)
CassandraStatement statement{insertIssuerNFT_};
auto const& [tokenID] = params.data;
statement.bindNextBytes(ripple::nft::getIssuer(tokenID));
statement.bindNextInt(
ripple::nft::toUInt32(ripple::nft::getTaxon(tokenID)));
statement.bindNextBytes(tokenID);
return statement;
},
"issuer_nf_tokens");
makeAndExecuteAsyncWrite(
this,
std::make_tuple(
record.tokenID, record.ledgerSequence, record.uri.value()),
[this](auto const& params) {
CassandraStatement statement{insertNFTURI_};
auto const& [tokenID, lgrSeq, uri] = params.data;
statement.bindNextBytes(tokenID);
statement.bindNextInt(lgrSeq);
statement.bindNextBytes(uri);
return statement;
},
"nf_token_uris");
}
}
}
@@ -598,18 +623,35 @@ CassandraBackend::fetchNFT(
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const
{
CassandraStatement statement{selectNFT_};
statement.bindNextBytes(tokenID);
statement.bindNextInt(ledgerSequence);
CassandraResult response = executeAsyncRead(statement, yield);
if (!response)
CassandraStatement nftStatement{selectNFT_};
nftStatement.bindNextBytes(tokenID);
nftStatement.bindNextInt(ledgerSequence);
CassandraResult nftResponse = executeAsyncRead(nftStatement, yield);
if (!nftResponse)
return {};
NFT result;
result.tokenID = tokenID;
result.ledgerSequence = response.getUInt32();
result.owner = response.getBytes();
result.isBurned = response.getBool();
result.ledgerSequence = nftResponse.getUInt32();
result.owner = nftResponse.getBytes();
result.isBurned = nftResponse.getBool();
// now fetch URI. Usually we will have the URI even for burned NFTs, but
// if the first ledger on this clio included NFTokenBurn transactions
// we will not have the URIs for any of those tokens. In any other case
// not having the URI indicates something went wrong with our data.
//
// TODO - in the future would be great for any handlers that use this
// could inject a warning in this case (the case of not having a URI
// because it was burned in the first ledger) to indicate that even though
// we are returning a blank URI, the NFT might have had one.
CassandraStatement uriStatement{selectNFTURI_};
uriStatement.bindNextBytes(tokenID);
uriStatement.bindNextInt(ledgerSequence);
CassandraResult uriResponse = executeAsyncRead(uriStatement, yield);
if (uriResponse.hasResult())
result.uri = uriResponse.getBytes();
return result;
}
@@ -1388,17 +1430,37 @@ CassandraBackend::open(bool readOnly)
query.str("");
query << "CREATE TABLE IF NOT EXISTS " << tablePrefix
<< "issuer_nf_tokens"
<< "issuer_nf_tokens_v2"
<< " ("
<< " issuer blob,"
<< " taxon bigint,"
<< " token_id blob,"
<< " PRIMARY KEY (issuer, token_id)"
<< " PRIMARY KEY (issuer, taxon, token_id)"
<< " )";
if (!executeSimpleStatement(query.str()))
continue;
query.str("");
query << "SELECT * FROM " << tablePrefix << "issuer_nf_tokens"
query << "SELECT * FROM " << tablePrefix << "issuer_nf_tokens_v2"
<< " LIMIT 1";
if (!executeSimpleStatement(query.str()))
continue;
query.str("");
query << "CREATE TABLE IF NOT EXISTS " << tablePrefix << "nf_token_uris"
<< " ("
<< " token_id blob,"
<< " sequence bigint,"
<< " uri blob,"
<< " PRIMARY KEY (token_id, sequence)"
<< " )"
<< " WITH CLUSTERING ORDER BY (sequence DESC)"
<< " AND default_time_to_live = " << ttl;
if (!executeSimpleStatement(query.str()))
continue;
query.str("");
query << "SELECT * FROM " << tablePrefix << "nf_token_uris"
<< " LIMIT 1";
if (!executeSimpleStatement(query.str()))
continue;
@@ -1558,12 +1620,28 @@ CassandraBackend::open(bool readOnly)
continue;
query.str("");
query << "INSERT INTO " << tablePrefix << "issuer_nf_tokens"
<< " (issuer,token_id)"
<< " VALUES (?,?)";
query << "INSERT INTO " << tablePrefix << "issuer_nf_tokens_v2"
<< " (issuer,taxon,token_id)"
<< " VALUES (?,?,?)";
if (!insertIssuerNFT_.prepareStatement(query, session_.get()))
continue;
query.str("");
query << "INSERT INTO " << tablePrefix << "nf_token_uris"
<< " (token_id,sequence,uri)"
<< " VALUES (?,?,?)";
if (!insertNFTURI_.prepareStatement(query, session_.get()))
continue;
query.str("");
query << "SELECT uri FROM " << tablePrefix << "nf_token_uris"
<< " WHERE token_id = ? AND"
<< " sequence <= ?"
<< " ORDER BY sequence DESC"
<< " LIMIT 1";
if (!selectNFTURI_.prepareStatement(query, session_.get()))
continue;
query.str("");
query << "INSERT INTO " << tablePrefix << "nf_token_transactions"
<< " (token_id,seq_idx,hash)"

View File

@@ -655,6 +655,8 @@ private:
CassandraPreparedStatement insertNFT_;
CassandraPreparedStatement selectNFT_;
CassandraPreparedStatement insertIssuerNFT_;
CassandraPreparedStatement insertNFTURI_;
CassandraPreparedStatement selectNFTURI_;
CassandraPreparedStatement insertNFTTx_;
CassandraPreparedStatement selectNFTTx_;
CassandraPreparedStatement selectNFTTxForward_;

View File

@@ -85,11 +85,41 @@ struct NFTsData
// final state of an NFT per ledger. Since we pull this from transactions
// we keep track of which tx index created this so we can de-duplicate, as
// it is possible for one ledger to have multiple txs that change the
// state of the same NFT.
std::uint32_t transactionIndex;
// state of the same NFT. This field is not applicable when we are loading
// initial NFT state via ledger objects, since we do not have to tiebreak
// NFT state for a given ledger in that case.
std::optional<std::uint32_t> transactionIndex;
ripple::AccountID owner;
bool isBurned;
// We only set the uri if this is a mint tx, or if we are
// loading initial state from NFTokenPage objects. In other words,
// uri should only be set if the etl process believes this NFT hasn't
// been seen before in our local database. We do this so that we don't
// write to the the nf_token_uris table every
// time the same NFT changes hands. We also can infer if there is a URI
// that we need to write to the issuer_nf_tokens table.
std::optional<ripple::Blob> uri;
bool isBurned = false;
// This constructor is used when parsing an NFTokenMint tx.
// Unfortunately because of the extreme edge case of being able to
// re-mint an NFT with the same ID, we must explicitly record a null
// URI. For this reason, we _always_ write this field as a result of
// this tx.
NFTsData(
ripple::uint256 const& tokenID,
ripple::AccountID const& owner,
ripple::Blob const& uri,
ripple::TxMeta const& meta)
: tokenID(tokenID)
, ledgerSequence(meta.getLgrSeq())
, transactionIndex(meta.getIndex())
, owner(owner)
, uri(uri)
{
}
// This constructor is used when parsing an NFTokenBurn or
// NFTokenAcceptOffer tx
NFTsData(
ripple::uint256 const& tokenID,
ripple::AccountID const& owner,
@@ -102,6 +132,24 @@ struct NFTsData
, isBurned(isBurned)
{
}
// This constructor is used when parsing an NFTokenPage directly from
// ledger state.
// Unfortunately because of the extreme edge case of being able to
// re-mint an NFT with the same ID, we must explicitly record a null
// URI. For this reason, we _always_ write this field as a result of
// this tx.
NFTsData(
ripple::uint256 const& tokenID,
std::uint32_t const ledgerSequence,
ripple::AccountID const& owner,
ripple::Blob const& uri)
: tokenID(tokenID)
, ledgerSequence(ledgerSequence)
, owner(owner)
, uri(uri)
{
}
};
template <class T>

View File

@@ -130,3 +130,91 @@ In each new ledger version with sequence `n`, a ledger object `v` can either be
1. Being **created**, add two new records of `seq=n` with one being `e` pointing to `v`, and `v` pointing to `w` (Linked List insertion operation).
2. Being **modified**, do nothing.
3. Being **deleted**, add a record of `seq=n` with `e` pointing to `v`'s `next` value (Linked List deletion operation).
### NFT data model
In `rippled` NFTs are stored in NFTokenPage ledger objects. This object is
implemented to save ledger space and has the property that it gives us O(1)
lookup time for an NFT, assuming we know who owns the NFT at a particular
ledger. However, if we do not know who owns the NFT at a specific ledger
height we have no alternative in rippled other than scanning the entire
ledger. Because of this tradeoff, clio implements a special NFT indexing data
structure that allows clio users to query NFTs quickly, while keeping
rippled's space-saving optimizations.
#### `nf_tokens`
```
CREATE TABLE clio.nf_tokens (
token_id blob, # The NFT's ID
sequence bigint, # Sequence of ledger version
owner blob, # The account ID of the owner of this NFT at this ledger
is_burned boolean, # True if token was burned in this ledger
PRIMARY KEY (token_id, sequence)
) WITH CLUSTERING ORDER BY (sequence DESC) ...
```
This table indexes NFT IDs with their owner at a given ledger. So
```
SELECT * FROM nf_tokens
WHERE token_id = N AND seq <= Y
ORDER BY seq DESC LIMIT 1;
```
will give you the owner of token N at ledger Y and whether it was burned. If
the token is burned, the owner field indicates the account that owned the
token at the time it was burned; it does not indicate the person who burned
the token, necessarily. If you need to determine who burned the token you can
use the `nft_history` API, which will give you the NFTokenBurn transaction
that burned this token, along with the account that submitted that
transaction.
#### `issuer_nf_tokens_v2`
```
CREATE TABLE clio.issuer_nf_tokens_v2 (
issuer blob, # The NFT issuer's account ID
taxon bigint, # The NFT's token taxon
token_id blob, # The NFT's ID
PRIMARY KEY (issuer, taxon, token_id)
)
```
This table indexes token IDs against their issuer and issuer/taxon
combination. This is useful for determining all the NFTs a specific account
issued, or all the NFTs a specific account issued with a specific taxon. It is
not useful to know all the NFTs with a given taxon while excluding issuer, since the
meaning of a taxon is left to an issuer.
#### `nf_token_uris`
```
CREATE TABLE clio.nf_token_uris (
token_id blob, # The NFT's ID
sequence bigint, # Sequence of ledger version
uri blob, # The NFT's URI
PRIMARY KEY (token_id, sequence)
) WITH CLUSTERING ORDER BY (sequence DESC) ...
```
This table is used to store an NFT's URI. Without storing this here, we would
need to traverse the NFT owner's entire set of NFTs to find the URI, again due
to the way that NFTs are stored in rippled. Furthermore, instead of storing
this in the `nf_tokens` table, we store it here to save space. A given NFT
will have only one entry in this table (see caveat below), written to this
table as soon as clio sees the NFTokenMint transaction, or when clio loads an
NFTokenPage from the initial ledger it downloaded. However, the `nf_tokens`
table is written to every time an NFT changes ownership, or if it is burned.
Given this, why do we have to store the sequence? Unfortunately there is an
extreme edge case where a given NFT ID can be burned, and then re-minted with
a different URI. This is extremely unlikely, and might be fixed in a future
version to rippled, but just in case we can handle that edge case by allowing
a given NFT ID to have a new URI assigned in this case, without removing the
prior URI.
#### `nf_token_transactions`
```
CREATE TABLE clio.nf_token_transactions (
token_id blob, # The NFT's ID
seq_idx tuple<bigint, bigint>, # Tuple of (ledger_index, transaction_index)
hash blob, # Hash of the transaction
PRIMARY KEY (token_id, seq_idx)
) WITH CLUSTERING ORDER BY (seq_idx DESC) ...
```
This table is the NFT equivalent of `account_tx`. It's motivated by the exact
same reasons and serves the analogous purpose here. It drives the
`nft_history` API.

View File

@@ -83,6 +83,7 @@ struct NFT
ripple::uint256 tokenID;
std::uint32_t ledgerSequence;
ripple::AccountID owner;
Blob uri;
bool isBurned;
// clearly two tokens are the same if they have the same ID, but this

View File

@@ -27,6 +27,7 @@
#include <backend/DBHelpers.h>
#include <etl/ETLSource.h>
#include <etl/NFTHelpers.h>
#include <etl/ProbingETLSource.h>
#include <etl/ReportingETL.h>
#include <log/Logger.h>
@@ -680,6 +681,8 @@ public:
request_.ledger().sequence(),
std::string{obj.key()});
lastKey_ = obj.key();
backend.writeNFTs(getNFTDataFromObj(
request_.ledger().sequence(), obj.key(), obj.data()));
backend.writeLedgerObject(
std::move(*obj.mutable_key()),
request_.ledger().sequence(),

View File

@@ -17,7 +17,6 @@
*/
//==============================================================================
#include <ripple/app/tx/impl/details/NFTokenUtils.h>
#include <ripple/protocol/STBase.h>
#include <ripple/protocol/STTx.h>
#include <ripple/protocol/TxMeta.h>
@@ -123,7 +122,11 @@ getNFTokenMintData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
return {
{NFTTransactionsData(
tokenIDResult.front(), txMeta, sttx.getTransactionID())},
NFTsData(tokenIDResult.front(), *owner, txMeta, false)};
NFTsData(
tokenIDResult.front(),
*owner,
sttx.getFieldVL(ripple::sfURI),
txMeta)};
std::stringstream msg;
msg << " - unexpected NFTokenMint data in tx " << sttx.getTransactionID();
@@ -150,11 +153,11 @@ getNFTokenBurnData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
// NFT burn can result in an NFTokenPage being modified to no longer
// include the target, or an NFTokenPage being deleted. If this is
// modified, we want to look for the target in the fields prior to
// modification. If deleted, it's possible that the page was modified
// to remove the target NFT prior to the entire page being deleted. In
// this case, we need to look in the PreviousFields. Otherwise, the
// page was not modified prior to deleting and we need to look in the
// FinalFields.
// modification. If deleted, it's possible that the page was
// modified to remove the target NFT prior to the entire page being
// deleted. In this case, we need to look in the PreviousFields.
// Otherwise, the page was not modified prior to deleting and we
// need to look in the FinalFields.
std::optional<ripple::STArray> prevNFTs;
if (node.isFieldPresent(ripple::sfPreviousFields))
@@ -360,7 +363,7 @@ getNFTokenCreateOfferData(
}
std::pair<std::vector<NFTTransactionsData>, std::optional<NFTsData>>
getNFTData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
getNFTDataFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
{
if (txMeta.getResultTER() != ripple::tesSUCCESS)
return {{}, {}};
@@ -386,3 +389,28 @@ getNFTData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
return {{}, {}};
}
}
std::vector<NFTsData>
getNFTDataFromObj(
std::uint32_t const seq,
std::string const& key,
std::string const& blob)
{
std::vector<NFTsData> nfts;
ripple::STLedgerEntry const sle = ripple::STLedgerEntry(
ripple::SerialIter{blob.data(), blob.size()},
ripple::uint256::fromVoid(key.data()));
if (sle.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltNFTOKEN_PAGE)
return nfts;
auto const owner = ripple::AccountID::fromVoid(key.data());
for (ripple::STObject const& node : sle.getFieldArray(ripple::sfNFTokens))
nfts.emplace_back(
node.getFieldH256(ripple::sfNFTokenID),
seq,
owner,
node.getFieldVL(ripple::sfURI));
return nfts;
}

36
src/etl/NFTHelpers.h Normal file
View File

@@ -0,0 +1,36 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <backend/DBHelpers.h>
#include <ripple/protocol/STTx.h>
#include <ripple/protocol/TxMeta.h>
// Pulling from tx via ReportingETL
std::pair<std::vector<NFTTransactionsData>, std::optional<NFTsData>>
getNFTDataFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx);
// Pulling from ledger object via loadInitialLedger
std::vector<NFTsData>
getNFTDataFromObj(
std::uint32_t const seq,
std::string const& key,
std::string const& blob);

View File

@@ -21,6 +21,8 @@
#include <ripple/beast/core/CurrentThreadName.h>
#include <backend/DBHelpers.h>
#include <etl/NFTHelpers.h>
#include <etl/ReportingETL.h>
#include <log/Logger.h>
#include <subscriptions/SubscriptionManager.h>
@@ -73,7 +75,7 @@ ReportingETL::insertTransactions(
ripple::TxMeta txMeta{
sttx.getTransactionID(), ledger.seq, txn.metadata_blob()};
auto const [nftTxs, maybeNFT] = getNFTData(txMeta, sttx);
auto const [nftTxs, maybeNFT] = getNFTDataFromTx(txMeta, sttx);
result.nfTokenTxData.insert(
result.nfTokenTxData.end(), nftTxs.begin(), nftTxs.end());
if (maybeNFT)

View File

@@ -38,13 +38,6 @@
#include <chrono>
/**
* Helper function for the ReportingETL, implemented in NFTHelpers.cpp, to
* pull to-write data out of a transaction that relates to NFTs.
*/
std::pair<std::vector<NFTTransactionsData>, std::optional<NFTsData>>
getNFTData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx);
struct AccountTransactionsData;
struct NFTTransactionsData;
struct NFTsData;

View File

@@ -24,89 +24,12 @@
#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{
RippledError::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{
RippledError::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{
RippledError::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;
auto const request = context.params;
boost::json::object response = {};
auto const maybeTokenID = getNFTID(request);
@@ -115,41 +38,29 @@ doNFTInfo(Context const& context)
auto const tokenID = std::get<ripple::uint256>(maybeTokenID);
auto const maybeLedgerInfo = ledgerInfoFromRequest(context);
if (auto status = std::get_if<Status>(&maybeLedgerInfo); status)
if (auto const status = std::get_if<Status>(&maybeLedgerInfo); status)
return *status;
auto const lgrInfo = std::get<ripple::LedgerInfo>(maybeLedgerInfo);
std::optional<Backend::NFT> dbResponse =
auto const dbResponse =
context.backend->fetchNFT(tokenID, lgrInfo.seq, context.yield);
if (!dbResponse)
return Status{RippledError::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[JS(nft_id)] = ripple::strHex(dbResponse->tokenID);
response[JS(ledger_index)] = dbResponse->ledgerSequence;
response[JS(owner)] = ripple::toBase58(dbResponse->owner);
response["is_burned"] = dbResponse->isBurned;
response[JS(uri)] = ripple::strHex(dbResponse->uri);
response["flags"] = ripple::nft::getFlags(dbResponse->tokenID);
response["transfer_fee"] = ripple::nft::getTransferFee(dbResponse->tokenID);
response["issuer"] =
response[JS(flags)] = ripple::nft::getFlags(dbResponse->tokenID);
response["transfer_rate"] =
ripple::nft::getTransferFee(dbResponse->tokenID);
response[JS(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); status)
return *status;
// A URI was found
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
response["uri"] = nullptr;
}
response[JS(nft_serial)] = ripple::nft::getSerial(dbResponse->tokenID);
return response;
}

View File

@@ -21,6 +21,7 @@
#include <backend/BackendInterface.h>
#include <backend/DBHelpers.h>
#include <config/Config.h>
#include <etl/NFTHelpers.h>
#include <etl/ReportingETL.h>
#include <log/Logger.h>
#include <rpc/RPCHelpers.h>
@@ -461,7 +462,7 @@ TEST_F(BackendTest, Basic)
ripple::SerialIter it{nftTxnBlob.data(), nftTxnBlob.size()};
ripple::STTx sttx{it};
auto const [parsedNFTTxsRef, parsedNFT] =
getNFTData(nftTxMeta, sttx);
getNFTDataFromTx(nftTxMeta, sttx);
// need to copy the nft txns so we can std::move later
std::vector<NFTTransactionsData> parsedNFTTxs;
parsedNFTTxs.insert(