Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion include/xrpl/protocol/NFTSyntheticSerializer.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace xrpl {
namespace RPC {

/**
Adds common synthetic fields to transaction-related JSON responses
Adds common synthetic fields to transaction metadata JSON

@{
*/
Expand Down
6 changes: 3 additions & 3 deletions src/libxrpl/protocol/NFTSyntheticSerializer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ namespace RPC {

void
insertNFTSyntheticInJson(
Json::Value& response,
Json::Value& metadata,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta)
{
insertNFTokenID(response[jss::meta], transaction, transactionMeta);
insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta);
insertNFTokenID(metadata, transaction, transactionMeta);
insertNFTokenOfferID(metadata, transaction, transactionMeta);
}

} // namespace RPC
Expand Down
187 changes: 177 additions & 10 deletions src/test/app/NFToken_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6060,17 +6060,88 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
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)))
// Test 1: Check tx RPC response
Json::Value const txResult = env.rpc("tx", txHash)[jss::result];
Json::Value const& txMeta = txResult[jss::meta];

// Expect nftoken_id field
if (!BEAST_EXPECT(txMeta.isMember(jss::nftoken_id)))
return;

// Check the value of NFT ID in the meta with the
// actual value
// Check the value of NFT ID matches
uint256 nftID;
BEAST_EXPECT(nftID.parseHex(meta[jss::nftoken_id].asString()));
BEAST_EXPECT(nftID.parseHex(txMeta[jss::nftoken_id].asString()));
BEAST_EXPECT(nftID == actualNftID);

// Get ledger sequence from tx response
auto const ledgerSeq = txResult[jss::ledger_index].asUInt();

// Test 2: Check ledger RPC response with expanded transactions
Json::Value ledgerParams;
ledgerParams[jss::ledger_index] = ledgerSeq;
ledgerParams[jss::transactions] = true;
ledgerParams[jss::expand] = true;

auto const ledgerResult = env.rpc("json", "ledger", to_string(ledgerParams));
auto const& tx = ledgerResult[jss::result][jss::ledger][jss::transactions][0u];

// Verify transaction hash matches
BEAST_EXPECT(tx[jss::hash].asString() == txHash);

// Check synthetic fields in ledger response (this tests our
// LedgerToJson.cpp fix)
Json::Value const* meta = nullptr;
if (tx.isMember(jss::meta))
meta = &tx[jss::meta];
else if (tx.isMember(jss::metaData))
meta = &tx[jss::metaData];

if (BEAST_EXPECT(meta != nullptr))
{
BEAST_EXPECT(meta->isMember(jss::nftoken_id));
if (meta->isMember(jss::nftoken_id))
{
uint256 ledgerNftId;
BEAST_EXPECT(ledgerNftId.parseHex((*meta)[jss::nftoken_id].asString()));
BEAST_EXPECT(ledgerNftId == actualNftID);
}
}

// Test 3: Check account_tx RPC response
Json::Value accountTxParams;
accountTxParams[jss::account] = alice.human();
accountTxParams[jss::limit] = 1;

auto const accountTxResult = env.rpc("json", "account_tx", to_string(accountTxParams));
auto const& accountTx = accountTxResult[jss::result][jss::transactions][0u];

// Check if the latest transaction is ours (it should be, but
// account_tx can be ordering-dependent)
bool const isOurTransaction = (accountTx[jss::hash].asString() == txHash);

// Only check synthetic fields if this is our transaction
if (isOurTransaction)
{
// Check synthetic fields in account_tx response
Json::Value const* accountMeta = nullptr;
if (accountTx.isMember(jss::meta))
accountMeta = &accountTx[jss::meta];
else if (accountTx.isMember(jss::metaData))
accountMeta = &accountTx[jss::metaData];

if (BEAST_EXPECT(accountMeta != nullptr))
{
BEAST_EXPECT(accountMeta->isMember(jss::nftoken_id));
if (accountMeta->isMember(jss::nftoken_id))
{
uint256 accountNftId;
BEAST_EXPECT(
accountNftId.parseHex((*accountMeta)[jss::nftoken_id].asString()));
BEAST_EXPECT(accountNftId == actualNftID);
}
}
}
};

// Verify `nftoken_ids` value equals to the NFTokenIDs that were
Expand All @@ -6080,17 +6151,20 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
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];

// Test 1: Check tx RPC response
Json::Value const txResult = env.rpc("tx", txHash)[jss::result];
Json::Value const& txMeta = txResult[jss::meta];

