mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-14 08:45:51 +00:00
Write NFT URIs to nf_token_uris table and pull from it for nft_info API (#313)
Fixes #308
This commit is contained in:
@@ -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)"
|
||||
|
||||
@@ -655,6 +655,8 @@ private:
|
||||
CassandraPreparedStatement insertNFT_;
|
||||
CassandraPreparedStatement selectNFT_;
|
||||
CassandraPreparedStatement insertIssuerNFT_;
|
||||
CassandraPreparedStatement insertNFTURI_;
|
||||
CassandraPreparedStatement selectNFTURI_;
|
||||
CassandraPreparedStatement insertNFTTx_;
|
||||
CassandraPreparedStatement selectNFTTx_;
|
||||
CassandraPreparedStatement selectNFTTxForward_;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
36
src/etl/NFTHelpers.h
Normal 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);
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user