diff --git a/src/rpc/RPCHelpers.cpp b/src/rpc/RPCHelpers.cpp index a995edcf3..a98f73c8b 100644 --- a/src/rpc/RPCHelpers.cpp +++ b/src/rpc/RPCHelpers.cpp @@ -295,21 +295,26 @@ toJson(ripple::SLE const& sle) } boost::json::object -toJson(ripple::LedgerHeader const& lgrInfo) +toJson(ripple::LedgerHeader const& lgrInfo, bool const binary) { boost::json::object header; - header["ledger_sequence"] = lgrInfo.seq; - header["ledger_hash"] = ripple::strHex(lgrInfo.hash); - header["txns_hash"] = ripple::strHex(lgrInfo.txHash); - header["state_hash"] = ripple::strHex(lgrInfo.accountHash); - header["parent_hash"] = ripple::strHex(lgrInfo.parentHash); - header["total_coins"] = ripple::to_string(lgrInfo.drops); - header["close_flags"] = lgrInfo.closeFlags; - - // Always show fields that contribute to the ledger hash - header["parent_close_time"] = lgrInfo.parentCloseTime.time_since_epoch().count(); - header["close_time"] = lgrInfo.closeTime.time_since_epoch().count(); - header["close_time_resolution"] = lgrInfo.closeTimeResolution.count(); + if (binary) { + header[JS(ledger_data)] = ripple::strHex(ledgerInfoToBlob(lgrInfo)); + } else { + header[JS(account_hash)] = ripple::strHex(lgrInfo.accountHash); + header[JS(close_flags)] = lgrInfo.closeFlags; + header[JS(close_time)] = lgrInfo.closeTime.time_since_epoch().count(); + header[JS(close_time_human)] = ripple::to_string(lgrInfo.closeTime); + header[JS(close_time_resolution)] = lgrInfo.closeTimeResolution.count(); + header[JS(close_time_iso)] = ripple::to_string_iso(lgrInfo.closeTime); + header[JS(ledger_hash)] = ripple::strHex(lgrInfo.hash); + header[JS(ledger_index)] = std::to_string(lgrInfo.seq); + header[JS(parent_close_time)] = lgrInfo.parentCloseTime.time_since_epoch().count(); + header[JS(parent_hash)] = ripple::strHex(lgrInfo.parentHash); + header[JS(total_coins)] = ripple::to_string(lgrInfo.drops); + header[JS(transaction_hash)] = ripple::strHex(lgrInfo.txHash); + } + header[JS(closed)] = true; return header; } @@ -1308,4 +1313,14 @@ isAmendmentEnabled( return std::find(listAmendments.begin(), listAmendments.end(), amendmentId) != listAmendments.end(); } +boost::json::object +toJsonWithBinaryTx(data::TransactionAndMetadata const& txnPlusMeta, std::uint32_t const apiVersion) +{ + boost::json::object obj{}; + auto const metaKey = apiVersion > 1 ? JS(meta_blob) : JS(meta); + obj[metaKey] = ripple::strHex(txnPlusMeta.metadata); + obj[JS(tx_blob)] = ripple::strHex(txnPlusMeta.transaction); + return obj; +} + } // namespace rpc diff --git a/src/rpc/RPCHelpers.h b/src/rpc/RPCHelpers.h index 22d2552f3..b845d5e77 100644 --- a/src/rpc/RPCHelpers.h +++ b/src/rpc/RPCHelpers.h @@ -78,6 +78,16 @@ toExpandedJson( std::optional networkId = std::nullopt ); +/** + * @brief Convert a TransactionAndMetadata to JSON object containing tx and metadata data in hex format. According to + * the apiVersion, the key is "tx_blob" and "meta" or "meta_blob". + * @param txnPlusMeta The TransactionAndMetadata to convert. + * @param apiVersion The api version + * @return The JSON object containing tx and metadata data in hex format. + */ +boost::json::object +toJsonWithBinaryTx(data::TransactionAndMetadata const& txnPlusMeta, std::uint32_t apiVersion); + /** * @brief Add "DeliverMax" which is the alias of "Amount" for "Payment" transaction to transaction json. Remove the * "Amount" field when version is greater than 1 @@ -101,8 +111,15 @@ toJson(ripple::STBase const& obj); boost::json::object toJson(ripple::SLE const& sle); +/** + * @brief Convert a LedgerHeader to JSON object. + * + * @param entry The LedgerHeader to convert. + * @param binary Whether to convert in hex format. + * @return The JSON object. + */ boost::json::object -toJson(ripple::LedgerHeader const& info); +toJson(ripple::LedgerHeader const& info, bool binary); boost::json::object toJson(ripple::TxMeta const& meta); diff --git a/src/rpc/handlers/AccountTx.cpp b/src/rpc/handlers/AccountTx.cpp index 9d8053d6b..a63c610d6 100644 --- a/src/rpc/handlers/AccountTx.cpp +++ b/src/rpc/handlers/AccountTx.cpp @@ -166,11 +166,9 @@ AccountTxHandler::process(AccountTxHandler::Input input, Context const& ctx) con boost::json::object obj; if (!input.binary) { auto [txn, meta] = toExpandedJson(txnPlusMeta, ctx.apiVersion, NFTokenjson::ENABLE); - obj[JS(meta)] = std::move(meta); - obj[JS(tx)] = std::move(txn); - if (obj[JS(tx)].as_object().contains(JS(TransactionType))) { - auto const objTransactionType = obj[JS(tx)].as_object()[JS(TransactionType)]; + if (txn.contains(JS(TransactionType))) { + auto const objTransactionType = txn[JS(TransactionType)]; auto const strType = util::toLower(objTransactionType.as_string().c_str()); // if transactionType does not match @@ -179,14 +177,29 @@ AccountTxHandler::process(AccountTxHandler::Input input, Context const& ctx) con continue; } - obj[JS(tx)].as_object()[JS(date)] = txnPlusMeta.date; - obj[JS(tx)].as_object()[JS(ledger_index)] = txnPlusMeta.ledgerSequence; - - if (ctx.apiVersion < 2u) - obj[JS(tx)].as_object()[JS(inLedger)] = txnPlusMeta.ledgerSequence; + auto const txKey = ctx.apiVersion < 2u ? JS(tx) : JS(tx_json); + obj[JS(meta)] = std::move(meta); + obj[txKey] = std::move(txn); + obj[txKey].as_object()[JS(date)] = txnPlusMeta.date; + obj[txKey].as_object()[JS(ledger_index)] = txnPlusMeta.ledgerSequence; + + if (ctx.apiVersion < 2u) { + obj[txKey].as_object()[JS(inLedger)] = txnPlusMeta.ledgerSequence; + } else { + obj[JS(ledger_index)] = txnPlusMeta.ledgerSequence; + if (obj[txKey].as_object().contains(JS(hash))) { + obj[JS(hash)] = obj[txKey].as_object()[JS(hash)]; + obj[txKey].as_object().erase(JS(hash)); + } + if (auto const ledgerInfo = + sharedPtrBackend_->fetchLedgerBySequence(txnPlusMeta.ledgerSequence, ctx.yield); + ledgerInfo) { + obj[JS(ledger_hash)] = ripple::strHex(ledgerInfo->hash); + obj[JS(close_time_iso)] = ripple::to_string_iso(ledgerInfo->closeTime); + } + } } else { - obj[JS(meta)] = ripple::strHex(txnPlusMeta.metadata); - obj[JS(tx_blob)] = ripple::strHex(txnPlusMeta.transaction); + obj = toJsonWithBinaryTx(txnPlusMeta, ctx.apiVersion); obj[JS(ledger_index)] = txnPlusMeta.ledgerSequence; } diff --git a/src/rpc/handlers/Ledger.cpp b/src/rpc/handlers/Ledger.cpp index aa9764656..5c1ce16c3 100644 --- a/src/rpc/handlers/Ledger.cpp +++ b/src/rpc/handlers/Ledger.cpp @@ -34,24 +34,7 @@ LedgerHandler::process(LedgerHandler::Input input, Context const& ctx) const auto const lgrInfo = std::get(lgrInfoOrStatus); Output output; - if (input.binary) { - output.header[JS(ledger_data)] = ripple::strHex(ledgerInfoToBlob(lgrInfo)); - } else { - output.header[JS(account_hash)] = ripple::strHex(lgrInfo.accountHash); - output.header[JS(close_flags)] = lgrInfo.closeFlags; - output.header[JS(close_time)] = lgrInfo.closeTime.time_since_epoch().count(); - output.header[JS(close_time_human)] = ripple::to_string(lgrInfo.closeTime); - output.header[JS(close_time_resolution)] = lgrInfo.closeTimeResolution.count(); - output.header[JS(closed)] = true; - output.header[JS(ledger_hash)] = ripple::strHex(lgrInfo.hash); - output.header[JS(ledger_index)] = std::to_string(lgrInfo.seq); - output.header[JS(parent_close_time)] = lgrInfo.parentCloseTime.time_since_epoch().count(); - output.header[JS(parent_hash)] = ripple::strHex(lgrInfo.parentHash); - output.header[JS(total_coins)] = ripple::to_string(lgrInfo.drops); - output.header[JS(transaction_hash)] = ripple::strHex(lgrInfo.txHash); - } - - output.header[JS(closed)] = true; + output.header = toJson(lgrInfo, input.binary); if (input.transactions) { output.header[JS(transactions)] = boost::json::value(boost::json::array_kind); @@ -60,20 +43,46 @@ LedgerHandler::process(LedgerHandler::Input input, Context const& ctx) const if (input.expand) { auto txns = sharedPtrBackend_->fetchAllTransactionsInLedger(lgrInfo.seq, ctx.yield); + auto const expandTxJsonV1 = [&](data::TransactionAndMetadata const& tx) { + if (!input.binary) { + auto [txn, meta] = toExpandedJson(tx, ctx.apiVersion); + txn[JS(metaData)] = std::move(meta); + return txn; + } + return toJsonWithBinaryTx(tx, ctx.apiVersion); + }; + + auto const expandTxJsonV2 = [&](data::TransactionAndMetadata const& tx) { + static auto const isoTimeStr = ripple::to_string_iso(lgrInfo.closeTime); + auto [txn, meta] = toExpandedJson(tx, ctx.apiVersion); + if (!input.binary) { + boost::json::object entry; + entry[JS(validated)] = true; + // same with rippled, ledger_index is a string here + entry[JS(ledger_index)] = std::to_string(lgrInfo.seq); + entry[JS(close_time_iso)] = isoTimeStr; + entry[JS(ledger_hash)] = ripple::strHex(lgrInfo.hash); + if (txn.contains(JS(hash))) { + entry[JS(hash)] = txn.at(JS(hash)); + txn.erase(JS(hash)); + } + entry[JS(tx_json)] = std::move(txn); + entry[JS(meta)] = std::move(meta); + return entry; + } + + auto entry = toJsonWithBinaryTx(tx, ctx.apiVersion); + if (txn.contains(JS(hash))) + entry[JS(hash)] = txn.at(JS(hash)); + return entry; + }; + std::transform( std::move_iterator(txns.begin()), std::move_iterator(txns.end()), std::back_inserter(jsonTxs), [&](auto obj) { - boost::json::object entry; - if (!input.binary) { - auto [txn, meta] = toExpandedJson(obj, ctx.apiVersion); - entry = std::move(txn); - entry[JS(metaData)] = std::move(meta); - } else { - entry[JS(tx_blob)] = ripple::strHex(obj.transaction); - entry[JS(meta)] = ripple::strHex(obj.metadata); - } + boost::json::object entry = ctx.apiVersion < 2u ? expandTxJsonV1(obj) : expandTxJsonV2(obj); if (input.ownerFunds) { // check the type of tx diff --git a/src/rpc/handlers/LedgerData.cpp b/src/rpc/handlers/LedgerData.cpp index 228f8b299..2e4dd7eb1 100644 --- a/src/rpc/handlers/LedgerData.cpp +++ b/src/rpc/handlers/LedgerData.cpp @@ -73,29 +73,11 @@ LedgerDataHandler::process(Input input, Context const& ctx) const auto const lgrInfo = std::get(lgrInfoOrStatus); - // no marker -> first call, return header information - auto header = boost::json::object(); Output output; + // no marker -> first call, return header information if ((!input.marker) && (!input.diffMarker)) { - if (input.binary) { - header[JS(ledger_data)] = ripple::strHex(ledgerInfoToBlob(lgrInfo)); - } else { - header[JS(account_hash)] = ripple::strHex(lgrInfo.accountHash); - header[JS(close_flags)] = lgrInfo.closeFlags; - header[JS(close_time)] = lgrInfo.closeTime.time_since_epoch().count(); - header[JS(close_time_human)] = ripple::to_string(lgrInfo.closeTime); - header[JS(close_time_resolution)] = lgrInfo.closeTimeResolution.count(); - header[JS(ledger_hash)] = ripple::strHex(lgrInfo.hash); - header[JS(ledger_index)] = std::to_string(lgrInfo.seq); - header[JS(parent_close_time)] = lgrInfo.parentCloseTime.time_since_epoch().count(); - header[JS(parent_hash)] = ripple::strHex(lgrInfo.parentHash); - header[JS(total_coins)] = ripple::to_string(lgrInfo.drops); - header[JS(transaction_hash)] = ripple::strHex(lgrInfo.txHash); - } - - header[JS(closed)] = true; - output.header = std::move(header); + output.header = toJson(lgrInfo, input.binary); } else { if (input.marker && !sharedPtrBackend_->fetchLedgerObject(*(input.marker), lgrInfo.seq, ctx.yield)) return Error{Status{RippledError::rpcINVALID_PARAMS, "markerDoesNotExist"}}; diff --git a/src/rpc/handlers/NFTHistory.cpp b/src/rpc/handlers/NFTHistory.cpp index 6140f706b..93bf983ea 100644 --- a/src/rpc/handlers/NFTHistory.cpp +++ b/src/rpc/handlers/NFTHistory.cpp @@ -107,15 +107,27 @@ NFTHistoryHandler::process(NFTHistoryHandler::Input input, Context const& ctx) c if (!input.binary) { auto [txn, meta] = toExpandedJson(txnPlusMeta, ctx.apiVersion); + auto const txKey = ctx.apiVersion > 1u ? JS(tx_json) : JS(tx); obj[JS(meta)] = std::move(meta); - obj[JS(tx)] = std::move(txn); - obj[JS(tx)].as_object()[JS(ledger_index)] = txnPlusMeta.ledgerSequence; - obj[JS(tx)].as_object()[JS(date)] = txnPlusMeta.date; + obj[txKey] = std::move(txn); + obj[txKey].as_object()[JS(ledger_index)] = txnPlusMeta.ledgerSequence; + obj[txKey].as_object()[JS(date)] = txnPlusMeta.date; + if (ctx.apiVersion > 1u) { + obj[JS(ledger_index)] = txnPlusMeta.ledgerSequence; + if (obj[txKey].as_object().contains(JS(hash))) { + obj[JS(hash)] = obj[txKey].at(JS(hash)); + obj[txKey].as_object().erase(JS(hash)); + } + if (auto const lgrInfo = + sharedPtrBackend_->fetchLedgerBySequence(txnPlusMeta.ledgerSequence, ctx.yield); + lgrInfo) { + obj[JS(close_time_iso)] = ripple::to_string_iso(lgrInfo->closeTime); + obj[JS(ledger_hash)] = ripple::strHex(lgrInfo->hash); + } + } } else { - obj[JS(meta)] = ripple::strHex(txnPlusMeta.metadata); - obj[JS(tx_blob)] = ripple::strHex(txnPlusMeta.transaction); + obj = toJsonWithBinaryTx(txnPlusMeta, ctx.apiVersion); obj[JS(ledger_index)] = txnPlusMeta.ledgerSequence; - // only clio has this field obj[JS(date)] = txnPlusMeta.date; } diff --git a/src/rpc/handlers/TransactionEntry.cpp b/src/rpc/handlers/TransactionEntry.cpp index 70b1163d6..bcbebeb6c 100644 --- a/src/rpc/handlers/TransactionEntry.cpp +++ b/src/rpc/handlers/TransactionEntry.cpp @@ -32,7 +32,10 @@ TransactionEntryHandler::process(TransactionEntryHandler::Input input, Context c if (auto status = std::get_if(&lgrInfoOrStatus)) return Error{*status}; - auto const lgrInfo = std::get(lgrInfoOrStatus); + auto output = TransactionEntryHandler::Output{}; + output.apiVersion = ctx.apiVersion; + + output.ledgerHeader = std::get(lgrInfoOrStatus); auto const dbRet = sharedPtrBackend_->fetchTransaction(ripple::uint256{input.txHash.c_str()}, ctx.yield); // Note: transaction_entry is meant to only search a specified ledger for // the specified transaction. tx searches the entire range of history. For @@ -43,16 +46,13 @@ TransactionEntryHandler::process(TransactionEntryHandler::Input input, Context c // the API for transaction_entry says the method only searches the specified // ledger; we simulate that here by returning not found if the transaction // is in a different ledger than the one specified. - if (!dbRet || dbRet->ledgerSequence != lgrInfo.seq) + if (!dbRet || dbRet->ledgerSequence != output.ledgerHeader->seq) return Error{Status{RippledError::rpcTXN_NOT_FOUND, "transactionNotFound", "Transaction not found."}}; - auto output = TransactionEntryHandler::Output{}; auto [txn, meta] = toExpandedJson(*dbRet, ctx.apiVersion); output.tx = std::move(txn); output.metadata = std::move(meta); - output.ledgerIndex = lgrInfo.seq; - output.ledgerHash = ripple::strHex(lgrInfo.hash); return output; } @@ -60,13 +60,22 @@ TransactionEntryHandler::process(TransactionEntryHandler::Input input, Context c void tag_invoke(boost::json::value_from_tag, boost::json::value& jv, TransactionEntryHandler::Output const& output) { + auto const metaKey = output.apiVersion > 1u ? JS(meta) : JS(metadata); jv = { {JS(validated), output.validated}, - {JS(metadata), output.metadata}, + {metaKey, output.metadata}, {JS(tx_json), output.tx}, - {JS(ledger_index), output.ledgerIndex}, - {JS(ledger_hash), output.ledgerHash}, + {JS(ledger_index), output.ledgerHeader->seq}, + {JS(ledger_hash), ripple::strHex(output.ledgerHeader->hash)}, }; + + if (output.apiVersion > 1u) { + jv.as_object()[JS(close_time_iso)] = ripple::to_string_iso(output.ledgerHeader->closeTime); + if (output.tx.contains(JS(hash))) { + jv.as_object()[JS(hash)] = output.tx.at(JS(hash)); + jv.as_object()[JS(tx_json)].as_object().erase(JS(hash)); + } + } } TransactionEntryHandler::Input diff --git a/src/rpc/handlers/TransactionEntry.h b/src/rpc/handlers/TransactionEntry.h index 0b3a8ede2..4ae0d53b5 100644 --- a/src/rpc/handlers/TransactionEntry.h +++ b/src/rpc/handlers/TransactionEntry.h @@ -37,13 +37,13 @@ class TransactionEntryHandler { public: struct Output { - uint32_t ledgerIndex; - std::string ledgerHash; + std::optional ledgerHeader; // TODO: use a better type for this boost::json::object metadata; boost::json::object tx; // validated should be sent via framework bool validated = true; + uint32_t apiVersion; }; struct Input { diff --git a/src/rpc/handlers/Tx.h b/src/rpc/handlers/Tx.h index 6c6bee152..aa8da298b 100644 --- a/src/rpc/handlers/Tx.h +++ b/src/rpc/handlers/Tx.h @@ -42,7 +42,8 @@ class BaseTxHandler { std::optional tx{}; std::optional metaStr{}; std::optional txStr{}; - std::optional ctid{}; // ctid when binary=true + std::optional ctid{}; // ctid when binary=true + std::optional ledgerHeader{}; // ledger hash when apiVersion >= 2 uint32_t apiVersion = 0u; bool validated = true; }; @@ -170,6 +171,10 @@ class BaseTxHandler { output.date = dbResponse->date; output.ledgerIndex = dbResponse->ledgerSequence; + // fetch ledger hash + if (ctx.apiVersion > 1u) + output.ledgerHeader = sharedPtrBackend_->fetchLedgerBySequence(dbResponse->ledgerSequence, ctx.yield); + return output; } @@ -192,23 +197,60 @@ class BaseTxHandler { friend void tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output) { - auto obj = boost::json::object{}; + auto const getJsonV1 = [&]() { + auto obj = boost::json::object{}; + + if (output.tx) { + obj = *output.tx; + obj[JS(meta)] = *output.meta; + } else { + obj[JS(meta)] = *output.metaStr; + obj[JS(tx)] = *output.txStr; + obj[JS(hash)] = output.hash; + } - if (output.tx) { - obj = *output.tx; - obj[JS(meta)] = *output.meta; - } else { - obj[JS(meta)] = *output.metaStr; - obj[JS(tx)] = *output.txStr; - obj[JS(hash)] = output.hash; - } + obj[JS(validated)] = output.validated; + obj[JS(date)] = output.date; + obj[JS(ledger_index)] = output.ledgerIndex; + obj[JS(inLedger)] = output.ledgerIndex; + return obj; + }; - obj[JS(validated)] = output.validated; - obj[JS(date)] = output.date; - obj[JS(ledger_index)] = output.ledgerIndex; + auto const getJsonV2 = [&]() { + auto obj = boost::json::object{}; + + if (output.tx) { + obj[JS(tx_json)] = *output.tx; + obj[JS(tx_json)].as_object()[JS(date)] = output.date; + obj[JS(tx_json)].as_object()[JS(ledger_index)] = output.ledgerIndex; + // move ctid from tx_json to root + if (obj[JS(tx_json)].as_object().contains(JS(ctid))) { + obj[JS(ctid)] = obj[JS(tx_json)].as_object()[JS(ctid)]; + obj[JS(tx_json)].as_object().erase(JS(ctid)); + } + // move hash from tx_json to root + if (obj[JS(tx_json)].as_object().contains(JS(hash))) { + obj[JS(hash)] = obj[JS(tx_json)].as_object()[JS(hash)]; + obj[JS(tx_json)].as_object().erase(JS(hash)); + } + obj[JS(meta)] = *output.meta; + } else { + obj[JS(meta_blob)] = *output.metaStr; + obj[JS(tx_blob)] = *output.txStr; + obj[JS(hash)] = output.hash; + } - if (output.apiVersion < 2u) - obj[JS(inLedger)] = output.ledgerIndex; + obj[JS(validated)] = output.validated; + obj[JS(ledger_index)] = output.ledgerIndex; + + if (output.ledgerHeader) { + obj[JS(ledger_hash)] = ripple::strHex(output.ledgerHeader->hash); + obj[JS(close_time_iso)] = ripple::to_string_iso(output.ledgerHeader->closeTime); + } + return obj; + }; + + auto obj = output.apiVersion > 1u ? getJsonV2() : getJsonV1(); if (output.ctid) obj[JS(ctid)] = *output.ctid; diff --git a/unittests/rpc/RPCHelpersTests.cpp b/unittests/rpc/RPCHelpersTests.cpp index 1d4fc0f2d..437889bf9 100644 --- a/unittests/rpc/RPCHelpersTests.cpp +++ b/unittests/rpc/RPCHelpersTests.cpp @@ -418,3 +418,54 @@ TEST_F(RPCHelpersTest, DeliverMaxAliasV2) ) ); } + +TEST_F(RPCHelpersTest, LedgerHeaderJson) +{ + auto const ledgerHeader = CreateLedgerInfo(INDEX1, 30); + auto const binJson = toJson(ledgerHeader, true); + + auto constexpr EXPECTBIN = R"({ + "ledger_data": "0000001E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "closed": true + })"; + EXPECT_EQ(binJson, boost::json::parse(EXPECTBIN)); + + auto const EXPECTJSON = fmt::format( + R"({{ + "account_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "close_flags": 0, + "close_time": 0, + "close_time_resolution": 0, + "close_time_iso": "2000-01-01T00:00:00Z", + "ledger_hash": "{}", + "ledger_index": "{}", + "parent_close_time": 0, + "parent_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "total_coins": "0", + "transaction_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "closed": true + }})", + INDEX1, + 30 + ); + auto json = toJson(ledgerHeader, false); + // remove platform-related close_time_human field + json.erase(JS(close_time_human)); + EXPECT_EQ(json, boost::json::parse(EXPECTJSON)); +} + +TEST_F(RPCHelpersTest, TransactionAndMetadataBinaryJsonV1) +{ + auto const txMeta = CreateAcceptNFTOfferTxWithMetadata(ACCOUNT, 30, 1, INDEX1); + auto const json = toJsonWithBinaryTx(txMeta, 1); + EXPECT_TRUE(json.contains(JS(tx_blob))); + EXPECT_TRUE(json.contains(JS(meta))); +} + +TEST_F(RPCHelpersTest, TransactionAndMetadataBinaryJsonV2) +{ + auto const txMeta = CreateAcceptNFTOfferTxWithMetadata(ACCOUNT, 30, 1, INDEX1); + auto const json = toJsonWithBinaryTx(txMeta, 2); + EXPECT_TRUE(json.contains(JS(tx_blob))); + EXPECT_TRUE(json.contains(JS(meta_blob))); +} diff --git a/unittests/rpc/handlers/AccountTxTests.cpp b/unittests/rpc/handlers/AccountTxTests.cpp index d695af35a..0b2a1fca2 100644 --- a/unittests/rpc/handlers/AccountTxTests.cpp +++ b/unittests/rpc/handlers/AccountTxTests.cpp @@ -670,6 +670,64 @@ TEST_F(RPCAccountTxHandlerTest, BinaryTrue) }); } +TEST_F(RPCAccountTxHandlerTest, BinaryTrueV2) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = dynamic_cast(mockBackendPtr.get()); + ASSERT_NE(rawBackendPtr, nullptr); + auto const transactions = genTransactions(MINSEQ + 1, MAXSEQ - 1); + auto const transCursor = TransactionsAndCursor{transactions, TransactionsCursor{12, 34}}; + EXPECT_CALL( + *rawBackendPtr, + fetchAccountTransactions( + testing::_, + testing::_, + false, + testing::Optional(testing::Eq(TransactionsCursor{MAXSEQ, INT32_MAX})), + testing::_ + ) + ) + .WillOnce(Return(transCursor)); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{AccountTxHandler{mockBackendPtr}}; + auto const static input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_index_min": {}, + "ledger_index_max": {}, + "binary": true + }})", + ACCOUNT, + -1, + -1 + )); + auto const output = handler.process(input, Context{.yield = yield, .apiVersion = 2u}); + ASSERT_TRUE(output); + EXPECT_EQ(output->at("account").as_string(), ACCOUNT); + EXPECT_EQ(output->at("ledger_index_min").as_uint64(), MINSEQ); + EXPECT_EQ(output->at("ledger_index_max").as_uint64(), MAXSEQ); + EXPECT_EQ(output->at("marker").as_object(), json::parse(R"({"ledger": 12, "seq": 34})")); + EXPECT_EQ(output->at("transactions").as_array().size(), 2); + EXPECT_EQ( + output->at("transactions").as_array()[0].as_object().at("meta_blob").as_string(), + "201C00000000F8E5110061E762400000000000001681144B4E9C06F24296074F7B" + "C48F92A97916C6DC5EA9E1E1E5110061E76240000000000000178114D31252CF90" + "2EF8DD8451243869B38667CBD89DF3E1E1F1031000" + ); + EXPECT_EQ( + output->at("transactions").as_array()[0].as_object().at("tx_blob").as_string(), + "120000240000002061400000000000000168400000000000000173047465737481" + "144B4E9C06F24296074F7BC48F92A97916C6DC5EA98314D31252CF902EF8DD8451" + "243869B38667CBD89DF3" + ); + EXPECT_FALSE(output->at("transactions").as_array()[0].as_object().contains("date")); + EXPECT_FALSE(output->at("transactions").as_array()[0].as_object().contains("inLedger")); + EXPECT_FALSE(output->as_object().contains("limit")); + }); +} + TEST_F(RPCAccountTxHandlerTest, LimitAndMarker) { mockBackendPtr->updateRange(MINSEQ); // min @@ -1287,7 +1345,11 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2) "TransactionResult": "tesSUCCESS", "nftoken_id": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF" }, - "tx": + "hash": "C74463F49CFDCBEF3E9902672719918CDE5042DC7E7660BEBD1D1105C4B6DFF4", + "ledger_index": 11, + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "close_time_iso": "2000-01-01T00:00:00Z", + "tx_json": { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Fee": "50", @@ -1295,7 +1357,6 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2) "Sequence": 1, "SigningPubKey": "74657374", "TransactionType": "NFTokenMint", - "hash": "C74463F49CFDCBEF3E9902672719918CDE5042DC7E7660BEBD1D1105C4B6DFF4", "ledger_index": 11, "date": 1 }, @@ -1321,7 +1382,11 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2) "TransactionResult": "tesSUCCESS", "nftoken_id": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA" }, - "tx": + "hash": "7682BE6BCDE62F8142915DD852936623B68FC3839A8A424A6064B898702B0CDF", + "ledger_index": 11, + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "close_time_iso": "2000-01-01T00:00:00Z", + "tx_json": { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Fee": "50", @@ -1329,7 +1394,6 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2) "Sequence": 1, "SigningPubKey": "74657374", "TransactionType": "NFTokenAcceptOffer", - "hash": "7682BE6BCDE62F8142915DD852936623B68FC3839A8A424A6064B898702B0CDF", "ledger_index": 11, "date": 2 }, @@ -1368,7 +1432,11 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2) "15FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF" ] }, - "tx": + "hash": "9F82743EEB30065FB9CB92C61F0F064B5859C5A590FA811FAAAD9C988E5B47DB", + "ledger_index": 11, + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "close_time_iso": "2000-01-01T00:00:00Z", + "tx_json": { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Fee": "50", @@ -1380,7 +1448,6 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2) "Sequence": 1, "SigningPubKey": "74657374", "TransactionType": "NFTokenCancelOffer", - "hash": "9F82743EEB30065FB9CB92C61F0F064B5859C5A590FA811FAAAD9C988E5B47DB", "ledger_index": 11, "date": 3 }, @@ -1403,7 +1470,11 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2) "TransactionResult": "tesSUCCESS", "offer_id": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA" }, - "tx": + "hash": "ECB1837EB7C7C0AC22ECDCCE59FDD4795C70E0B9D8F4E1C9A9408BB7EC75DA5C", + "ledger_index": 11, + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "close_time_iso": "2000-01-01T00:00:00Z", + "tx_json": { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Amount": "123", @@ -1412,7 +1483,6 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2) "Sequence": 1, "SigningPubKey": "74657374", "TransactionType": "NFTokenCreateOffer", - "hash": "ECB1837EB7C7C0AC22ECDCCE59FDD4795C70E0B9D8F4E1C9A9408BB7EC75DA5C", "ledger_index": 11, "date": 4 }, @@ -1441,6 +1511,9 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2) ) .Times(1); + auto const ledgerInfo = CreateLedgerInfo(LEDGERHASH, 11); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(transactions.size()).WillRepeatedly(Return(ledgerInfo)); + runSpawn([&, this](auto yield) { auto const handler = AnyHandler{AccountTxHandler{mockBackendPtr}}; auto const static input = json::parse(fmt::format( @@ -1738,6 +1811,10 @@ generateTransactionTypeTestValues() })", R"([ { + "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "ledger_index": 30, + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "close_time_iso": "2000-01-01T00:00:00Z", "meta": { "AffectedNodes": [ { @@ -1762,7 +1839,7 @@ generateTransactionTypeTestValues() "TransactionResult": "tesSUCCESS", "delivered_amount": "unavailable" }, - "tx": { + "tx_json": { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "DeliverMax": "1", "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", @@ -1770,7 +1847,6 @@ generateTransactionTypeTestValues() "Sequence": 32, "SigningPubKey": "74657374", "TransactionType": "Payment", - "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", "ledger_index": 30, "date": 1 }, @@ -1860,8 +1936,8 @@ TEST_P(AccountTxTransactionTypeTest, SpecificTransactionType) .Times(1); auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, MAXSEQ); - EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); ON_CALL(*rawBackendPtr, fetchLedgerBySequence(MAXSEQ, _)).WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence(MAXSEQ, _)).Times(Between(1, 2)); auto const testBundle = GetParam(); runSpawn([&, this](auto yield) { diff --git a/unittests/rpc/handlers/LedgerDataTests.cpp b/unittests/rpc/handlers/LedgerDataTests.cpp index b6dd08be8..8b027cc94 100644 --- a/unittests/rpc/handlers/LedgerDataTests.cpp +++ b/unittests/rpc/handlers/LedgerDataTests.cpp @@ -229,6 +229,7 @@ TEST_F(RPCLedgerDataHandlerTest, NoMarker) "close_flags":0, "close_time":0, "close_time_resolution":0, + "close_time_iso":"2000-01-01T00:00:00Z", "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index":"30", "parent_close_time":0, @@ -291,6 +292,7 @@ TEST_F(RPCLedgerDataHandlerTest, TypeFilter) "close_flags":0, "close_time":0, "close_time_resolution":0, + "close_time_iso":"2000-01-01T00:00:00Z", "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index":"30", "parent_close_time":0, @@ -356,6 +358,7 @@ TEST_F(RPCLedgerDataHandlerTest, TypeFilterAMM) "close_flags":0, "close_time":0, "close_time_resolution":0, + "close_time_iso":"2000-01-01T00:00:00Z", "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index":"30", "parent_close_time":0, @@ -418,6 +421,7 @@ TEST_F(RPCLedgerDataHandlerTest, OutOfOrder) "close_flags":0, "close_time":0, "close_time_resolution":0, + "close_time_iso":"2000-01-01T00:00:00Z", "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index":"30", "parent_close_time":0, diff --git a/unittests/rpc/handlers/LedgerTests.cpp b/unittests/rpc/handlers/LedgerTests.cpp index d54500f1f..8c1c0a1b8 100644 --- a/unittests/rpc/handlers/LedgerTests.cpp +++ b/unittests/rpc/handlers/LedgerTests.cpp @@ -263,6 +263,7 @@ TEST_F(RPCLedgerHandlerTest, Default) "close_time":0, "close_time_resolution":0, "closed":true, + "close_time_iso":"2000-01-01T00:00:00Z", "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index":"30", "parent_close_time":0, @@ -445,6 +446,60 @@ TEST_F(RPCLedgerHandlerTest, TransactionsExpandBinary) }); } +TEST_F(RPCLedgerHandlerTest, TransactionsExpandBinaryV2) +{ + static auto constexpr expectedOut = + R"({ + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index": 30, + "validated": true, + "ledger":{ + "ledger_data": "0000001E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "closed": true, + "transactions": [ + { + "hash": "70436A9332F7CD928FAEC1A41269A677739D8B11F108CE23AE23CBF0C9113F8C", + "tx_blob": "120000240000001E61400000000000006468400000000000000373047465737481144B4E9C06F24296074F7BC48F92A97916C6DC5EA98314D31252CF902EF8DD8451243869B38667CBD89DF3", + "meta_blob": "201C00000000F8E5110061E762400000000000006E81144B4E9C06F24296074F7BC48F92A97916C6DC5EA9E1E1E5110061E762400000000000001E8114D31252CF902EF8DD8451243869B38667CBD89DF3E1E1F1031000" + }, + { + "hash": "70436A9332F7CD928FAEC1A41269A677739D8B11F108CE23AE23CBF0C9113F8C", + "tx_blob": "120000240000001E61400000000000006468400000000000000373047465737481144B4E9C06F24296074F7BC48F92A97916C6DC5EA98314D31252CF902EF8DD8451243869B38667CBD89DF3", + "meta_blob": "201C00000000F8E5110061E762400000000000006E81144B4E9C06F24296074F7BC48F92A97916C6DC5EA9E1E1E5110061E762400000000000001E8114D31252CF902EF8DD8451243869B38667CBD89DF3E1E1F1031000" + } + ] + } + })"; + auto const rawBackendPtr = dynamic_cast(mockBackendPtr.get()); + ASSERT_NE(rawBackendPtr, nullptr); + mockBackendPtr->updateRange(RANGEMIN); + mockBackendPtr->updateRange(RANGEMAX); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, RANGEMAX); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)).WillOnce(Return(ledgerinfo)); + + TransactionAndMetadata t1; + t1.transaction = CreatePaymentTransactionObject(ACCOUNT, ACCOUNT2, 100, 3, RANGEMAX).getSerializer().peekData(); + t1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT, ACCOUNT2, 110, 30).getSerializer().peekData(); + t1.ledgerSequence = RANGEMAX; + + EXPECT_CALL(*rawBackendPtr, fetchAllTransactionsInLedger(RANGEMAX, _)).WillOnce(Return(std::vector{t1, t1})); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{LedgerHandler{mockBackendPtr}}; + auto const req = json::parse( + R"({ + "binary": true, + "expand": true, + "transactions": true + })" + ); + auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 2u}); + ASSERT_TRUE(output); + EXPECT_EQ(*output, json::parse(expectedOut)); + }); +} + TEST_F(RPCLedgerHandlerTest, TransactionsExpandNotBinary) { static auto constexpr expectedOut = @@ -461,6 +516,7 @@ TEST_F(RPCLedgerHandlerTest, TransactionsExpandNotBinary) "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index":"30", "parent_close_time":0, + "close_time_iso":"2000-01-01T00:00:00Z", "parent_hash":"0000000000000000000000000000000000000000000000000000000000000000", "total_coins":"0", "transaction_hash":"0000000000000000000000000000000000000000000000000000000000000000", @@ -538,6 +594,108 @@ TEST_F(RPCLedgerHandlerTest, TransactionsExpandNotBinary) }); } +TEST_F(RPCLedgerHandlerTest, TransactionsExpandNotBinaryV2) +{ + static auto constexpr expectedOut = + R"({ + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index": 30, + "validated": true, + "ledger":{ + "account_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "close_flags": 0, + "close_time": 0, + "close_time_resolution": 0, + "closed": true, + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index": "30", + "parent_close_time": 0, + "close_time_iso": "2000-01-01T00:00:00Z", + "parent_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "total_coins": "0", + "transaction_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "transactions":[ + { + "validated": true, + "close_time_iso": "2000-01-01T00:00:00Z", + "hash": "70436A9332F7CD928FAEC1A41269A677739D8B11F108CE23AE23CBF0C9113F8C", + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index": "30", + "tx_json": + { + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "DeliverMax": "100", + "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee": "3", + "Sequence": 30, + "SigningPubKey": "74657374", + "TransactionType": "Payment" + }, + "meta":{ + "AffectedNodes":[ + { + "ModifiedNode": + { + "FinalFields": + { + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Balance": "110" + }, + "LedgerEntryType": "AccountRoot" + } + }, + { + "ModifiedNode": + { + "FinalFields": + { + "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Balance": "30" + }, + "LedgerEntryType": "AccountRoot" + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS", + "delivered_amount": "unavailable" + } + } + ] + } + })"; + auto const rawBackendPtr = dynamic_cast(mockBackendPtr.get()); + ASSERT_NE(rawBackendPtr, nullptr); + mockBackendPtr->updateRange(RANGEMIN); + mockBackendPtr->updateRange(RANGEMAX); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, RANGEMAX); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)).WillOnce(Return(ledgerinfo)); + + TransactionAndMetadata t1; + t1.transaction = CreatePaymentTransactionObject(ACCOUNT, ACCOUNT2, 100, 3, RANGEMAX).getSerializer().peekData(); + t1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT, ACCOUNT2, 110, 30).getSerializer().peekData(); + t1.ledgerSequence = RANGEMAX; + + EXPECT_CALL(*rawBackendPtr, fetchAllTransactionsInLedger(RANGEMAX, _)).WillOnce(Return(std::vector{t1})); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{LedgerHandler{mockBackendPtr}}; + auto const req = json::parse( + R"({ + "binary": false, + "expand": true, + "transactions": true + })" + ); + auto output = handler.process(req, Context{.yield = yield, .apiVersion = 2u}); + ASSERT_TRUE(output); + // remove human readable time, it is sightly different cross the platform + EXPECT_EQ(output->as_object().at("ledger").as_object().erase("close_time_human"), 1); + EXPECT_EQ(*output, json::parse(expectedOut)); + }); +} + TEST_F(RPCLedgerHandlerTest, TransactionsNotExpand) { auto const rawBackendPtr = dynamic_cast(mockBackendPtr.get()); @@ -689,6 +847,7 @@ TEST_F(RPCLedgerHandlerTest, OwnerFundsEmtpy) "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index":"30", "parent_close_time":0, + "close_time_iso":"2000-01-01T00:00:00Z", "parent_hash":"0000000000000000000000000000000000000000000000000000000000000000", "total_coins":"0", "transaction_hash":"0000000000000000000000000000000000000000000000000000000000000000", @@ -780,6 +939,7 @@ TEST_F(RPCLedgerHandlerTest, OwnerFundsTrueBinaryFalse) "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index": "30", "parent_close_time": 0, + "close_time_iso": "2000-01-01T00:00:00Z", "parent_hash": "0000000000000000000000000000000000000000000000000000000000000000", "total_coins": "0", "transaction_hash": "0000000000000000000000000000000000000000000000000000000000000000", diff --git a/unittests/rpc/handlers/NFTHistoryTests.cpp b/unittests/rpc/handlers/NFTHistoryTests.cpp index eca43ad87..51c856fdf 100644 --- a/unittests/rpc/handlers/NFTHistoryTests.cpp +++ b/unittests/rpc/handlers/NFTHistoryTests.cpp @@ -285,8 +285,110 @@ TEST_F(RPCNFTHistoryHandlerTest, IndexSpecificForwardTrue) }); } -TEST_F(RPCNFTHistoryHandlerTest, IndexSpecificForwardFalse) +TEST_F(RPCNFTHistoryHandlerTest, IndexSpecificForwardFalseV1) { + auto constexpr OUTPUT = R"({ + "nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", + "ledger_index_min": 11, + "ledger_index_max": 29, + "transactions": + [ + { + "meta": + { + "AffectedNodes": + [ + { + "ModifiedNode":{ + "FinalFields":{ + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Balance": "22" + }, + "LedgerEntryType": "AccountRoot" + } + }, + { + "ModifiedNode":{ + "FinalFields":{ + "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Balance": "23" + }, + "LedgerEntryType": "AccountRoot" + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS", + "delivered_amount": "unavailable" + }, + "tx": + { + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount": "1", + "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee": "1", + "Sequence": 32, + "SigningPubKey": "74657374", + "TransactionType": "Payment", + "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "DeliverMax": "1", + "ledger_index": 11, + "date": 1 + }, + "validated": true + }, + { + "meta": + { + "AffectedNodes": + [ + { + "ModifiedNode":{ + "FinalFields":{ + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Balance": "22" + }, + "LedgerEntryType": "AccountRoot" + } + }, + { + "ModifiedNode":{ + "FinalFields":{ + "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Balance": "23" + }, + "LedgerEntryType": "AccountRoot" + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS", + "delivered_amount": "unavailable" + }, + "tx": + { + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount": "1", + "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee": "1", + "Sequence": 32, + "SigningPubKey": "74657374", + "TransactionType": "Payment", + "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "DeliverMax": "1", + "ledger_index": 29, + "date": 2 + }, + "validated": true + } + ], + "validated": true, + "marker": + { + "ledger": 12, + "seq": 34 + } + })"; mockBackendPtr->updateRange(MINSEQ); // min mockBackendPtr->updateRange(MAXSEQ); // max MockBackend* rawBackendPtr = dynamic_cast(mockBackendPtr.get()); @@ -321,12 +423,164 @@ TEST_F(RPCNFTHistoryHandlerTest, IndexSpecificForwardFalse) )); auto const output = handler.process(input, Context{yield}); ASSERT_TRUE(output); - EXPECT_EQ(output->at("nft_id").as_string(), NFTID); - EXPECT_EQ(output->at("ledger_index_min").as_uint64(), MINSEQ + 1); - EXPECT_EQ(output->at("ledger_index_max").as_uint64(), MAXSEQ - 1); - EXPECT_EQ(output->at("marker").as_object(), json::parse(R"({"ledger":12,"seq":34})")); - EXPECT_EQ(output->at("transactions").as_array().size(), 2); - EXPECT_FALSE(output->as_object().contains("limit")); + EXPECT_EQ(output.value(), boost::json::parse(OUTPUT)); + }); +} + +TEST_F(RPCNFTHistoryHandlerTest, IndexSpecificForwardFalseV2) +{ + auto constexpr OUTPUT = R"({ + "nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", + "ledger_index_min": 11, + "ledger_index_max": 29, + "transactions": + [ + { + "meta": + { + "AffectedNodes": + [ + { + "ModifiedNode": + { + "FinalFields": + { + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Balance": "22" + }, + "LedgerEntryType": "AccountRoot" + } + }, + { + "ModifiedNode": + { + "FinalFields": + { + "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Balance": "23" + }, + "LedgerEntryType": "AccountRoot" + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS", + "delivered_amount": "unavailable" + }, + "tx_json": + { + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee": "1", + "Sequence": 32, + "SigningPubKey": "74657374", + "TransactionType": "Payment", + "DeliverMax": "1", + "ledger_index": 11, + "date": 1 + }, + "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "ledger_index": 11, + "close_time_iso": "2000-01-01T00:00:00Z", + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "validated": true + }, + { + "meta": + { + "AffectedNodes": + [ + { + "ModifiedNode": + { + "FinalFields": + { + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Balance": "22" + }, + "LedgerEntryType": "AccountRoot" + } + }, + { + "ModifiedNode": + { + "FinalFields": + { + "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Balance": "23" + }, + "LedgerEntryType": "AccountRoot" + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS", + "delivered_amount": "unavailable" + }, + "tx_json": + { + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee": "1", + "Sequence": 32, + "SigningPubKey": "74657374", + "TransactionType": "Payment", + "DeliverMax": "1", + "ledger_index": 29, + "date": 2 + }, + "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "ledger_index": 29, + "close_time_iso": "2000-01-01T00:00:00Z", + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "validated": true + } + ], + "validated": true, + "marker": + { + "ledger": 12, + "seq": 34 + } + })"; + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = dynamic_cast(mockBackendPtr.get()); + ASSERT_NE(rawBackendPtr, nullptr); + auto const transactions = genTransactions(MINSEQ + 1, MAXSEQ - 1); + auto const transCursor = TransactionsAndCursor{transactions, TransactionsCursor{12, 34}}; + EXPECT_CALL( + *rawBackendPtr, + fetchNFTTransactions( + testing::_, + testing::_, + false, + testing::Optional(testing::Eq(TransactionsCursor{MAXSEQ - 1, INT32_MAX})), + testing::_ + ) + ) + .WillOnce(Return(transCursor)); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, MAXSEQ); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(2); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_index_min": {}, + "ledger_index_max": {}, + "forward": false + }})", + NFTID, + MINSEQ + 1, + MAXSEQ - 1 + )); + auto const output = handler.process(input, Context{.yield = yield, .apiVersion = 2u}); + ASSERT_TRUE(output); + EXPECT_EQ(output.value(), boost::json::parse(OUTPUT)); }); } @@ -416,7 +670,7 @@ TEST_F(RPCNFTHistoryHandlerTest, IndexNotSpecificForwardFalse) }); } -TEST_F(RPCNFTHistoryHandlerTest, BinaryTrue) +TEST_F(RPCNFTHistoryHandlerTest, BinaryTrueV1) { mockBackendPtr->updateRange(MINSEQ); // min mockBackendPtr->updateRange(MAXSEQ); // max @@ -475,6 +729,64 @@ TEST_F(RPCNFTHistoryHandlerTest, BinaryTrue) }); } +TEST_F(RPCNFTHistoryHandlerTest, BinaryTrueV2) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = dynamic_cast(mockBackendPtr.get()); + ASSERT_NE(rawBackendPtr, nullptr); + auto const transactions = genTransactions(MINSEQ + 1, MAXSEQ - 1); + auto const transCursor = TransactionsAndCursor{transactions, TransactionsCursor{12, 34}}; + EXPECT_CALL( + *rawBackendPtr, + fetchNFTTransactions( + testing::_, + testing::_, + false, + testing::Optional(testing::Eq(TransactionsCursor{MAXSEQ, INT32_MAX})), + testing::_ + ) + ) + .WillOnce(Return(transCursor)); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_index_min": {}, + "ledger_index_max": {}, + "binary": true + }})", + NFTID, + -1, + -1 + )); + auto const output = handler.process(input, Context{.yield = yield, .apiVersion = 2u}); + ASSERT_TRUE(output); + EXPECT_EQ(output->at("nft_id").as_string(), NFTID); + EXPECT_EQ(output->at("ledger_index_min").as_uint64(), MINSEQ); + EXPECT_EQ(output->at("ledger_index_max").as_uint64(), MAXSEQ); + EXPECT_EQ(output->at("marker").as_object(), json::parse(R"({"ledger":12,"seq":34})")); + EXPECT_EQ(output->at("transactions").as_array().size(), 2); + EXPECT_EQ( + output->at("transactions").as_array()[0].as_object().at("meta_blob").as_string(), + "201C00000000F8E5110061E762400000000000001681144B4E9C06F24296074F7B" + "C48F92A97916C6DC5EA9E1E1E5110061E76240000000000000178114D31252CF90" + "2EF8DD8451243869B38667CBD89DF3E1E1F1031000" + ); + EXPECT_EQ( + output->at("transactions").as_array()[0].as_object().at("tx_blob").as_string(), + "120000240000002061400000000000000168400000000000000173047465737481" + "144B4E9C06F24296074F7BC48F92A97916C6DC5EA98314D31252CF902EF8DD8451" + "243869B38667CBD89DF3" + ); + EXPECT_EQ(output->at("transactions").as_array()[0].as_object().at("date").as_uint64(), 1); + + EXPECT_FALSE(output->as_object().contains("limit")); + }); +} + TEST_F(RPCNFTHistoryHandlerTest, LimitAndMarker) { mockBackendPtr->updateRange(MINSEQ); // min diff --git a/unittests/rpc/handlers/TransactionEntryTests.cpp b/unittests/rpc/handlers/TransactionEntryTests.cpp index fd9193023..58392aa35 100644 --- a/unittests/rpc/handlers/TransactionEntryTests.cpp +++ b/unittests/rpc/handlers/TransactionEntryTests.cpp @@ -179,48 +179,49 @@ TEST_F(RPCTransactionEntryHandlerTest, LedgerSeqNotMatch) TEST_F(RPCTransactionEntryHandlerTest, NormalPath) { static auto constexpr OUTPUT = R"({ - "metadata":{ + "metadata": + { "AffectedNodes": [ { "CreatedNode": { - "LedgerEntryType":"Offer", + "LedgerEntryType": "Offer", "NewFields": { - "TakerGets":"200", + "TakerGets": "200", "TakerPays": { - "currency":"0158415500000000C1F76FF6ECB0BAC600000000", - "issuer":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "value":"300" + "currency": "0158415500000000C1F76FF6ECB0BAC600000000", + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "value": "300" } } } } ], - "TransactionIndex":100, - "TransactionResult":"tesSUCCESS" + "TransactionIndex": 100, + "TransactionResult": "tesSUCCESS" }, "tx_json": { - "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Fee":"2", - "Sequence":100, - "SigningPubKey":"74657374", + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Fee": "2", + "Sequence": 100, + "SigningPubKey": "74657374", "TakerGets": { - "currency":"0158415500000000C1F76FF6ECB0BAC600000000", - "issuer":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "value":"200" + "currency": "0158415500000000C1F76FF6ECB0BAC600000000", + "issuer": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "value": "200" }, - "TakerPays":"300", - "TransactionType":"OfferCreate", - "hash":"2E2FBAAFF767227FE4381C4BE9855986A6B9F96C62F6E443731AB36F7BBB8A08" + "TakerPays": "300", + "TransactionType": "OfferCreate", + "hash": "2E2FBAAFF767227FE4381C4BE9855986A6B9F96C62F6E443731AB36F7BBB8A08" }, - "ledger_index":30, - "ledger_hash":"E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322", - "validated":true + "ledger_index": 30, + "ledger_hash": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322", + "validated": true })"; auto const rawBackendPtr = dynamic_cast(mockBackendPtr.get()); ASSERT_NE(rawBackendPtr, nullptr); @@ -253,3 +254,81 @@ TEST_F(RPCTransactionEntryHandlerTest, NormalPath) EXPECT_EQ(json::parse(OUTPUT), *output); }); } + +TEST_F(RPCTransactionEntryHandlerTest, NormalPathV2) +{ + static auto constexpr OUTPUT = R"({ + "meta": + { + "AffectedNodes": + [ + { + "CreatedNode": + { + "LedgerEntryType": "Offer", + "NewFields": + { + "TakerGets": "200", + "TakerPays": + { + "currency": "0158415500000000C1F76FF6ECB0BAC600000000", + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "value": "300" + } + } + } + } + ], + "TransactionIndex": 100, + "TransactionResult": "tesSUCCESS" + }, + "tx_json": + { + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Fee": "2", + "Sequence": 100, + "SigningPubKey": "74657374", + "TakerGets": + { + "currency": "0158415500000000C1F76FF6ECB0BAC600000000", + "issuer": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "value": "200" + }, + "TakerPays": "300", + "TransactionType": "OfferCreate" + }, + "ledger_index": 30, + "ledger_hash": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322", + "close_time_iso": "2000-01-01T00:00:00Z", + "hash": "2E2FBAAFF767227FE4381C4BE9855986A6B9F96C62F6E443731AB36F7BBB8A08", + "validated": true + })"; + auto const rawBackendPtr = dynamic_cast(mockBackendPtr.get()); + ASSERT_NE(rawBackendPtr, nullptr); + TransactionAndMetadata tx; + tx.metadata = CreateMetaDataForCreateOffer(CURRENCY, ACCOUNT, 100, 200, 300).getSerializer().peekData(); + tx.transaction = + CreateCreateOfferTransactionObject(ACCOUNT, 2, 100, CURRENCY, ACCOUNT2, 200, 300).getSerializer().peekData(); + tx.date = 123456; + tx.ledgerSequence = 30; + EXPECT_CALL(*rawBackendPtr, fetchTransaction(ripple::uint256{TXNID}, _)).WillOnce(Return(tx)); + + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(tx.ledgerSequence); // max + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).WillOnce(Return(CreateLedgerInfo(INDEX, tx.ledgerSequence))); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{TransactionEntryHandler{mockBackendPtr}}; + auto const req = json::parse(fmt::format( + R"({{ + "tx_hash": "{}", + "ledger_index": {} + }})", + TXNID, + tx.ledgerSequence + )); + auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 2}); + ASSERT_TRUE(output); + EXPECT_EQ(json::parse(OUTPUT), *output); + }); +} diff --git a/unittests/rpc/handlers/TxTests.cpp b/unittests/rpc/handlers/TxTests.cpp index bb5109562..235e0a1d9 100644 --- a/unittests/rpc/handlers/TxTests.cpp +++ b/unittests/rpc/handlers/TxTests.cpp @@ -33,12 +33,13 @@ using TestTxHandler = BaseTxHandler; auto constexpr static TXNID = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD"; auto constexpr static NFTID = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"; auto constexpr static NFTID2 = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"; +constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; auto constexpr static ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; auto constexpr static ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; auto constexpr static CURRENCY = "0158415500000000C1F76FF6ECB0BAC600000000"; constexpr static auto CTID = "C002807000010002"; // seq 163952 txindex 1 netid 2 constexpr static auto SEQ_FROM_CTID = 163952; -auto constexpr static DEFAULT_OUT = R"({ +auto constexpr static DEFAULT_OUT_1 = R"({ "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Fee": "2", "Sequence": 100, @@ -72,6 +73,49 @@ auto constexpr static DEFAULT_OUT = R"({ }, "date": 123456, "ledger_index": 100, + "inLedger": 100, + "validated": true +})"; + +auto constexpr static DEFAULT_OUT_2 = R"({ + "hash": "2E2FBAAFF767227FE4381C4BE9855986A6B9F96C62F6E443731AB36F7BBB8A08", + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index": 100, + "meta": { + "AffectedNodes": [ + { + "CreatedNode": { + "LedgerEntryType": "Offer", + "NewFields": { + "TakerGets": "200", + "TakerPays": { + "currency": "0158415500000000C1F76FF6ECB0BAC600000000", + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "value": "300" + } + } + } + } + ], + "TransactionIndex": 100, + "TransactionResult": "tesSUCCESS" + }, + "tx_json": { + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "date": 123456, + "Fee": "2", + "ledger_index": 100, + "Sequence": 100, + "SigningPubKey": "74657374", + "TakerGets": { + "currency": "0158415500000000C1F76FF6ECB0BAC600000000", + "issuer": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "value": "200" + }, + "TakerPays": "300", + "TransactionType": "OfferCreate" + }, + "close_time_iso": "2000-01-01T00:00:00Z", "validated": true })"; @@ -282,9 +326,7 @@ TEST_F(RPCTxTest, DefaultParameter_API_v1) auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 1u}); ASSERT_TRUE(output); - auto v1Output = json::parse(DEFAULT_OUT); - v1Output.as_object()[JS(inLedger)] = v1Output.as_object()[JS(ledger_index)]; - EXPECT_EQ(*output, v1Output); + EXPECT_EQ(*output, json::parse(DEFAULT_OUT_1)); }); } @@ -333,6 +375,7 @@ TEST_F(RPCTxTest, PaymentTx_API_v2) tx.ledgerSequence = 100; EXPECT_CALL(*rawBackendPtr, fetchTransaction(ripple::uint256{TXNID}, _)).WillOnce(Return(tx)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence(tx.ledgerSequence, _)).WillOnce(Return(std::nullopt)); auto const rawETLPtr = dynamic_cast(mockETLServicePtr.get()); ASSERT_NE(rawETLPtr, nullptr); @@ -349,8 +392,9 @@ TEST_F(RPCTxTest, PaymentTx_API_v2) )); auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 2u}); ASSERT_TRUE(output); - EXPECT_TRUE(output->as_object().contains("DeliverMax")); - EXPECT_FALSE(output->as_object().contains("Amount")); + EXPECT_TRUE(output->as_object().contains("tx_json")); + EXPECT_TRUE(output->as_object().at("tx_json").as_object().contains("DeliverMax")); + EXPECT_FALSE(output->as_object().at("tx_json").as_object().contains("Amount")); }); } @@ -367,6 +411,8 @@ TEST_F(RPCTxTest, DefaultParameter_API_v2) tx.ledgerSequence = 100; EXPECT_CALL(*rawBackendPtr, fetchTransaction(ripple::uint256{TXNID}, _)).WillOnce(Return(tx)); + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, tx.ledgerSequence); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence(tx.ledgerSequence, _)).WillOnce(Return(ledgerinfo)); auto const rawETLPtr = dynamic_cast(mockETLServicePtr.get()); ASSERT_NE(rawETLPtr, nullptr); @@ -383,7 +429,7 @@ TEST_F(RPCTxTest, DefaultParameter_API_v2) )); auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 2u}); ASSERT_TRUE(output); - EXPECT_EQ(*output, json::parse(DEFAULT_OUT)); + EXPECT_EQ(*output, json::parse(DEFAULT_OUT_2)); }); }