// Expect nftokens_ids field and verify the values
if (!BEAST_EXPECT(meta.isMember(jss::nftoken_ids)))
if (!BEAST_EXPECT(txMeta.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(),
txMeta[jss::nftoken_ids].begin(),
txMeta[jss::nftoken_ids].end(),
std::back_inserter(metaIDs),
[this](Json::Value id) {
uint256 nftID;
Expand All @@ -6109,6 +6183,99 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
// actual values
for (size_t i = 0; i < metaIDs.size(); ++i)
BEAST_EXPECT(metaIDs[i] == actualNftIDs[i]);

// Get ledger sequence from tx response
auto const ledgerSeq = txResult[jss::ledger_index].asUInt();

// Test 2: Check ledger RPC response with expanded transactions
Json::Value ledgerParams;
ledgerParams[jss::ledger_index] = ledgerSeq;
ledgerParams[jss::transactions] = true;
ledgerParams[jss::expand] = true;

auto const ledgerResult = env.rpc("json", "ledger", to_string(ledgerParams));
auto const& tx = ledgerResult[jss::result][jss::ledger][jss::transactions][0u];

// Verify transaction hash matches
BEAST_EXPECT(tx[jss::hash].asString() == txHash);

// Check synthetic fields in ledger response
Json::Value const* meta = nullptr;
if (tx.isMember(jss::meta))
meta = &tx[jss::meta];
else if (tx.isMember(jss::metaData))
meta = &tx[jss::metaData];

if (BEAST_EXPECT(meta != nullptr))
{
BEAST_EXPECT(meta->isMember(jss::nftoken_ids));
if (meta->isMember(jss::nftoken_ids))
{
// Convert and verify NFT IDs in ledger response
std::vector<uint256> ledgerMetaIDs;
std::transform(
(*meta)[jss::nftoken_ids].begin(),
(*meta)[jss::nftoken_ids].end(),
std::back_inserter(ledgerMetaIDs),
[this](Json::Value id) {
uint256 nftID;
BEAST_EXPECT(nftID.parseHex(id.asString()));
return nftID;
});

std::sort(ledgerMetaIDs.begin(), ledgerMetaIDs.end());
BEAST_EXPECT(ledgerMetaIDs.size() == actualNftIDs.size());
for (size_t i = 0; i < ledgerMetaIDs.size(); ++i)
BEAST_EXPECT(ledgerMetaIDs[i] == actualNftIDs[i]);
}
}

// Test 3: Check account_tx RPC response
Json::Value accountTxParams;
accountTxParams[jss::account] = alice.human();
accountTxParams[jss::limit] = 1;

auto const accountTxResult = env.rpc("json", "account_tx", to_string(accountTxParams));
auto const& accountTx = accountTxResult[jss::result][jss::transactions][0u];

// Check if the latest transaction is ours (it should be, but
// account_tx can be ordering-dependent)
bool const isOurTransaction = (accountTx[jss::hash].asString() == txHash);

// Only check synthetic fields if this is our transaction
if (isOurTransaction)
{
// Check synthetic fields in account_tx response
Json::Value const* accountMeta = nullptr;
if (accountTx.isMember(jss::meta))
accountMeta = &accountTx[jss::meta];
else if (accountTx.isMember(jss::metaData))
accountMeta = &accountTx[jss::metaData];

if (BEAST_EXPECT(accountMeta != nullptr))
{
BEAST_EXPECT(accountMeta->isMember(jss::nftoken_ids));
if (accountMeta->isMember(jss::nftoken_ids))
{
// Convert and verify NFT IDs in account_tx response
std::vector<uint256> accountMetaIDs;
std::transform(
(*accountMeta)[jss::nftoken_ids].begin(),
(*accountMeta)[jss::nftoken_ids].end(),
std::back_inserter(accountMetaIDs),
[this](Json::Value id) {
uint256 nftID;
BEAST_EXPECT(nftID.parseHex(id.asString()));
return nftID;
});

std::sort(accountMetaIDs.begin(), accountMetaIDs.end());
BEAST_EXPECT(accountMetaIDs.size() == actualNftIDs.size());
for (size_t i = 0; i < accountMetaIDs.size(); ++i)
BEAST_EXPECT(accountMetaIDs[i] == actualNftIDs[i]);
}
}
}
};

// Verify `offer_id` value equals to the offerID that was
Expand Down
42 changes: 14 additions & 28 deletions src/xrpld/app/ledger/detail/LedgerToJson.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
#include <xrpld/app/misc/DeliverMax.h>
#include <xrpld/app/misc/TxQ.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/DeliveredAmount.h>
#include <xrpld/rpc/MPTokenIssuanceID.h>
#include <xrpld/rpc/detail/SyntheticFields.h>

#include <xrpl/basics/base_uint.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/NFTSyntheticSerializer.h>
#include <xrpl/protocol/jss.h>

namespace xrpl {
Expand Down Expand Up @@ -121,19 +121,12 @@ fillJsonTx(
{
txJson[jss::meta] = stMeta->getJson(JsonOptions::none);

// If applicable, insert delivered amount
if (txnType == ttPAYMENT || txnType == ttCHECK_CASH)
{
RPC::insertDeliveredAmount(
txJson[jss::meta],
fill.ledger,
txn,
{txn->getTransactionID(), fill.ledger.seq(), *stMeta});
}

// If applicable, insert mpt issuance id
RPC::insertMPTokenIssuanceID(
txJson[jss::meta], txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta});
// Insert all synthetic fields
RPC::insertAllSyntheticInJson(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above — the ttACCOUNT_DELETE delivered amount behavior change is intentional, making ledger RPC consistent with tx and account_tx endpoints.

txJson[jss::meta],
fill.ledger,
txn,
{txn->getTransactionID(), fill.ledger.seq(), *stMeta});
}

if (!fill.ledger.open())
Expand All @@ -157,19 +150,12 @@ fillJsonTx(
{
txJson[jss::metaData] = stMeta->getJson(JsonOptions::none);

// If applicable, insert delivered amount
if (txnType == ttPAYMENT || txnType == ttCHECK_CASH)
{
RPC::insertDeliveredAmount(
txJson[jss::metaData],
fill.ledger,
txn,
{txn->getTransactionID(), fill.ledger.seq(), *stMeta});
}

// If applicable, insert mpt issuance id
RPC::insertMPTokenIssuanceID(
txJson[jss::metaData], txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta});
// Insert all synthetic fields
RPC::insertAllSyntheticInJson(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a change in behaviour. We'll insert delivered amount for ttACCOUNT_DELETE after the change, as per canHaveDeliveredAmount

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that a problem? That seems like a fix to me

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is an intentional behavioral change. The old ttPAYMENT || ttCHECK_CASH conditional in LedgerToJson.cpp was outdated — canHaveDeliveredAmount (used internally by insertDeliveredAmount) already correctly handles ttACCOUNT_DELETE, and the other RPC endpoints (tx, account_tx) have always included delivered amount for ttACCOUNT_DELETE transactions. This change makes ledger RPC consistent with the rest.

txJson[jss::metaData],
fill.ledger,
txn,
{txn->getTransactionID(), fill.ledger.seq(), *stMeta});
}
}

Expand Down
7 changes: 2 additions & 5 deletions src/xrpld/app/misc/NetworkOPs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@
#include <xrpld/overlay/predicates.h>
#include <xrpld/rpc/BookChanges.h>
#include <xrpld/rpc/CTID.h>
#include <xrpld/rpc/DeliveredAmount.h>
#include <xrpld/rpc/MPTokenIssuanceID.h>
#include <xrpld/rpc/ServerHandler.h>
#include <xrpld/rpc/detail/SyntheticFields.h>

#include <xrpl/basics/UptimeClock.h>
#include <xrpl/basics/mulDiv.h>
Expand Down Expand Up @@ -3130,9 +3129,7 @@ NetworkOPsImp::transJson(
if (meta)
{
jvObj[jss::meta] = meta->get().getJson(JsonOptions::none);
RPC::insertDeliveredAmount(jvObj[jss::meta], *ledger, transaction, meta->get());
RPC::insertNFTSyntheticInJson(jvObj, transaction, meta->get());
RPC::insertMPTokenIssuanceID(jvObj[jss::meta], transaction, meta->get());
RPC::insertAllSyntheticInJson(jvObj[jss::meta], *ledger, transaction, meta->get());
}

// add CTID where the needed data for it exists
Expand Down
37 changes: 37 additions & 0 deletions src/xrpld/rpc/detail/SyntheticFields.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include <xrpld/rpc/DeliveredAmount.h>
#include <xrpld/rpc/MPTokenIssuanceID.h>
#include <xrpld/rpc/detail/SyntheticFields.h>

#include <xrpl/json/json_value.h>
#include <xrpl/protocol/NFTSyntheticSerializer.h>
#include <xrpl/protocol/jss.h>

namespace xrpl {
namespace RPC {

void
insertAllSyntheticInJson(
Json::Value& metadata,
ReadView const& ledger,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta)
{
insertDeliveredAmount(metadata, ledger, transaction, transactionMeta);
insertNFTSyntheticInJson(metadata, transaction, transactionMeta);
insertMPTokenIssuanceID(metadata, transaction, transactionMeta);
}

void
insertAllSyntheticInJson(
Json::Value& metadata,
JsonContext const& context,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta)
{
insertDeliveredAmount(metadata, context, transaction, transactionMeta);
insertNFTSyntheticInJson(metadata, transaction, transactionMeta);
insertMPTokenIssuanceID(metadata, transaction, transactionMeta);
}

} // namespace RPC
} // namespace xrpl
Loading
Loading