From 7e40269bc25ae5577c6d038d0da52f1afffce133 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Thu, 21 Dec 2023 11:57:35 -0500 Subject: [PATCH 1/4] Meta field and APIs --- Builds/CMake/RippledCore.cmake | 2 + src/ripple/app/ledger/impl/LedgerToJson.cpp | 15 ++ src/ripple/app/misc/NetworkOPs.cpp | 3 + .../app/tx/impl/CFTokenIssuanceCreate.cpp | 9 +- src/ripple/protocol/Indexes.h | 8 +- src/ripple/protocol/impl/Indexes.cpp | 4 +- src/ripple/protocol/impl/Issue.cpp | 6 +- src/ripple/protocol/impl/LedgerFormats.cpp | 1 + src/ripple/protocol/impl/STAmount.cpp | 6 +- src/ripple/protocol/jss.h | 8 +- src/ripple/rpc/CFTokenIssuanceID.h | 53 ++++++ src/ripple/rpc/handlers/AccountObjects.cpp | 4 +- src/ripple/rpc/handlers/AccountTx.cpp | 2 + src/ripple/rpc/handlers/CFTHolders.cpp | 160 ++++++++++++++++++ src/ripple/rpc/handlers/Handlers.h | 2 + src/ripple/rpc/handlers/LedgerEntry.cpp | 20 +++ src/ripple/rpc/handlers/Tx.cpp | 2 + src/ripple/rpc/impl/CFTokenIssuanceID.cpp | 81 +++++++++ src/ripple/rpc/impl/Handler.cpp | 1 + src/ripple/rpc/impl/RPCHelpers.cpp | 12 +- src/ripple/rpc/impl/Tuning.h | 3 + src/test/rpc/AccountObjects_test.cpp | 47 ++++- 22 files changed, 429 insertions(+), 20 deletions(-) create mode 100644 src/ripple/rpc/CFTokenIssuanceID.h create mode 100644 src/ripple/rpc/handlers/CFTHolders.cpp create mode 100644 src/ripple/rpc/impl/CFTokenIssuanceID.cpp diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 4ac9dc250eb..0ff8c83bd30 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -683,6 +683,7 @@ target_sources (rippled PRIVATE src/ripple/rpc/handlers/BlackList.cpp src/ripple/rpc/handlers/BookOffers.cpp src/ripple/rpc/handlers/CanDelete.cpp + src/ripple/rpc/handlers/CFTHolders.cpp src/ripple/rpc/handlers/Connect.cpp src/ripple/rpc/handlers/ConsensusInfo.cpp src/ripple/rpc/handlers/CrawlShards.cpp @@ -737,6 +738,7 @@ target_sources (rippled PRIVATE src/ripple/rpc/handlers/ValidatorListSites.cpp src/ripple/rpc/handlers/Validators.cpp src/ripple/rpc/handlers/WalletPropose.cpp + src/ripple/rpc/impl/CFTokenIssuanceID.cpp src/ripple/rpc/impl/DeliveredAmount.cpp src/ripple/rpc/impl/Handler.cpp src/ripple/rpc/impl/LegacyPathFind.cpp diff --git a/src/ripple/app/ledger/impl/LedgerToJson.cpp b/src/ripple/app/ledger/impl/LedgerToJson.cpp index 9cef4e65b56..e9b7aa0dcf5 100644 --- a/src/ripple/app/ledger/impl/LedgerToJson.cpp +++ b/src/ripple/app/ledger/impl/LedgerToJson.cpp @@ -28,6 +28,7 @@ #include #include #include +#include namespace ripple { @@ -157,6 +158,13 @@ fillJsonTx( fill.ledger, txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); + + // If applicable, insert cft issuance id + if (txnType == ttCFTOKEN_ISSUANCE_CREATE) + RPC::insertCFTokenIssuanceID( + txJson[jss::meta], + txn, + {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } if (!fill.ledger.open()) @@ -190,6 +198,13 @@ fillJsonTx( fill.ledger, txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); + + // If applicable, insert cft issuance id + if (txnType == ttCFTOKEN_ISSUANCE_CREATE) + RPC::insertCFTokenIssuanceID( + txJson[jss::metaData], + txn, + {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } } diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 7ed7694a5b5..5c563a99306 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -72,6 +72,7 @@ #include #include #include +#include #include #include @@ -3117,6 +3118,8 @@ NetworkOPsImp::transJson( jvObj[jss::meta] = meta->get().getJson(JsonOptions::none); RPC::insertDeliveredAmount( jvObj[jss::meta], *ledger, transaction, meta->get()); + RPC::insertCFTokenIssuanceID( + jvObj[jss::meta], transaction, meta->get()); } if (!ledger->open()) diff --git a/src/ripple/app/tx/impl/CFTokenIssuanceCreate.cpp b/src/ripple/app/tx/impl/CFTokenIssuanceCreate.cpp index 38349231fa2..706d8757c8c 100644 --- a/src/ripple/app/tx/impl/CFTokenIssuanceCreate.cpp +++ b/src/ripple/app/tx/impl/CFTokenIssuanceCreate.cpp @@ -74,25 +74,26 @@ CFTokenIssuanceCreate::doApply() if (mPriorBalance < view().fees().accountReserve((*acct)[sfOwnerCount] + 1)) return tecINSUFFICIENT_RESERVE; - auto const cftIssuanceID = + auto const cftIssuanceKeylet = keylet::cftIssuance(account_, ctx_.tx.getSeqProxy().value()); // create the CFTokenIssuance { auto const ownerNode = view().dirInsert( keylet::ownerDir(account_), - cftIssuanceID, + cftIssuanceKeylet, describeOwnerDir(account_)); if (!ownerNode) return tecDIR_FULL; - auto cftIssuance = std::make_shared(cftIssuanceID); + auto cftIssuance = std::make_shared(cftIssuanceKeylet); (*cftIssuance)[sfFlags] = ctx_.tx.getFlags() & ~tfUniversal; (*cftIssuance)[sfIssuer] = account_; (*cftIssuance)[sfOutstandingAmount] = 0; (*cftIssuance)[sfOwnerNode] = *ownerNode; - + (*cftIssuance)[sfSequence] = ctx_.tx.getSeqProxy().value(); + if (auto const max = ctx_.tx[~sfMaximumAmount]) (*cftIssuance)[sfMaximumAmount] = *max; diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 3ddec4adb03..810d3a5f901 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -304,8 +304,14 @@ cftoken(CFT const& issuanceID, AccountID const& holder) noexcept; Keylet cftoken(uint192 const& issuanceID, AccountID const& holder) noexcept; +inline Keylet +cftoken(uint256 const& cftokenKey) +{ + return {ltCFTOKEN, cftokenKey}; +} + Keylet -cftoken(uint256 const& issuanceID, AccountID const& holder) noexcept; +cftoken(uint256 const& issuanceKey, AccountID const& holder) noexcept; Keylet cft_dir(uint192 const& id) noexcept; diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 96439518f10..9d813d596e8 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -490,9 +490,9 @@ cftoken(CFT const& cftID, AccountID const& holder) noexcept } Keylet -cftoken(uint256 const& issuanceID, AccountID const& holder) noexcept +cftoken(uint256 const& issuanceKey, AccountID const& holder) noexcept { - return {ltCFTOKEN, indexHash(LedgerNameSpace::CFTOKEN, issuanceID, holder)}; + return {ltCFTOKEN, indexHash(LedgerNameSpace::CFTOKEN, issuanceKey, holder)}; } Keylet diff --git a/src/ripple/protocol/impl/Issue.cpp b/src/ripple/protocol/impl/Issue.cpp index 123b248d447..c0f57a6887c 100644 --- a/src/ripple/protocol/impl/Issue.cpp +++ b/src/ripple/protocol/impl/Issue.cpp @@ -75,7 +75,7 @@ to_json(Issue const& is) { Json::Value jv; if (is.asset().isCFT()) - jv[jss::cft_asset_id] = to_string(is.asset()); + jv[jss::cft_issuance_id] = to_string(is.asset()); else jv[jss::currency] = to_string(is.asset()); if (!isXRP(is.asset()) && !is.asset().isCFT()) @@ -92,10 +92,10 @@ issueFromJson(Json::Value const& v) "issueFromJson can only be specified with an 'object' Json value"); } - bool const isCFT = v.isMember(jss::cft_asset_id); + bool const isCFT = v.isMember(jss::cft_issuance_id); Json::Value const assetStr = - isCFT ? v[jss::cft_asset_id] : v[jss::currency]; + isCFT ? v[jss::cft_issuance_id] : v[jss::currency]; Json::Value const issStr = v[jss::issuer]; if (!assetStr.isString()) diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 1ec96abc004..793a6d7b95f 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -345,6 +345,7 @@ LedgerFormats::LedgerFormats() ltCFTOKEN_ISSUANCE, { {sfIssuer, soeREQUIRED}, + {sfSequence, soeREQUIRED}, {sfTransferFee, soeDEFAULT}, {sfOwnerNode, soeREQUIRED}, {sfAssetScale, soeDEFAULT}, diff --git a/src/ripple/protocol/impl/STAmount.cpp b/src/ripple/protocol/impl/STAmount.cpp index b0e0133a8ff..de3e0083379 100644 --- a/src/ripple/protocol/impl/STAmount.cpp +++ b/src/ripple/protocol/impl/STAmount.cpp @@ -598,7 +598,7 @@ STAmount::setJson(Json::Value& elem) const // json. elem[jss::value] = getText(); if (mIssue.isCFT()) - elem[jss::cft_asset_id] = to_string(mIssue.asset()); + elem[jss::cft_issuance_id] = to_string(mIssue.asset()); else { elem[jss::currency] = to_string(mIssue.asset()); @@ -1033,10 +1033,10 @@ amountFromJson(SField const& name, Json::Value const& v) else if (v.isObject()) { value = v[jss::value]; - if (v.isMember(jss::cft_asset_id)) + if (v.isMember(jss::cft_issuance_id)) { isCFT = true; - asset = v[jss::cft_asset_id]; + asset = v[jss::cft_issuance_id]; } else { diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 13bb16db3ce..632cae5d8a8 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -227,7 +227,11 @@ JSS(build_path); // in: TransactionSign JSS(build_version); // out: NetworkOPs JSS(cancel_after); // out: AccountChannels JSS(can_delete); // out: CanDelete -JSS(cft_asset_id); // in: Payment +JSS(cft_amount); // out: cft_holders +JSS(cft_issuance); // in: LedgerEntry, AccountObjects +JSS(cft_issuance_id); // in: Payment, cft_holders +JSS(cftoken); // in: LedgerEntry, AccountObjects +JSS(cftoken_index); // out: cft_holders JSS(changes); // out: BookChanges JSS(channel_id); // out: AccountChannels JSS(channels); // out: AccountChannels @@ -359,6 +363,7 @@ JSS(high); // out: BookChanges JSS(highest_sequence); // out: AccountInfo JSS(highest_ticket); // out: AccountInfo JSS(historical_perminute); // historical_perminute. +JSS(holders); // out: CFTHolders JSS(hostid); // out: NetworkOPs JSS(hotwallet); // in: GatewayBalances JSS(id); // websocket. @@ -446,6 +451,7 @@ JSS(load_fee); // out: LoadFeeTrackImp, NetworkOPs JSS(local); // out: resource/Logic.h JSS(local_txs); // out: GetCounts JSS(local_static_keys); // out: ValidatorList +JSS(locked_amount); // out: CFTHolders JSS(low); // out: BookChanges JSS(lowest_sequence); // out: AccountInfo JSS(lowest_ticket); // out: AccountInfo diff --git a/src/ripple/rpc/CFTokenIssuanceID.h b/src/ripple/rpc/CFTokenIssuanceID.h new file mode 100644 index 00000000000..6ce3215b529 --- /dev/null +++ b/src/ripple/rpc/CFTokenIssuanceID.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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_CFTOKENISSUANCEID_H_INCLUDED +#define RIPPLE_RPC_CFTOKENISSUANCEID_H_INCLUDED + +#include +#include +#include +#include + +#include +#include + +namespace ripple { + +namespace RPC { + +bool +canHaveCFTokenIssuanceID( + std::shared_ptr const& serializedTx, + TxMeta const& transactionMeta); + +std::optional +getIDFromCreatedIssuance(TxMeta const& transactionMeta); + +void +insertCFTokenIssuanceID( + Json::Value& response, + std::shared_ptr const& transaction, + TxMeta const& transactionMeta); +/** @} */ + +} // namespace RPC +} // namespace ripple + +#endif diff --git a/src/ripple/rpc/handlers/AccountObjects.cpp b/src/ripple/rpc/handlers/AccountObjects.cpp index bbf5b6e126a..a507774b4ce 100644 --- a/src/ripple/rpc/handlers/AccountObjects.cpp +++ b/src/ripple/rpc/handlers/AccountObjects.cpp @@ -201,7 +201,9 @@ doAccountObjects(RPC::JsonContext& context) {jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID}, {jss::xchain_owned_create_account_claim_id, ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, - {jss::bridge, ltBRIDGE}}; + {jss::bridge, ltBRIDGE}, + {jss::cft_issuance, ltCFTOKEN_ISSUANCE}, + {jss::cftoken, ltCFTOKEN}}; typeFilter.emplace(); typeFilter->reserve(std::size(deletionBlockers)); diff --git a/src/ripple/rpc/handlers/AccountTx.cpp b/src/ripple/rpc/handlers/AccountTx.cpp index 40395aae32f..37ff5db25ee 100644 --- a/src/ripple/rpc/handlers/AccountTx.cpp +++ b/src/ripple/rpc/handlers/AccountTx.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include @@ -361,6 +362,7 @@ populateJsonResponse( insertDeliveredAmount( jvObj[jss::meta], context, txn, *txnMeta); insertNFTSyntheticInJson(jvObj, sttx, *txnMeta); + RPC::insertCFTokenIssuanceID(jvObj[jss::meta], sttx, *txnMeta); } else assert(false && "Missing transaction medatata"); diff --git a/src/ripple/rpc/handlers/CFTHolders.cpp b/src/ripple/rpc/handlers/CFTHolders.cpp new file mode 100644 index 00000000000..700bca19701 --- /dev/null +++ b/src/ripple/rpc/handlers/CFTHolders.cpp @@ -0,0 +1,160 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +static void +appendCFTHolderJson( + Application const& app, + std::shared_ptr const& cft, + Json::Value& holders) +{ + Json::Value& obj(holders.append(Json::objectValue)); + + obj[jss::cftoken_index] = to_string(cft->key()); + obj[jss::flags] = (*cft)[sfFlags]; + obj[jss::account] = toBase58(cft->getAccountID(sfAccount)); + obj[jss::cft_amount] = STUInt64{(*cft)[sfCFTAmount]}.getJson(JsonOptions::none); + + if((*cft)[sfLockedAmount]) + obj[jss::locked_amount] = STUInt64{(*cft)[sfLockedAmount]}.getJson(JsonOptions::none); +} + +// { +// cft_issuance_id: +// ledger_hash : +// ledger_index : +// limit: integer // optional +// marker: opaque // optional, resume previous query +// } +static Json::Value +enumerateCFTHolders( + RPC::JsonContext& context, + uint192 const& cftIssuanceID, + Keylet const& directory) +{ + unsigned int limit; + if (auto err = readLimitField(limit, RPC::Tuning::cftHolders, context)) + return *err; + + std::shared_ptr ledger; + + if (auto result = RPC::lookupLedger(ledger, context); !ledger) + return result; + + if (!ledger->exists(directory)) + return rpcError(rpcOBJECT_NOT_FOUND); + + Json::Value result; + result[jss::cft_issuance_id] = to_string(cftIssuanceID); + + Json::Value& jsonHolders(result[jss::holders] = Json::arrayValue); + + std::vector> holders; + unsigned int reserve(limit); + uint256 startAfter; + std::uint64_t startHint = 0; + + if (context.params.isMember(jss::marker)) + { + // We have a start point. Use limit - 1 from the result and use the + // very last one for the resume. + Json::Value const& marker(context.params[jss::marker]); + + if (!marker.isString()) + return RPC::expected_field_error(jss::marker, "string"); + + if (!startAfter.parseHex(marker.asString())) + return rpcError(rpcINVALID_PARAMS); + + auto const sle = ledger->read(keylet::cftoken(startAfter)); + + if (!sle || cftIssuanceID != sle->getFieldH192(sfCFTokenIssuanceID)) + return rpcError(rpcINVALID_PARAMS); + + startHint = sle->getFieldU64(sfCFTokenNode); + appendCFTHolderJson(context.app, sle, jsonHolders); + holders.reserve(reserve); + } + else + { + // We have no start point, limit should be one higher than requested. + holders.reserve(++reserve); + } + + if (!forEachItemAfter( + *ledger, + directory, + startAfter, + startHint, + reserve, + [&holders](std::shared_ptr const& cftoken) { + if (cftoken->getType() == ltCFTOKEN) + { + holders.emplace_back(cftoken); + return true; + } + + return false; + })) + { + return rpcError(rpcINVALID_PARAMS); + } + + if (holders.size() == reserve) + { + result[jss::limit] = limit; + result[jss::marker] = to_string(holders.back()->key()); + holders.pop_back(); + } + + for (auto const& cft : holders) + appendCFTHolderJson(context.app, cft, jsonHolders); + + context.loadType = Resource::feeMediumBurdenRPC; + return result; +} + +Json::Value +doCFTHolders(RPC::JsonContext& context) +{ + if (!context.params.isMember(jss::cft_issuance_id)) + return RPC::missing_field_error(jss::cft_issuance_id); + + uint192 cftIssuanceID; + + if (!cftIssuanceID.parseHex(context.params[jss::cft_issuance_id].asString())) + return RPC::invalid_field_error(jss::cft_issuance_id); + + return enumerateCFTHolders(context, cftIssuanceID, keylet::cft_dir(cftIssuanceID)); +} + +} // namespace ripple diff --git a/src/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h index ba93be54513..c9bab0ba897 100644 --- a/src/ripple/rpc/handlers/Handlers.h +++ b/src/ripple/rpc/handlers/Handlers.h @@ -51,6 +51,8 @@ doBlackList(RPC::JsonContext&); Json::Value doCanDelete(RPC::JsonContext&); Json::Value +doCFTHolders(RPC::JsonContext&); +Json::Value doChannelAuthorize(RPC::JsonContext&); Json::Value doChannelVerify(RPC::JsonContext&); diff --git a/src/ripple/rpc/handlers/LedgerEntry.cpp b/src/ripple/rpc/handlers/LedgerEntry.cpp index baff721cc1f..2cc1a69c38a 100644 --- a/src/ripple/rpc/handlers/LedgerEntry.cpp +++ b/src/ripple/rpc/handlers/LedgerEntry.cpp @@ -598,6 +598,26 @@ doLedgerEntry(RPC::JsonContext& context) else uNodeIndex = keylet::did(*account).key; } + else if (context.params.isMember(jss::cft_issuance_id)) + { + expectedType = ltCFTOKEN_ISSUANCE; + auto const unparsedCFTIssuanceID = context.params[jss::cft_issuance_id]; + if (unparsedCFTIssuanceID.isString()) + { + uint192 cftIssuanceID; + if (!cftIssuanceID.parseHex(unparsedCFTIssuanceID.asString())) + { + uNodeIndex = beast::zero; + jvResult[jss::error] = "malformedRequest"; + } + else + uNodeIndex = keylet::cftIssuance(cftIssuanceID).key; + } + else + { + jvResult[jss::error] = "malformedRequest"; + } + } else { if (context.params.isMember("params") && diff --git a/src/ripple/rpc/handlers/Tx.cpp b/src/ripple/rpc/handlers/Tx.cpp index 0237fef22ac..e2ed6fc2cf2 100644 --- a/src/ripple/rpc/handlers/Tx.cpp +++ b/src/ripple/rpc/handlers/Tx.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -385,6 +386,7 @@ populateJsonResponse( insertDeliveredAmount( response[jss::meta], context, result.txn, *meta); insertNFTSyntheticInJson(response, sttx, *meta); + RPC::insertCFTokenIssuanceID(response[jss::meta], sttx, *meta); } } response[jss::validated] = result.validated; diff --git a/src/ripple/rpc/impl/CFTokenIssuanceID.cpp b/src/ripple/rpc/impl/CFTokenIssuanceID.cpp new file mode 100644 index 00000000000..280c4af4cf3 --- /dev/null +++ b/src/ripple/rpc/impl/CFTokenIssuanceID.cpp @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include +#include +#include +#include + +namespace ripple { + +namespace RPC { + +bool +canHaveCFTokenIssuanceID( + std::shared_ptr const& serializedTx, + TxMeta const& transactionMeta) +{ + if (!serializedTx) + return false; + + TxType const tt = serializedTx->getTxnType(); + if (tt != ttCFTOKEN_ISSUANCE_CREATE) + return false; + + // if the transaction failed nothing could have been delivered. + if (transactionMeta.getResultTER() != tesSUCCESS) + return false; + + return true; +} + +std::optional +getIDFromCreatedIssuance(TxMeta const& transactionMeta) +{ + for (STObject const& node : transactionMeta.getNodes()) + { + if (node.getFieldU16(sfLedgerEntryType) != ltCFTOKEN_ISSUANCE || + node.getFName() != sfCreatedNode) + continue; + + auto const& cftNode = node.peekAtField(sfNewFields).downcast(); + return getCftID(cftNode.getAccountID(sfIssuer), cftNode.getFieldU32(sfSequence)); + } + + return std::nullopt; +} + +void +insertCFTokenIssuanceID( + Json::Value& response, + std::shared_ptr const& transaction, + TxMeta const& transactionMeta) +{ + if (!canHaveCFTokenIssuanceID(transaction, transactionMeta)) + return; + + std::optional result = getIDFromCreatedIssuance(transactionMeta); + if (result.has_value()) + response[jss::cft_issuance_id] = to_string(result.value()); +} + +} // namespace RPC +} // namespace ripple diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index d05c3279800..acf13df0a7b 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -95,6 +95,7 @@ Handler const handlerArray[]{ {"book_changes", byRef(&doBookChanges), Role::USER, NO_CONDITION}, {"book_offers", byRef(&doBookOffers), Role::USER, NO_CONDITION}, {"can_delete", byRef(&doCanDelete), Role::ADMIN, NO_CONDITION}, + {"cft_holders", byRef(&doCFTHolders), Role::USER, NO_CONDITION}, {"channel_authorize", byRef(&doChannelAuthorize), Role::USER, NO_CONDITION}, {"channel_verify", byRef(&doChannelVerify), Role::USER, NO_CONDITION}, {"connect", byRef(&doConnect), Role::ADMIN, NO_CONDITION}, diff --git a/src/ripple/rpc/impl/RPCHelpers.cpp b/src/ripple/rpc/impl/RPCHelpers.cpp index 672095fe950..1a85a68d13c 100644 --- a/src/ripple/rpc/impl/RPCHelpers.cpp +++ b/src/ripple/rpc/impl/RPCHelpers.cpp @@ -285,7 +285,11 @@ getAccountObjects( if (!typeFilter.has_value() || typeMatchesFilter(typeFilter.value(), sleNode->getType())) { - jvObjects.append(sleNode->getJson(JsonOptions::none)); + auto sleJson = sleNode->getJson(JsonOptions::none); + if (sleNode->getType() == ltCFTOKEN_ISSUANCE) + sleJson[jss::cft_issuance_id] = to_string(getCftID(sleNode->getAccountID(sfIssuer), (*sleNode)[sfSequence])); + + jvObjects.append(sleJson); } if (++i == mlimit) @@ -934,7 +938,7 @@ chooseLedgerEntryType(Json::Value const& params) std::pair result{RPC::Status::OK, ltANY}; if (params.isMember(jss::type)) { - static constexpr std::array, 20> + static constexpr std::array, 22> types{ {{jss::account, ltACCOUNT_ROOT}, {jss::amendments, ltAMENDMENTS}, @@ -956,7 +960,9 @@ chooseLedgerEntryType(Json::Value const& params) {jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID}, {jss::xchain_owned_create_account_claim_id, ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, - {jss::did, ltDID}}}; + {jss::did, ltDID}, + {jss::cft_issuance, ltCFTOKEN_ISSUANCE}, + {jss::cftoken, ltCFTOKEN}}}; auto const& p = params[jss::type]; if (!p.isString()) diff --git a/src/ripple/rpc/impl/Tuning.h b/src/ripple/rpc/impl/Tuning.h index 4f4a8be1bf7..e0f2a5a0bd3 100644 --- a/src/ripple/rpc/impl/Tuning.h +++ b/src/ripple/rpc/impl/Tuning.h @@ -57,6 +57,9 @@ static LimitRange constexpr accountNFTokens = {20, 100, 400}; /** Limits for the nft_buy_offers & nft_sell_offers commands. */ static LimitRange constexpr nftOffers = {50, 250, 500}; +/** Limits for the nft_buy_offers & nft_sell_offers commands. */ +static LimitRange constexpr cftHolders = {10, 200, 400}; + static int constexpr defaultAutoFillFeeMultiplier = 10; static int constexpr defaultAutoFillFeeDivisor = 1; static int constexpr maxPathfindsInProgress = 2; diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 17217f2c880..93a9b157f52 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -600,6 +600,8 @@ class AccountObjects_test : public beast::unit_test::suite BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::ticket), 0)); BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::amm), 0)); BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::did), 0)); + BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::cft_issuance), 0)); + BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::cftoken), 0)); // gw mints an NFT so we can find it. uint256 const nftID{token::getNextID(env, gw, 0u, tfTransferable)}; @@ -875,6 +877,45 @@ class AccountObjects_test : public beast::unit_test::suite BEAST_EXPECT(did[sfAccount.jsonName] == gw.human()); BEAST_EXPECT(did[sfURI.jsonName] == strHex(std::string{"uri"})); } + { + // gw creates a CFTokenIssuance that we can look for in the ledger. + Json::Value jvCFTIssuance; + jvCFTIssuance[jss::TransactionType] = jss::CFTokenIssuanceCreate; + jvCFTIssuance[jss::Flags] = tfUniversal; + jvCFTIssuance[jss::Account] = gw.human(); + jvCFTIssuance[sfCFTokenMetadata.jsonName] = strHex(std::string{"metadata"}); + env(jvCFTIssuance); + env.close(); + } + { + // Find the CFTokenIssuance. + Json::Value const resp = acct_objs(gw, jss::cft_issuance); + BEAST_EXPECT(acct_objs_is_size(resp, 1)); + + auto const& cftIssuance = resp[jss::result][jss::account_objects][0u]; + BEAST_EXPECT(cftIssuance[sfIssuer.jsonName] == gw.human()); + BEAST_EXPECT(cftIssuance[sfCFTokenMetadata.jsonName] == strHex(std::string{"metadata"})); + } + { + // alice creates CFToken that is going to be used by gw + auto const issuanceID = getCftID(alice, env.seq(alice)); + env(cft::create(alice)); + env.close(); + + // gw creates a CFToken that we can look for in the ledger. + Json::Value jvCFToken; + jvCFToken[jss::TransactionType] = jss::CFTokenAuthorize; + jvCFToken[jss::CFTokenIssuanceID] = to_string(issuanceID); + jvCFToken[jss::Account] = gw.human(); + env(jvCFToken); + env.close(); + + // Find the CFToken. + Json::Value const resp = acct_objs(gw, jss::cftoken); + BEAST_EXPECT(acct_objs_is_size(resp, 1)); + auto const& cftoken = resp[jss::result][jss::account_objects][0u]; + BEAST_EXPECT(cftoken[sfCFTokenIssuanceID.jsonName] == to_string(issuanceID)); + } // Make gw multisigning by adding a signerList. env(jtx::signers(gw, 6, {{alice, 7}})); env.close(); @@ -902,7 +943,7 @@ class AccountObjects_test : public beast::unit_test::suite auto const& ticket = resp[jss::result][jss::account_objects][0u]; BEAST_EXPECT(ticket[sfAccount.jsonName] == gw.human()); BEAST_EXPECT(ticket[sfLedgerEntryType.jsonName] == jss::Ticket); - BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == 14); + BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == 16); } { // See how "deletion_blockers_only" handles gw's directory. @@ -917,7 +958,9 @@ class AccountObjects_test : public beast::unit_test::suite jss::Check.c_str(), jss::NFTokenPage.c_str(), jss::RippleState.c_str(), - jss::PayChannel.c_str()}; + jss::PayChannel.c_str(), + jss::CFTokenIssuance.c_str(), + jss::CFToken.c_str()}; std::sort(v.begin(), v.end()); return v; }(); From 0b3d18c259c9a6ceed18d234ea4fac92ab702574 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Wed, 3 Jan 2024 15:29:15 -0500 Subject: [PATCH 2/4] test for APIs --- src/test/app/CFToken_test.cpp | 37 +++++++++++++++++++++++++ src/test/rpc/AccountObjects_test.cpp | 6 +++-- src/test/rpc/LedgerRPC_test.cpp | 40 ++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/test/app/CFToken_test.cpp b/src/test/app/CFToken_test.cpp index 22bc0173bbd..d94cda9f0ad 100644 --- a/src/test/app/CFToken_test.cpp +++ b/src/test/app/CFToken_test.cpp @@ -950,6 +950,37 @@ class CFToken_test : public beast::unit_test::suite BEAST_EXPECT(expectOffers(env, alice, 0)); } + void + testTxJsonMetaFields(FeatureBitset features) + { + // checks synthetically parsed cftissuanceid from `tx` response + // it checks the parsing logic + testcase("Test synthetic fields from tx response"); + + using namespace test::jtx; + + Account const alice{"alice"}; + + Env env{*this, features}; + env.fund(XRP(10000), alice); + env.close(); + + auto const id = getCftID(alice.id(), env.seq(alice)); + + env(cft::create(alice)); + env.close(); + + std::string const txHash{ + env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; + + Json::Value const meta = + env.rpc("tx", txHash)[jss::result][jss::meta]; + + // Expect cft_issuance_id field + BEAST_EXPECT(meta.isMember(jss::cft_issuance_id)); + BEAST_EXPECT(meta[jss::cft_issuance_id] == to_string(id)); + } + public: void run() override @@ -978,6 +1009,12 @@ class CFToken_test : public beast::unit_test::suite // Test CFT Amount is invalid in non-Payment Tx testCFTInvalidInTx(all); + + // Test parsed CFTokenIssuanceID in API response metadata + // TODO: This test exercises the parsing logic of cftID in `tx`, but, + // cftID is also parsed in different places like `account_tx`, `subscribe`, `ledger`. + // We should create test for these occurances (lower prioirity). + testTxJsonMetaFields(all); } }; diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 93a9b157f52..dcdbad7a6ef 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -879,6 +879,8 @@ class AccountObjects_test : public beast::unit_test::suite } { // gw creates a CFTokenIssuance that we can look for in the ledger. + auto const id = getCftID(gw, env.seq(gw)); + Json::Value jvCFTIssuance; jvCFTIssuance[jss::TransactionType] = jss::CFTokenIssuanceCreate; jvCFTIssuance[jss::Flags] = tfUniversal; @@ -886,13 +888,13 @@ class AccountObjects_test : public beast::unit_test::suite jvCFTIssuance[sfCFTokenMetadata.jsonName] = strHex(std::string{"metadata"}); env(jvCFTIssuance); env.close(); - } - { + // Find the CFTokenIssuance. Json::Value const resp = acct_objs(gw, jss::cft_issuance); BEAST_EXPECT(acct_objs_is_size(resp, 1)); auto const& cftIssuance = resp[jss::result][jss::account_objects][0u]; + BEAST_EXPECT(cftIssuance[jss::cft_issuance_id] == to_string(id)); BEAST_EXPECT(cftIssuance[sfIssuer.jsonName] == gw.human()); BEAST_EXPECT(cftIssuance[sfCFTokenMetadata.jsonName] == strHex(std::string{"metadata"})); } diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 2b4d8527a64..a16409ab92f 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -1581,6 +1581,45 @@ class LedgerRPC_test : public beast::unit_test::suite } } + void + testLedgerEntryCFTIssuanceID() + { + testcase("ledger_entry Request CFTokenIssuance"); + using namespace test::jtx; + using namespace std::literals::chrono_literals; + Env env{*this}; + Account const alice{"alice"}; + + env.fund(XRP(10000), alice); + env.close(); + + auto const id = getCftID(alice, env.seq(alice)); + env(cft::create(alice)); + env.close(); + + std::string const ledgerHash{to_string(env.closed()->info().hash)}; + + { + // Request the CFTokenIssuance using its ID. + Json::Value jvParams; + jvParams[jss::cft_issuance_id] = to_string(id); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][sfIssuer.jsonName] == alice.human()); + } + { + // Request an index that is not a CFTokenIssuacne. + Json::Value jvParams; + jvParams[jss::cft_issuance_id] = ledgerHash; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedRequest", ""); + } + } + void testLedgerEntryInvalidParams(unsigned int apiVersion) { @@ -2304,6 +2343,7 @@ class LedgerRPC_test : public beast::unit_test::suite testQueue(); testLedgerAccountsOption(); testLedgerEntryDID(); + testLedgerEntryCFTIssuanceID(); test::jtx::forAllApiVersions(std::bind_front( &LedgerRPC_test::testLedgerEntryInvalidParams, this)); From a597fcbbed9f2f3e251c016def867b9521872078 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Wed, 3 Jan 2024 17:36:44 -0500 Subject: [PATCH 3/4] Add cft_holder tests --- src/ripple/rpc/handlers/CFTHolders.cpp | 2 +- src/test/app/CFToken_test.cpp | 172 +++++++++++++++++++++++++ src/test/rpc/AccountObjects_test.cpp | 1 + 3 files changed, 174 insertions(+), 1 deletion(-) diff --git a/src/ripple/rpc/handlers/CFTHolders.cpp b/src/ripple/rpc/handlers/CFTHolders.cpp index 700bca19701..870cb4e262b 100644 --- a/src/ripple/rpc/handlers/CFTHolders.cpp +++ b/src/ripple/rpc/handlers/CFTHolders.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2022 Ripple Labs Inc. + Copyright (c) 2024 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 diff --git a/src/test/app/CFToken_test.cpp b/src/test/app/CFToken_test.cpp index d94cda9f0ad..a9dd096aef5 100644 --- a/src/test/app/CFToken_test.cpp +++ b/src/test/app/CFToken_test.cpp @@ -981,6 +981,175 @@ class CFToken_test : public beast::unit_test::suite BEAST_EXPECT(meta[jss::cft_issuance_id] == to_string(id)); } + void + testCFTHoldersAPI(FeatureBitset features) + { + testcase("CFT Holders"); + using namespace test::jtx; + + // a lambda that checks API correctness given different numbers of CFToken + auto checkCFTokens =[&](int expectCount, + int expectMarkerCount, + int line){ + Env env{*this, features}; + Account const alice("alice"); // issuer + + env.fund(XRP(10000), alice); + env.close(); + + auto const id = getCftID(alice.id(), env.seq(alice)); + + env(cft::create(alice)); + env.close(); + + // create accounts that will create CFTokens + for (auto i = 0; i < expectCount; i++) + { + Account const bob{std::string("bob") + std::to_string(i)}; + env.fund(XRP(1000), bob); + env.close(); + + // a holder creates a cftoken + env(cft::authorize(bob, id, std::nullopt)); + env.close(); + } + + // Checks cft_holder query responses + { + int markerCount = 0; + Json::Value allHolders(Json::arrayValue); + std::string marker; + + // The do/while collects results until no marker is returned. + do + { + Json::Value cftHolders = [&env, &id, &marker]() { + Json::Value params; + params[jss::cft_issuance_id] = to_string(id); + + if (!marker.empty()) + params[jss::marker] = marker; + return env.rpc("json", "cft_holders", to_string(params)); + }(); + + // If there are cftokens we get an error + if (expectCount == 0) + { + if (expect( + cftHolders.isMember(jss::result), + "expected \"result\"", + __FILE__, + line)) + { + if (expect( + cftHolders[jss::result].isMember(jss::error), + "expected \"error\"", + __FILE__, + line)) + { + expect( + cftHolders[jss::result][jss::error].asString() == + "objectNotFound", + "expected \"objectNotFound\"", + __FILE__, + line); + } + } + break; + } + + marker.clear(); + if (expect( + cftHolders.isMember(jss::result), + "expected \"result\"", + __FILE__, + line)) + { + Json::Value& result = cftHolders[jss::result]; + + if (result.isMember(jss::marker)) + { + ++markerCount; + marker = result[jss::marker].asString(); + } + + if (expect( + result.isMember(jss::holders), + "expected \"holders\"", + __FILE__, + line)) + { + Json::Value& someHolders = result[jss::holders]; + for (std::size_t i = 0; i < someHolders.size(); ++i) + allHolders.append(someHolders[i]); + } + } + } while (!marker.empty()); + + // Verify the contents of allHolders makes sense. + expect( + allHolders.size() == expectCount, + "Unexpected returned offer count", + __FILE__, + line); + expect( + markerCount == expectMarkerCount, + "Unexpected marker count", + __FILE__, + line); + std::optional globalFlags; + std::set cftIndexes; + std::set holderAddresses; + for (Json::Value const& holder : allHolders) + { + // The flags on all found offers should be the same. + if (!globalFlags) + globalFlags = holder[jss::flags].asInt(); + + expect( + *globalFlags == holder[jss::flags].asInt(), + "Inconsistent flags returned", + __FILE__, + line); + + // The test conditions should produce unique indexes and + // amounts for all holders. + cftIndexes.insert(holder[jss::cftoken_index].asString()); + holderAddresses.insert(holder[jss::account].asString()); + } + + expect( + cftIndexes.size() == expectCount, + "Duplicate indexes returned?", + __FILE__, + line); + expect( + holderAddresses.size() == expectCount, + "Duplicate addresses returned?", + __FILE__, + line); + } + }; + + // Test 1 CFToken + checkCFTokens(1, 0, __LINE__); + + // Test 10 CFTokens + checkCFTokens(10, 0, __LINE__); + + // Test 200 CFTokens + checkCFTokens(200, 0, __LINE__); + + // Test 201 CFTokens + checkCFTokens(201, 1, __LINE__); + + // Test 400 CFTokens + checkCFTokens(400, 1, __LINE__); + + // Test 401 CFTokesn + checkCFTokens(401, 2, __LINE__); + } + public: void run() override @@ -1015,6 +1184,9 @@ class CFToken_test : public beast::unit_test::suite // cftID is also parsed in different places like `account_tx`, `subscribe`, `ledger`. // We should create test for these occurances (lower prioirity). testTxJsonMetaFields(all); + + // Test cft_holders + testCFTHoldersAPI(all); } }; diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index dcdbad7a6ef..fb588426c3c 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -917,6 +917,7 @@ class AccountObjects_test : public beast::unit_test::suite BEAST_EXPECT(acct_objs_is_size(resp, 1)); auto const& cftoken = resp[jss::result][jss::account_objects][0u]; BEAST_EXPECT(cftoken[sfCFTokenIssuanceID.jsonName] == to_string(issuanceID)); + BEAST_EXPECT(cftoken[sfAccount.jsonName] == gw.human()); } // Make gw multisigning by adding a signerList. env(jtx::signers(gw, 6, {{alice, 7}})); From 17a1bd90b574246bc317402974fb3312c59b3935 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Thu, 4 Jan 2024 10:26:38 -0500 Subject: [PATCH 4/4] remove unneeded if-guard --- src/ripple/app/ledger/impl/LedgerToJson.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/ripple/app/ledger/impl/LedgerToJson.cpp b/src/ripple/app/ledger/impl/LedgerToJson.cpp index e9b7aa0dcf5..030b076d275 100644 --- a/src/ripple/app/ledger/impl/LedgerToJson.cpp +++ b/src/ripple/app/ledger/impl/LedgerToJson.cpp @@ -160,11 +160,10 @@ fillJsonTx( {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); // If applicable, insert cft issuance id - if (txnType == ttCFTOKEN_ISSUANCE_CREATE) - RPC::insertCFTokenIssuanceID( - txJson[jss::meta], - txn, - {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); + RPC::insertCFTokenIssuanceID( + txJson[jss::meta], + txn, + {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } if (!fill.ledger.open()) @@ -200,11 +199,10 @@ fillJsonTx( {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); // If applicable, insert cft issuance id - if (txnType == ttCFTOKEN_ISSUANCE_CREATE) - RPC::insertCFTokenIssuanceID( - txJson[jss::metaData], - txn, - {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); + RPC::insertCFTokenIssuanceID( + txJson[jss::metaData], + txn, + {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } }