mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-05 08:48:03 +00:00
Add nftoken_id, nftoken_ids, offer_id fields for NFTokens (#4447)
Three new fields are added to the `Tx` responses for NFTs: 1. `nftoken_id`: This field is included in the `Tx` responses for `NFTokenMint` and `NFTokenAcceptOffer`. This field indicates the `NFTokenID` for the `NFToken` that was modified on the ledger by the transaction. 2. `nftoken_ids`: This array is included in the `Tx` response for `NFTokenCancelOffer`. This field provides a list of all the `NFTokenID`s for the `NFToken`s that were modified on the ledger by the transaction. 3. `offer_id`: This field is included in the `Tx` response for `NFTokenCreateOffer` transactions and shows the OfferID of the `NFTokenOffer` created. The fields make it easier to track specific tokens and offers. The implementation includes code (by @ledhed2222) from the Clio project to extract NFTokenIDs from mint transactions.
This commit is contained in:
@@ -653,6 +653,9 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/rpc/impl/ShardVerificationScheduler.cpp
|
||||
src/ripple/rpc/impl/Status.cpp
|
||||
src/ripple/rpc/impl/TransactionSign.cpp
|
||||
src/ripple/rpc/impl/NFTokenID.cpp
|
||||
src/ripple/rpc/impl/NFTokenOfferID.cpp
|
||||
src/ripple/rpc/impl/NFTSyntheticSerializer.cpp
|
||||
#[===============================[
|
||||
main sources:
|
||||
subdir: perflog
|
||||
|
||||
@@ -416,6 +416,8 @@ JSS(nft_offer_index); // out nft_buy_offers, nft_sell_offers
|
||||
JSS(nft_page); // in: LedgerEntry
|
||||
JSS(nft_serial); // out: account_nfts
|
||||
JSS(nft_taxon); // out: nft_info (clio)
|
||||
JSS(nftoken_id); // out: insertNFTokenID
|
||||
JSS(nftoken_ids); // out: insertNFTokenID
|
||||
JSS(no_ripple); // out: AccountLines
|
||||
JSS(no_ripple_peer); // out: AccountLines
|
||||
JSS(node); // out: LedgerEntry
|
||||
@@ -436,6 +438,7 @@ JSS(node_writes_delayed); // out::GetCounts
|
||||
JSS(obligations); // out: GatewayBalances
|
||||
JSS(offer); // in: LedgerEntry
|
||||
JSS(offers); // out: NetworkOPs, AccountOffers, Subscribe
|
||||
JSS(offer_id); // out: insertNFTokenOfferID
|
||||
JSS(offline); // in: TransactionSign
|
||||
JSS(offset); // in/out: AccountTxOld
|
||||
JSS(open); // out: handlers/Ledger
|
||||
|
||||
58
src/ripple/rpc/NFTSyntheticSerializer.h
Normal file
58
src/ripple/rpc/NFTSyntheticSerializer.h
Normal file
@@ -0,0 +1,58 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_RPC_NFTSYNTHETICSERIALIZER_H_INCLUDED
|
||||
#define RIPPLE_RPC_NFTSYNTHETICSERIALIZER_H_INCLUDED
|
||||
|
||||
#include <ripple/protocol/Protocol.h>
|
||||
#include <ripple/protocol/STBase.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace Json {
|
||||
class Value;
|
||||
}
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class TxMeta;
|
||||
class STTx;
|
||||
|
||||
namespace RPC {
|
||||
|
||||
struct JsonContext;
|
||||
|
||||
/**
|
||||
Adds common synthetic fields to transaction-related JSON responses
|
||||
|
||||
@{
|
||||
*/
|
||||
void
|
||||
insertNFTSyntheticInJson(
|
||||
Json::Value&,
|
||||
RPC::JsonContext const&,
|
||||
std::shared_ptr<STTx const> const&,
|
||||
TxMeta const&);
|
||||
/** @} */
|
||||
|
||||
} // namespace RPC
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
68
src/ripple/rpc/NFTokenID.h
Normal file
68
src/ripple/rpc/NFTokenID.h
Normal file
@@ -0,0 +1,68 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_RPC_NFTOKENID_H_INCLUDED
|
||||
#define RIPPLE_RPC_NFTOKENID_H_INCLUDED
|
||||
|
||||
#include <ripple/protocol/Protocol.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace Json {
|
||||
class Value;
|
||||
}
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class TxMeta;
|
||||
class STTx;
|
||||
|
||||
namespace RPC {
|
||||
|
||||
/**
|
||||
Add a `nftoken_ids` field to the `meta` output parameter.
|
||||
The field is only added to successful NFTokenMint, NFTokenAcceptOffer,
|
||||
and NFTokenCancelOffer transactions.
|
||||
|
||||
Helper functions are not static because they can be used by Clio.
|
||||
@{
|
||||
*/
|
||||
bool
|
||||
canHaveNFTokenID(
|
||||
std::shared_ptr<STTx const> const& serializedTx,
|
||||
TxMeta const& transactionMeta);
|
||||
|
||||
std::optional<uint256>
|
||||
getNFTokenIDFromPage(TxMeta const& transactionMeta);
|
||||
|
||||
std::vector<uint256>
|
||||
getNFTokenIDFromDeletedOffer(TxMeta const& transactionMeta);
|
||||
|
||||
void
|
||||
insertNFTokenID(
|
||||
Json::Value& response,
|
||||
std::shared_ptr<STTx const> const& transaction,
|
||||
TxMeta const& transactionMeta);
|
||||
/** @} */
|
||||
|
||||
} // namespace RPC
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
64
src/ripple/rpc/NFTokenOfferID.h
Normal file
64
src/ripple/rpc/NFTokenOfferID.h
Normal file
@@ -0,0 +1,64 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_RPC_NFTOKENOFFERID_H_INCLUDED
|
||||
#define RIPPLE_RPC_NFTOKENOFFERID_H_INCLUDED
|
||||
|
||||
#include <ripple/protocol/Protocol.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace Json {
|
||||
class Value;
|
||||
}
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class TxMeta;
|
||||
class STTx;
|
||||
|
||||
namespace RPC {
|
||||
|
||||
/**
|
||||
Add an `offer_id` field to the `meta` output parameter.
|
||||
The field is only added to successful NFTokenCreateOffer transactions.
|
||||
|
||||
Helper functions are not static because they can be used by Clio.
|
||||
@{
|
||||
*/
|
||||
bool
|
||||
canHaveNFTokenOfferID(
|
||||
std::shared_ptr<STTx const> const& serializedTx,
|
||||
TxMeta const& transactionMeta);
|
||||
|
||||
std::optional<uint256>
|
||||
getOfferIDFromCreatedOffer(TxMeta const& transactionMeta);
|
||||
|
||||
void
|
||||
insertNFTokenOfferID(
|
||||
Json::Value& response,
|
||||
std::shared_ptr<STTx const> const& transaction,
|
||||
TxMeta const& transactionMeta);
|
||||
/** @} */
|
||||
|
||||
} // namespace RPC
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <ripple/resource/Fees.h>
|
||||
#include <ripple/rpc/Context.h>
|
||||
#include <ripple/rpc/DeliveredAmount.h>
|
||||
#include <ripple/rpc/NFTSyntheticSerializer.h>
|
||||
#include <ripple/rpc/Role.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
|
||||
@@ -307,6 +308,8 @@ populateJsonResponse(
|
||||
jvObj[jss::validated] = true;
|
||||
insertDeliveredAmount(
|
||||
jvObj[jss::meta], context, txn, *txnMeta);
|
||||
insertNFTSyntheticInJson(
|
||||
jvObj, context, txn->getSTransaction(), *txnMeta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <ripple/rpc/Context.h>
|
||||
#include <ripple/rpc/DeliveredAmount.h>
|
||||
#include <ripple/rpc/GRPCHandlers.h>
|
||||
#include <ripple/rpc/NFTSyntheticSerializer.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
|
||||
namespace ripple {
|
||||
@@ -295,6 +296,8 @@ populateJsonResponse(
|
||||
response[jss::meta] = meta->getJson(JsonOptions::none);
|
||||
insertDeliveredAmount(
|
||||
response[jss::meta], context, result.txn, *meta);
|
||||
insertNFTSyntheticInJson(
|
||||
response, context, result.txn->getSTransaction(), *meta);
|
||||
}
|
||||
}
|
||||
response[jss::validated] = result.validated;
|
||||
|
||||
50
src/ripple/rpc/impl/NFTSyntheticSerializer.cpp
Normal file
50
src/ripple/rpc/impl/NFTSyntheticSerializer.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/rpc/NFTSyntheticSerializer.h>
|
||||
|
||||
#include <ripple/app/ledger/LedgerMaster.h>
|
||||
#include <ripple/app/ledger/OpenLedger.h>
|
||||
#include <ripple/app/misc/Transaction.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/net/RPCErr.h>
|
||||
#include <ripple/protocol/AccountID.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/rpc/Context.h>
|
||||
#include <ripple/rpc/NFTokenID.h>
|
||||
#include <ripple/rpc/NFTokenOfferID.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
|
||||
namespace ripple {
|
||||
namespace RPC {
|
||||
|
||||
void
|
||||
insertNFTSyntheticInJson(
|
||||
Json::Value& response,
|
||||
RPC::JsonContext const& context,
|
||||
std::shared_ptr<STTx const> const& transaction,
|
||||
TxMeta const& transactionMeta)
|
||||
{
|
||||
insertNFTokenID(response[jss::meta], transaction, transactionMeta);
|
||||
insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta);
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
} // namespace ripple
|
||||
202
src/ripple/rpc/impl/NFTokenID.cpp
Normal file
202
src/ripple/rpc/impl/NFTokenID.cpp
Normal file
@@ -0,0 +1,202 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/rpc/NFTokenID.h>
|
||||
|
||||
#include <ripple/app/ledger/LedgerMaster.h>
|
||||
#include <ripple/app/ledger/OpenLedger.h>
|
||||
#include <ripple/app/misc/Transaction.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/net/RPCErr.h>
|
||||
#include <ripple/protocol/AccountID.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/rpc/Context.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
|
||||
namespace ripple {
|
||||
namespace RPC {
|
||||
|
||||
bool
|
||||
canHaveNFTokenID(
|
||||
std::shared_ptr<STTx const> const& serializedTx,
|
||||
TxMeta const& transactionMeta)
|
||||
{
|
||||
if (!serializedTx)
|
||||
return false;
|
||||
|
||||
TxType const tt = serializedTx->getTxnType();
|
||||
if (tt != ttNFTOKEN_MINT && tt != ttNFTOKEN_ACCEPT_OFFER &&
|
||||
tt != ttNFTOKEN_CANCEL_OFFER)
|
||||
return false;
|
||||
|
||||
// if the transaction failed nothing could have been delivered.
|
||||
if (transactionMeta.getResultTER() != tesSUCCESS)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<uint256>
|
||||
getNFTokenIDFromPage(TxMeta const& transactionMeta)
|
||||
{
|
||||
// The metadata does not make it obvious which NFT was added. To figure
|
||||
// that out we gather up all of the previous NFT IDs and all of the final
|
||||
// NFT IDs and compare them to find what changed.
|
||||
std::vector<uint256> prevIDs;
|
||||
std::vector<uint256> finalIDs;
|
||||
|
||||
for (STObject const& node : transactionMeta.getNodes())
|
||||
{
|
||||
if (node.getFieldU16(sfLedgerEntryType) != ltNFTOKEN_PAGE)
|
||||
continue;
|
||||
|
||||
SField const& fName = node.getFName();
|
||||
if (fName == sfCreatedNode)
|
||||
{
|
||||
STArray const& toAddPrevNFTs = node.peekAtField(sfNewFields)
|
||||
.downcast<STObject>()
|
||||
.getFieldArray(sfNFTokens);
|
||||
std::transform(
|
||||
toAddPrevNFTs.begin(),
|
||||
toAddPrevNFTs.end(),
|
||||
std::back_inserter(finalIDs),
|
||||
[](STObject const& nft) {
|
||||
return nft.getFieldH256(sfNFTokenID);
|
||||
});
|
||||
}
|
||||
else if (fName == sfModifiedNode)
|
||||
{
|
||||
// When a mint results in splitting an existing page,
|
||||
// it results in a created page and a modified node. Sometimes,
|
||||
// the created node needs to be linked to a third page, resulting
|
||||
// in modifying that third page's PreviousPageMin or NextPageMin
|
||||
// field changing, but no NFTs within that page changing. In this
|
||||
// case, there will be no previous NFTs and we need to skip.
|
||||
// However, there will always be NFTs listed in the final fields,
|
||||
// as rippled outputs all fields in final fields even if they were
|
||||
// not changed.
|
||||
STObject const& previousFields =
|
||||
node.peekAtField(sfPreviousFields).downcast<STObject>();
|
||||
if (!previousFields.isFieldPresent(sfNFTokens))
|
||||
continue;
|
||||
|
||||
STArray const& toAddPrevNFTs =
|
||||
previousFields.getFieldArray(sfNFTokens);
|
||||
std::transform(
|
||||
toAddPrevNFTs.begin(),
|
||||
toAddPrevNFTs.end(),
|
||||
std::back_inserter(prevIDs),
|
||||
[](STObject const& nft) {
|
||||
return nft.getFieldH256(sfNFTokenID);
|
||||
});
|
||||
|
||||
STArray const& toAddFinalNFTs = node.peekAtField(sfFinalFields)
|
||||
.downcast<STObject>()
|
||||
.getFieldArray(sfNFTokens);
|
||||
std::transform(
|
||||
toAddFinalNFTs.begin(),
|
||||
toAddFinalNFTs.end(),
|
||||
std::back_inserter(finalIDs),
|
||||
[](STObject const& nft) {
|
||||
return nft.getFieldH256(sfNFTokenID);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// We expect NFTs to be added one at a time. So finalIDs should be one
|
||||
// longer than prevIDs. If that's not the case something is messed up.
|
||||
if (finalIDs.size() != prevIDs.size() + 1)
|
||||
return std::nullopt;
|
||||
|
||||
// Find the first NFT ID that doesn't match. We're looking for an
|
||||
// added NFT, so the one we want will be the mismatch in finalIDs.
|
||||
auto const diff = std::mismatch(
|
||||
finalIDs.begin(), finalIDs.end(), prevIDs.begin(), prevIDs.end());
|
||||
|
||||
// There should always be a difference so the returned finalIDs
|
||||
// iterator should never be end(). But better safe than sorry.
|
||||
if (diff.first == finalIDs.end())
|
||||
return std::nullopt;
|
||||
|
||||
return *diff.first;
|
||||
}
|
||||
|
||||
std::vector<uint256>
|
||||
getNFTokenIDFromDeletedOffer(TxMeta const& transactionMeta)
|
||||
{
|
||||
std::vector<uint256> tokenIDResult;
|
||||
for (STObject const& node : transactionMeta.getNodes())
|
||||
{
|
||||
if (node.getFieldU16(sfLedgerEntryType) != ltNFTOKEN_OFFER ||
|
||||
node.getFName() != sfDeletedNode)
|
||||
continue;
|
||||
|
||||
auto const& toAddNFT = node.peekAtField(sfFinalFields)
|
||||
.downcast<STObject>()
|
||||
.getFieldH256(sfNFTokenID);
|
||||
tokenIDResult.push_back(toAddNFT);
|
||||
}
|
||||
|
||||
// Deduplicate the NFT IDs because multiple offers could affect the same NFT
|
||||
// and hence we would get duplicate NFT IDs
|
||||
sort(tokenIDResult.begin(), tokenIDResult.end());
|
||||
tokenIDResult.erase(
|
||||
unique(tokenIDResult.begin(), tokenIDResult.end()),
|
||||
tokenIDResult.end());
|
||||
return tokenIDResult;
|
||||
}
|
||||
|
||||
void
|
||||
insertNFTokenID(
|
||||
Json::Value& response,
|
||||
std::shared_ptr<STTx const> const& transaction,
|
||||
TxMeta const& transactionMeta)
|
||||
{
|
||||
if (!canHaveNFTokenID(transaction, transactionMeta))
|
||||
return;
|
||||
|
||||
// We extract the NFTokenID from metadata by comparing affected nodes
|
||||
if (auto const type = transaction->getTxnType(); type == ttNFTOKEN_MINT)
|
||||
{
|
||||
std::optional<uint256> result = getNFTokenIDFromPage(transactionMeta);
|
||||
if (result.has_value())
|
||||
response[jss::nftoken_id] = to_string(result.value());
|
||||
}
|
||||
else if (type == ttNFTOKEN_ACCEPT_OFFER)
|
||||
{
|
||||
std::vector<uint256> result =
|
||||
getNFTokenIDFromDeletedOffer(transactionMeta);
|
||||
|
||||
if (result.size() > 0)
|
||||
response[jss::nftoken_id] = to_string(result.front());
|
||||
}
|
||||
else if (type == ttNFTOKEN_CANCEL_OFFER)
|
||||
{
|
||||
std::vector<uint256> result =
|
||||
getNFTokenIDFromDeletedOffer(transactionMeta);
|
||||
|
||||
response[jss::nftoken_ids] = Json::Value(Json::arrayValue);
|
||||
for (auto const& nftID : result)
|
||||
response[jss::nftoken_ids].append(to_string(nftID));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
} // namespace ripple
|
||||
85
src/ripple/rpc/impl/NFTokenOfferID.cpp
Normal file
85
src/ripple/rpc/impl/NFTokenOfferID.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/rpc/NFTokenOfferID.h>
|
||||
|
||||
#include <ripple/app/ledger/LedgerMaster.h>
|
||||
#include <ripple/app/ledger/OpenLedger.h>
|
||||
#include <ripple/app/misc/Transaction.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/net/RPCErr.h>
|
||||
#include <ripple/protocol/AccountID.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/rpc/Context.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
|
||||
namespace ripple {
|
||||
namespace RPC {
|
||||
|
||||
bool
|
||||
canHaveNFTokenOfferID(
|
||||
std::shared_ptr<STTx const> const& serializedTx,
|
||||
TxMeta const& transactionMeta)
|
||||
{
|
||||
if (!serializedTx)
|
||||
return false;
|
||||
|
||||
TxType const tt = serializedTx->getTxnType();
|
||||
if (tt != ttNFTOKEN_CREATE_OFFER)
|
||||
return false;
|
||||
|
||||
// if the transaction failed nothing could have been delivered.
|
||||
if (transactionMeta.getResultTER() != tesSUCCESS)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<uint256>
|
||||
getOfferIDFromCreatedOffer(TxMeta const& transactionMeta)
|
||||
{
|
||||
for (STObject const& node : transactionMeta.getNodes())
|
||||
{
|
||||
if (node.getFieldU16(sfLedgerEntryType) != ltNFTOKEN_OFFER ||
|
||||
node.getFName() != sfCreatedNode)
|
||||
continue;
|
||||
|
||||
return node.getFieldH256(sfLedgerIndex);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void
|
||||
insertNFTokenOfferID(
|
||||
Json::Value& response,
|
||||
std::shared_ptr<STTx const> const& transaction,
|
||||
TxMeta const& transactionMeta)
|
||||
{
|
||||
if (!canHaveNFTokenOfferID(transaction, transactionMeta))
|
||||
return;
|
||||
|
||||
std::optional<uint256> result = getOfferIDFromCreatedOffer(transactionMeta);
|
||||
|
||||
if (result.has_value())
|
||||
response[jss::offer_id] = to_string(result.value());
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
} // namespace ripple
|
||||
@@ -6566,6 +6566,238 @@ class NFToken_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testTxJsonMetaFields(FeatureBitset features)
|
||||
{
|
||||
// `nftoken_id` is added in the `tx` response for NFTokenMint and
|
||||
// NFTokenAcceptOffer.
|
||||
//
|
||||
// `nftoken_ids` is added in the `tx` response for NFTokenCancelOffer
|
||||
//
|
||||
// `offer_id` is added in the `tx` response for NFTokenCreateOffer
|
||||
//
|
||||
// The values of these fields are dependent on the NFTokenID/OfferID
|
||||
// changed in its corresponding transaction. We want to validate each
|
||||
// transaction to make sure the synethic fields hold the right values.
|
||||
|
||||
testcase("Test synthetic fields from JSON response");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const broker{"broker"};
|
||||
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10000), alice, bob, broker);
|
||||
env.close();
|
||||
|
||||
// Verify `nftoken_id` value equals to the NFTokenID that was
|
||||
// changed in the most recent NFTokenMint or NFTokenAcceptOffer
|
||||
// transaction
|
||||
auto verifyNFTokenID = [&](uint256 const& actualNftID) {
|
||||
// Get the hash for the most recent transaction.
|
||||
std::string const txHash{
|
||||
env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
|
||||
|
||||
env.close();
|
||||
Json::Value const meta =
|
||||
env.rpc("tx", txHash)[jss::result][jss::meta];
|
||||
|
||||
// Expect nftokens_id field
|
||||
if (!BEAST_EXPECT(meta.isMember(jss::nftoken_id)))
|
||||
return;
|
||||
|
||||
// Check the value of NFT ID in the meta with the
|
||||
// actual value
|
||||
uint256 nftID;
|
||||
BEAST_EXPECT(nftID.parseHex(meta[jss::nftoken_id].asString()));
|
||||
BEAST_EXPECT(nftID == actualNftID);
|
||||
};
|
||||
|
||||
// Verify `nftoken_ids` value equals to the NFTokenIDs that were
|
||||
// changed in the most recent NFTokenCancelOffer transaction
|
||||
auto verifyNFTokenIDsInCancelOffer =
|
||||
[&](std::vector<uint256> actualNftIDs) {
|
||||
// Get the hash for the most recent transaction.
|
||||
std::string const txHash{
|
||||
env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
|
||||
|
||||
env.close();
|
||||
Json::Value const meta =
|
||||
env.rpc("tx", txHash)[jss::result][jss::meta];
|
||||
|
||||
// Expect nftokens_ids field and verify the values
|
||||
if (!BEAST_EXPECT(meta.isMember(jss::nftoken_ids)))
|
||||
return;
|
||||
|
||||
// Convert NFT IDs from Json::Value to uint256
|
||||
std::vector<uint256> metaIDs;
|
||||
std::transform(
|
||||
meta[jss::nftoken_ids].begin(),
|
||||
meta[jss::nftoken_ids].end(),
|
||||
std::back_inserter(metaIDs),
|
||||
[this](Json::Value id) {
|
||||
uint256 nftID;
|
||||
BEAST_EXPECT(nftID.parseHex(id.asString()));
|
||||
return nftID;
|
||||
});
|
||||
|
||||
// Sort both array to prepare for comparison
|
||||
std::sort(metaIDs.begin(), metaIDs.end());
|
||||
std::sort(actualNftIDs.begin(), actualNftIDs.end());
|
||||
|
||||
// Make sure the expect number of NFTs is correct
|
||||
BEAST_EXPECT(metaIDs.size() == actualNftIDs.size());
|
||||
|
||||
// Check the value of NFT ID in the meta with the
|
||||
// actual values
|
||||
for (size_t i = 0; i < metaIDs.size(); ++i)
|
||||
BEAST_EXPECT(metaIDs[i] == actualNftIDs[i]);
|
||||
};
|
||||
|
||||
// Verify `offer_id` value equals to the offerID that was
|
||||
// changed in the most recent NFTokenCreateOffer tx
|
||||
auto verifyNFTokenOfferID = [&](uint256 const& offerID) {
|
||||
// Get the hash for the most recent transaction.
|
||||
std::string const txHash{
|
||||
env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
|
||||
|
||||
env.close();
|
||||
Json::Value const meta =
|
||||
env.rpc("tx", txHash)[jss::result][jss::meta];
|
||||
|
||||
// Expect offer_id field and verify the value
|
||||
if (!BEAST_EXPECT(meta.isMember(jss::offer_id)))
|
||||
return;
|
||||
|
||||
uint256 metaOfferID;
|
||||
BEAST_EXPECT(metaOfferID.parseHex(meta[jss::offer_id].asString()));
|
||||
BEAST_EXPECT(metaOfferID == offerID);
|
||||
};
|
||||
|
||||
// Check new fields in tx meta when for all NFTtransactions
|
||||
{
|
||||
// Alice mints 2 NFTs
|
||||
// Verify the NFTokenIDs are correct in the NFTokenMint tx meta
|
||||
uint256 const nftId1{
|
||||
token::getNextID(env, alice, 0u, tfTransferable)};
|
||||
env(token::mint(alice, 0u), txflags(tfTransferable));
|
||||
env.close();
|
||||
verifyNFTokenID(nftId1);
|
||||
|
||||
uint256 const nftId2{
|
||||
token::getNextID(env, alice, 0u, tfTransferable)};
|
||||
env(token::mint(alice, 0u), txflags(tfTransferable));
|
||||
env.close();
|
||||
verifyNFTokenID(nftId2);
|
||||
|
||||
// Alice creates one sell offer for each NFT
|
||||
// Verify the offer indexes are correct in the NFTokenCreateOffer tx
|
||||
// meta
|
||||
uint256 const aliceOfferIndex1 =
|
||||
keylet::nftoffer(alice, env.seq(alice)).key;
|
||||
env(token::createOffer(alice, nftId1, drops(1)),
|
||||
txflags(tfSellNFToken));
|
||||
env.close();
|
||||
verifyNFTokenOfferID(aliceOfferIndex1);
|
||||
|
||||
uint256 const aliceOfferIndex2 =
|
||||
keylet::nftoffer(alice, env.seq(alice)).key;
|
||||
env(token::createOffer(alice, nftId2, drops(1)),
|
||||
txflags(tfSellNFToken));
|
||||
env.close();
|
||||
verifyNFTokenOfferID(aliceOfferIndex2);
|
||||
|
||||
// Alice cancels two offers she created
|
||||
// Verify the NFTokenIDs are correct in the NFTokenCancelOffer tx
|
||||
// meta
|
||||
env(token::cancelOffer(
|
||||
alice, {aliceOfferIndex1, aliceOfferIndex2}));
|
||||
env.close();
|
||||
verifyNFTokenIDsInCancelOffer({nftId1, nftId2});
|
||||
|
||||
// Bobs creates a buy offer for nftId1
|
||||
// Verify the offer id is correct in the NFTokenCreateOffer tx meta
|
||||
auto const bobBuyOfferIndex =
|
||||
keylet::nftoffer(bob, env.seq(bob)).key;
|
||||
env(token::createOffer(bob, nftId1, drops(1)), token::owner(alice));
|
||||
env.close();
|
||||
verifyNFTokenOfferID(bobBuyOfferIndex);
|
||||
|
||||
// Alice accepts bob's buy offer
|
||||
// Verify the NFTokenID is correct in the NFTokenAcceptOffer tx meta
|
||||
env(token::acceptBuyOffer(alice, bobBuyOfferIndex));
|
||||
env.close();
|
||||
verifyNFTokenID(nftId1);
|
||||
}
|
||||
|
||||
// Check `nftoken_ids` in brokered mode
|
||||
{
|
||||
// Alice mints a NFT
|
||||
uint256 const nftId{
|
||||
token::getNextID(env, alice, 0u, tfTransferable)};
|
||||
env(token::mint(alice, 0u), txflags(tfTransferable));
|
||||
env.close();
|
||||
verifyNFTokenID(nftId);
|
||||
|
||||
// Alice creates sell offer and set broker as destination
|
||||
uint256 const offerAliceToBroker =
|
||||
keylet::nftoffer(alice, env.seq(alice)).key;
|
||||
env(token::createOffer(alice, nftId, drops(1)),
|
||||
token::destination(broker),
|
||||
txflags(tfSellNFToken));
|
||||
env.close();
|
||||
verifyNFTokenOfferID(offerAliceToBroker);
|
||||
|
||||
// Bob creates buy offer
|
||||
uint256 const offerBobToBroker =
|
||||
keylet::nftoffer(bob, env.seq(bob)).key;
|
||||
env(token::createOffer(bob, nftId, drops(1)), token::owner(alice));
|
||||
env.close();
|
||||
verifyNFTokenOfferID(offerBobToBroker);
|
||||
|
||||
// Check NFTokenID meta for NFTokenAcceptOffer in brokered mode
|
||||
env(token::brokerOffers(
|
||||
broker, offerBobToBroker, offerAliceToBroker));
|
||||
env.close();
|
||||
verifyNFTokenID(nftId);
|
||||
}
|
||||
|
||||
// Check if there are no duplicate nft id in Cancel transactions where
|
||||
// multiple offers are cancelled for the same NFT
|
||||
{
|
||||
// Alice mints a NFT
|
||||
uint256 const nftId{
|
||||
token::getNextID(env, alice, 0u, tfTransferable)};
|
||||
env(token::mint(alice, 0u), txflags(tfTransferable));
|
||||
env.close();
|
||||
verifyNFTokenID(nftId);
|
||||
|
||||
// Alice creates 2 sell offers for the same NFT
|
||||
uint256 const aliceOfferIndex1 =
|
||||
keylet::nftoffer(alice, env.seq(alice)).key;
|
||||
env(token::createOffer(alice, nftId, drops(1)),
|
||||
txflags(tfSellNFToken));
|
||||
env.close();
|
||||
verifyNFTokenOfferID(aliceOfferIndex1);
|
||||
|
||||
uint256 const aliceOfferIndex2 =
|
||||
keylet::nftoffer(alice, env.seq(alice)).key;
|
||||
env(token::createOffer(alice, nftId, drops(1)),
|
||||
txflags(tfSellNFToken));
|
||||
env.close();
|
||||
verifyNFTokenOfferID(aliceOfferIndex2);
|
||||
|
||||
// Make sure the metadata only has 1 nft id, since both offers are
|
||||
// for the same nft
|
||||
env(token::cancelOffer(
|
||||
alice, {aliceOfferIndex1, aliceOfferIndex2}));
|
||||
env.close();
|
||||
verifyNFTokenIDsInCancelOffer({nftId});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
@@ -6598,6 +6830,7 @@ class NFToken_test : public beast::unit_test::suite
|
||||
testIOUWithTransferFee(features);
|
||||
testBrokeredSaleToSelf(features);
|
||||
testFixNFTokenRemint(features);
|
||||
testTxJsonMetaFields(features);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
Reference in New Issue
Block a user