Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2ccd108
Initial plan
Copilot Aug 20, 2025
ed7167c
Fix missing nftoken_id in ledger RPC response
Copilot Aug 20, 2025
e260651
Update NFToken test helpers to include ledger and account_tx RPC checks
Copilot Aug 20, 2025
72bb19c
Fix clang-format violations
Copilot Aug 20, 2025
14ff58a
Fix NFT synthetic field insertion for different API versions
Copilot Aug 20, 2025
2e4b30d
try adding copilot-setup-steps
mvadari Aug 21, 2025
2f7cbc4
messed up the file
mvadari Aug 21, 2025
3d8c1a3
oops
mvadari Aug 21, 2025
a8d5d99
actual setup steps
mvadari Aug 21, 2025
f6d15f3
try again
mvadari Aug 21, 2025
ac16c83
fix push so it doesn't run twice
mvadari Aug 21, 2025
c491adc
Fix NFT test failures by handling API version metadata field differences
Copilot Aug 21, 2025
649fd10
[Claude] add folder structure document
mvadari Aug 21, 2025
376bca2
[Claude] get tests working
mvadari Aug 22, 2025
de84c82
[Claude] simplify tests
mvadari Aug 22, 2025
45fdb1b
[Claude] replace with a helper function
mvadari Aug 22, 2025
e79416b
remove copilot-setup-steps
mvadari Aug 22, 2025
7e2dcbd
remove folder structure file
mvadari Aug 22, 2025
539074b
[Claude] fix tests
mvadari Aug 22, 2025
ee3cba2
pre-commit fixes
mvadari Aug 22, 2025
bb70423
Merge branch 'develop' into copilot/fix-5adea215-d850-4ab8-a595-b04e6…
mvadari Sep 11, 2025
4063cca
refactor: replace individual synthetic field functions with insertAll…
Copilot Sep 11, 2025
3bb3f1b
Merge branch 'develop' into copilot/fix-5adea215-d850-4ab8-a595-b04e6…
mvadari Sep 23, 2025
ec01e14
Merge branch 'develop' into copilot/fix-5adea215-d850-4ab8-a595-b04e6…
mvadari Oct 6, 2025
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
4 changes: 2 additions & 2 deletions include/xrpl/protocol/NFTSyntheticSerializer.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ namespace ripple {
namespace RPC {

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

@{
*/
void
insertNFTSyntheticInJson(
Json::Value&,
Json::Value& metadata,
std::shared_ptr<STTx const> const&,
TxMeta const&);
/** @} */
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 @@ -32,12 +32,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
259 changes: 217 additions & 42 deletions src/test/app/NFToken_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6894,60 +6894,235 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
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 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
// 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()};
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];
env.close();

// Expect nftokens_ids field and verify the values
if (!BEAST_EXPECT(meta.isMember(jss::nftoken_ids)))
return;
// Test 1: Check tx RPC response
Json::Value const txResult = env.rpc("tx", txHash)[jss::result];
Json::Value const& txMeta = txResult[jss::meta];

// 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]);
};
// Expect nftokens_ids field and verify the values
if (!BEAST_EXPECT(txMeta.isMember(jss::nftoken_ids)))
return;

// Convert NFT IDs from Json::Value to uint256
std::vector<uint256> metaIDs;
std::transform(
txMeta[jss::nftoken_ids].begin(),
txMeta[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]);

// 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 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
// changed in the most recent NFTokenCreateOffer tx
Expand Down
30 changes: 8 additions & 22 deletions src/xrpld/app/ledger/detail/LedgerToJson.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
#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/protocol/ApiVersion.h>
#include <xrpl/protocol/NFTSyntheticSerializer.h>
#include <xrpl/protocol/jss.h>

namespace ripple {
Expand Down Expand Up @@ -150,17 +150,10 @@ 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(
// Insert all synthetic fields
RPC::insertAllSyntheticInJson(
txJson[jss::meta],
fill.ledger,
txn,
{txn->getTransactionID(), fill.ledger.seq(), *stMeta});
}
Expand All @@ -187,17 +180,10 @@ 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(
// Insert all synthetic fields
RPC::insertAllSyntheticInJson(
txJson[jss::metaData],
fill.ledger,
txn,
{txn->getTransactionID(), fill.ledger.seq(), *stMeta});
}
Expand Down
8 changes: 2 additions & 6 deletions src/xrpld/app/misc/NetworkOPs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@
#include <xrpld/perflog/PerfLog.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 @@ -3269,11 +3268,8 @@ NetworkOPsImp::transJson(
if (meta)
{
jvObj[jss::meta] = meta->get().getJson(JsonOptions::none);
RPC::insertDeliveredAmount(
RPC::insertAllSyntheticInJson(
jvObj[jss::meta], *ledger, transaction, meta->get());
RPC::insertNFTSyntheticInJson(jvObj, transaction, meta->get());
RPC::insertMPTokenIssuanceID(
jvObj[jss::meta], transaction, meta->get());
}

// add CTID where the needed data for it exists
Expand Down
Loading
Loading