diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index b3c9b18d2f8..e5d30b3a3d6 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -141,6 +141,31 @@ In API version 2, the following methods are no longer available: - `tx_history` - Instead, use other methods such as `account_tx` or `ledger` with the `transactions` field set to `true`. - `ledger_header` - Instead, use the `ledger` method. +#### Modifications to JSON transaction element in V2 + +In API version 2, JSON elements for transaction output have been changed and made consistent for all methods which output transactions: + +- JSON transaction element is named `tx_json` +- Binary transaction element is named `tx_blob` +- JSON transaction metadata element is named `meta` +- Binary transaction metadata element is named `meta_blob` + +Additionally, these elements are now consistently available next to `tx_json` (i.e. sibling elements), where possible: + +- `hash` - Transaction ID. This data was stored inside transaction output in API version 1, but in API version 2 is a sibling element. +- `ledger_index` - Ledger index (only set on validated ledgers) +- `ledger_hash` - Ledger hash (only set on closed or validated ledgers) +- `close_time_iso` - Ledger close time expressed in ISO 8601 time format (only set on validated ledgers) +- `validated` - Bool element set to `true` if the transaction is in a validated ledger, otherwise `false` + +This change affects the following methods: + +- `tx` - Transaction data moved into element `tx_json` (was inline inside `result`) or, if binary output was requested, moved from `tx` to `tx_blob`. Renamed binary transaction metadata element (if it was requested) from `meta` to `meta_blob`. Changed location of `hash` and added new elements +- `account_tx` - Renamed transaction element from `tx` to `tx_json`. Renamed binary transaction metadata element (if it was requested) from `meta` to `meta_blob`. Changed location of `hash` and added new elements +- `transaction_entry` - Renamed transaction metadata element from `metadata` to `meta`. Changed location of `hash` and added new elements +- `subscribe` - Renamed transaction element from `transaction` to `tx_json`. Changed location of `hash` and added new elements +- `sign`, `sign_for`, `submit` and `submit_multisigned` - Changed location of `hash` element. + #### Modifications to account_info response in V2 - `signer_lists` is returned in the root of the response. Previously, in API version 1, it was nested under `account_data`. (https://github.com/XRPLF/rippled/pull/3770) diff --git a/src/ripple/app/ledger/LedgerMaster.h b/src/ripple/app/ledger/LedgerMaster.h index 26738844536..e2ca3039935 100644 --- a/src/ripple/app/ledger/LedgerMaster.h +++ b/src/ripple/app/ledger/LedgerMaster.h @@ -215,6 +215,8 @@ class LedgerMaster : public AbstractFetchPackContainer void clearLedger(std::uint32_t seq); bool + isValidated(ReadView const& ledger); + bool getValidatedRange(std::uint32_t& minVal, std::uint32_t& maxVal); bool getFullValidatedRange(std::uint32_t& minVal, std::uint32_t& maxVal); diff --git a/src/ripple/app/ledger/LedgerToJson.h b/src/ripple/app/ledger/LedgerToJson.h index f658583885f..78947ca91d1 100644 --- a/src/ripple/app/ledger/LedgerToJson.h +++ b/src/ripple/app/ledger/LedgerToJson.h @@ -21,8 +21,10 @@ #define RIPPLE_APP_LEDGER_LEDGERTOJSON_H_INCLUDED #include +#include #include #include +#include #include #include #include @@ -41,6 +43,8 @@ struct LedgerFill LedgerEntryType t = ltANY) : ledger(l), options(o), txQueue(std::move(q)), type(t), context(ctx) { + if (context) + closeTime = context->ledgerMaster.getCloseTimeBySeq(ledger.seq()); } enum Options { @@ -58,6 +62,7 @@ struct LedgerFill std::vector txQueue; LedgerEntryType type; RPC::Context* context; + std::optional closeTime; }; /** Given a Ledger and options, fill a Json::Object or Json::Value with a diff --git a/src/ripple/app/ledger/impl/LedgerMaster.cpp b/src/ripple/app/ledger/impl/LedgerMaster.cpp index 050e2f3ef3d..857c0efcc28 100644 --- a/src/ripple/app/ledger/impl/LedgerMaster.cpp +++ b/src/ripple/app/ledger/impl/LedgerMaster.cpp @@ -603,6 +603,54 @@ LedgerMaster::clearLedger(std::uint32_t seq) mCompleteLedgers.erase(seq); } +bool +LedgerMaster::isValidated(ReadView const& ledger) +{ + if (app_.config().reporting()) + return true; // Reporting mode only supports validated ledger + + if (ledger.open()) + return false; + + if (ledger.info().validated) + return true; + + auto const seq = ledger.info().seq; + try + { + // Use the skip list in the last validated ledger to see if ledger + // comes before the last validated ledger (and thus has been + // validated). + auto const hash = walkHashBySeq(seq, InboundLedger::Reason::GENERIC); + + if (!hash || ledger.info().hash != *hash) + { + // This ledger's hash is not the hash of the validated ledger + if (hash) + { + assert(hash->isNonZero()); + uint256 valHash = + app_.getRelationalDatabase().getHashByIndex(seq); + if (valHash == ledger.info().hash) + { + // SQL database doesn't match ledger chain + clearLedger(seq); + } + } + return false; + } + } + catch (SHAMapMissingNode const& mn) + { + JLOG(m_journal.warn()) << "Ledger #" << seq << ": " << mn.what(); + return false; + } + + // Mark ledger as validated to save time if we see it again. + ledger.info().validated = true; + return true; +} + // returns Ledgers we have all the nodes for bool LedgerMaster::getFullValidatedRange( diff --git a/src/ripple/app/ledger/impl/LedgerToJson.cpp b/src/ripple/app/ledger/impl/LedgerToJson.cpp index 55123ba2362..d22cc7cd487 100644 --- a/src/ripple/app/ledger/impl/LedgerToJson.cpp +++ b/src/ripple/app/ledger/impl/LedgerToJson.cpp @@ -17,12 +17,14 @@ */ //============================================================================== +#include #include #include #include #include #include #include +#include #include #include @@ -83,6 +85,7 @@ fillJson(Object& json, bool closed, LedgerInfo const& info, bool bFull) json[jss::close_time_human] = to_string(info.closeTime); if (!getCloseAgree(info)) json[jss::close_time_estimated] = true; + json[jss::close_time_iso] = to_string_iso(info.closeTime); } } @@ -118,8 +121,48 @@ fillJsonTx( if (bBinary) { txJson[jss::tx_blob] = serializeHex(*txn); + if (fill.context->apiVersion > 1) + txJson[jss::hash] = to_string(txn->getTransactionID()); + + auto const json_meta = + (fill.context->apiVersion > 1 ? jss::meta_blob : jss::meta); if (stMeta) - txJson[jss::meta] = serializeHex(*stMeta); + txJson[json_meta] = serializeHex(*stMeta); + } + else if (fill.context->apiVersion > 1) + { + copyFrom( + txJson[jss::tx_json], + txn->getJson(JsonOptions::disable_API_prior_V2, false)); + txJson[jss::hash] = to_string(txn->getTransactionID()); + RPC::insertDeliverMax( + txJson[jss::tx_json], txnType, fill.context->apiVersion); + + if (stMeta) + { + txJson[jss::meta] = stMeta->getJson(JsonOptions::none); + + // If applicable, insert delivered amount + if (txnType == ttPAYMENT || txnType == ttCHECK_CASH) + RPC::insertDeliveredAmount( + txJson[jss::meta], + fill.ledger, + txn, + {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); + } + + if (!fill.ledger.open()) + txJson[jss::ledger_hash] = to_string(fill.ledger.info().hash); + + const bool validated = + fill.context->ledgerMaster.isValidated(fill.ledger); + txJson[jss::validated] = validated; + if (validated) + { + txJson[jss::ledger_index] = to_string(fill.ledger.seq()); + if (fill.closeTime) + txJson[jss::close_time_iso] = to_string_iso(*fill.closeTime); + } } else { @@ -254,7 +297,11 @@ fillJsonQueue(Object& json, LedgerFill const& fill) if (tx.lastResult) txJson["last_result"] = transToken(*tx.lastResult); - txJson[jss::tx] = fillJsonTx(fill, bBinary, bExpanded, tx.txn, nullptr); + auto&& temp = fillJsonTx(fill, bBinary, bExpanded, tx.txn, nullptr); + if (fill.context->apiVersion > 1) + copyFrom(txJson, temp); + else + copyFrom(txJson[jss::tx], temp); } } diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 077092ca7a5..b20ef5d37f7 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -63,6 +63,7 @@ #include #include #include +#include #include #include #include @@ -74,6 +75,7 @@ #include #include +#include #include #include #include @@ -3096,7 +3098,11 @@ NetworkOPsImp::transJson( transResultInfo(result, sToken, sHuman); jvObj[jss::type] = "transaction"; - jvObj[jss::transaction] = transaction->getJson(JsonOptions::none); + // NOTE jvObj which is not a finished object for either API version. After + // it's populated, we need to finish it for a specific API version. This is + // done in a loop, near the end of this function. + jvObj[jss::transaction] = + transaction->getJson(JsonOptions::disable_API_prior_V2, false); if (meta) { @@ -3105,13 +3111,16 @@ NetworkOPsImp::transJson( jvObj[jss::meta], *ledger, transaction, meta->get()); } + if (!ledger->open()) + jvObj[jss::ledger_hash] = to_string(ledger->info().hash); + if (validated) { jvObj[jss::ledger_index] = ledger->info().seq; - jvObj[jss::ledger_hash] = to_string(ledger->info().hash); jvObj[jss::transaction][jss::date] = ledger->info().closeTime.time_since_epoch().count(); jvObj[jss::validated] = true; + jvObj[jss::close_time_iso] = to_string_iso(ledger->info().closeTime); // WRITEME: Put the account next seq here } @@ -3144,6 +3153,7 @@ NetworkOPsImp::transJson( } } + std::string const hash = to_string(transaction->getTransactionID()); MultiApiJson multiObj({jvObj, jvObj}); // Minimum supported API version must match index 0 in MultiApiJson static_assert(apiVersionSelector(RPC::apiMinimumSupportedVersion)() == 0); @@ -3160,11 +3170,21 @@ NetworkOPsImp::transJson( assert(index < MultiApiJson::size); if (index != lastIndex) { - RPC::insertDeliverMax( - multiObj.val[index][jss::transaction], - transaction->getTxnType(), - apiVersion); lastIndex = index; + + Json::Value& jvTx = multiObj.val[index]; + RPC::insertDeliverMax( + jvTx[jss::transaction], transaction->getTxnType(), apiVersion); + + if (apiVersion > 1) + { + jvTx[jss::tx_json] = jvTx.removeMember(jss::transaction); + jvTx[jss::hash] = hash; + } + else + { + jvTx[jss::transaction][jss::hash] = hash; + } } } diff --git a/src/ripple/app/misc/Transaction.h b/src/ripple/app/misc/Transaction.h index 07802becfeb..36815ba0aa0 100644 --- a/src/ripple/app/misc/Transaction.h +++ b/src/ripple/app/misc/Transaction.h @@ -24,10 +24,11 @@ #include #include #include +#include #include #include #include -#include + #include #include @@ -99,13 +100,13 @@ class Transaction : public std::enable_shared_from_this, LedgerIndex getLedger() const { - return mInLedger; + return mLedgerIndex; } bool isValidated() const { - return mInLedger != 0; + return mLedgerIndex != 0; } TransStatus @@ -138,7 +139,7 @@ class Transaction : public std::enable_shared_from_this, void setLedger(LedgerIndex ledger) { - mInLedger = ledger; + mLedgerIndex = ledger; } /** @@ -386,7 +387,7 @@ class Transaction : public std::enable_shared_from_this, uint256 mTransactionID; - LedgerIndex mInLedger = 0; + LedgerIndex mLedgerIndex = 0; TransStatus mStatus = INVALID; TER mResult = temUNCERTAIN; bool mApplying = false; diff --git a/src/ripple/app/misc/impl/Transaction.cpp b/src/ripple/app/misc/impl/Transaction.cpp index 9adef982d01..c38a6b7438f 100644 --- a/src/ripple/app/misc/impl/Transaction.cpp +++ b/src/ripple/app/misc/impl/Transaction.cpp @@ -62,7 +62,7 @@ void Transaction::setStatus(TransStatus ts, std::uint32_t lseq) { mStatus = ts; - mInLedger = lseq; + mLedgerIndex = lseq; } TransStatus @@ -167,16 +167,25 @@ Transaction::load( Json::Value Transaction::getJson(JsonOptions options, bool binary) const { - Json::Value ret(mTransaction->getJson(JsonOptions::none, binary)); + // Note, we explicitly suppress `include_date` option here + Json::Value ret( + mTransaction->getJson(options & ~JsonOptions::include_date, binary)); - if (mInLedger) + if (mLedgerIndex) { - ret[jss::inLedger] = mInLedger; // Deprecated. - ret[jss::ledger_index] = mInLedger; + if (!(options & JsonOptions::disable_API_prior_V2)) + { + // Behaviour before API version 2 + ret[jss::inLedger] = mLedgerIndex; + } + + // TODO: disable_API_prior_V3 to disable output of both `date` and + // `ledger_index` elements (taking precedence over include_date) + ret[jss::ledger_index] = mLedgerIndex; - if (options == JsonOptions::include_date) + if (options & JsonOptions::include_date) { - auto ct = mApp.getLedgerMaster().getCloseTimeBySeq(mInLedger); + auto ct = mApp.getLedgerMaster().getCloseTimeBySeq(mLedgerIndex); if (ct) ret[jss::date] = ct->time_since_epoch().count(); } diff --git a/src/ripple/basics/chrono.h b/src/ripple/basics/chrono.h index f50d765d58f..ea82f928b7e 100644 --- a/src/ripple/basics/chrono.h +++ b/src/ripple/basics/chrono.h @@ -25,9 +25,12 @@ #include #include #include + #include #include +#include #include +#include namespace ripple { @@ -43,8 +46,19 @@ using weeks = std::chrono:: /** Clock for measuring the network time. The epoch is January 1, 2000 - epoch_offset = days(10957); // 2000-01-01 + + epoch_offset + = date(2000-01-01) - date(1970-0-01) + = days(10957) + = seconds(946684800) */ + +constexpr static std::chrono::seconds epoch_offset = + date::sys_days{date::year{2000} / 1 / 1} - + date::sys_days{date::year{1970} / 1 / 1}; + +static_assert(epoch_offset.count() == 946684800); + class NetClock { public: @@ -71,7 +85,25 @@ to_string(NetClock::time_point tp) // 2000-01-01 00:00:00 UTC is 946684800s from 1970-01-01 00:00:00 UTC using namespace std::chrono; return to_string( - system_clock::time_point{tp.time_since_epoch() + 946684800s}); + system_clock::time_point{tp.time_since_epoch() + epoch_offset}); +} + +template +std::string +to_string_iso(date::sys_time tp) +{ + using namespace std::chrono; + return date::format("%FT%TZ", tp); +} + +inline std::string +to_string_iso(NetClock::time_point tp) +{ + // 2000-01-01 00:00:00 UTC is 946684800s from 1970-01-01 00:00:00 UTC + // Note, NetClock::duration is seconds, as checked by static_assert + static_assert(std::is_same_v>); + return to_string_iso(date::sys_time{ + tp.time_since_epoch() + epoch_offset}); } /** A clock for measuring elapsed time. diff --git a/src/ripple/core/TimeKeeper.h b/src/ripple/core/TimeKeeper.h index 55970ec8227..e239a2f7565 100644 --- a/src/ripple/core/TimeKeeper.h +++ b/src/ripple/core/TimeKeeper.h @@ -22,6 +22,7 @@ #include #include + #include namespace ripple { @@ -37,7 +38,7 @@ class TimeKeeper : public beast::abstract_clock adjust(std::chrono::system_clock::time_point when) { return time_point(std::chrono::duration_cast( - when.time_since_epoch() - days(10957))); + when.time_since_epoch() - epoch_offset)); } public: diff --git a/src/ripple/protocol/STBase.h b/src/ripple/protocol/STBase.h index 914a3e0f60b..ec8c34a9ddd 100644 --- a/src/ripple/protocol/STBase.h +++ b/src/ripple/protocol/STBase.h @@ -31,7 +31,62 @@ #include namespace ripple { -enum class JsonOptions { none = 0, include_date = 1 }; +/// Note, should be treated as flags that can be | and & +struct JsonOptions +{ + using underlying_t = unsigned int; + underlying_t value; + + enum values : underlying_t { + // clang-format off + none = 0b0000'0000, + include_date = 0b0000'0001, + disable_API_prior_V2 = 0b0000'0010, + + // IMPORTANT `_all` must be union of all of the above; see also operator~ + _all = 0b0000'0011 + // clang-format on + }; + + constexpr JsonOptions(underlying_t v) noexcept : value(v) + { + } + + [[nodiscard]] constexpr explicit operator underlying_t() const noexcept + { + return value; + } + [[nodiscard]] constexpr explicit operator bool() const noexcept + { + return value != 0u; + } + [[nodiscard]] constexpr auto friend + operator==(JsonOptions lh, JsonOptions rh) noexcept -> bool = default; + [[nodiscard]] constexpr auto friend + operator!=(JsonOptions lh, JsonOptions rh) noexcept -> bool = default; + + /// Returns JsonOptions union of lh and rh + [[nodiscard]] constexpr JsonOptions friend + operator|(JsonOptions lh, JsonOptions rh) noexcept + { + return {lh.value | rh.value}; + } + + /// Returns JsonOptions intersection of lh and rh + [[nodiscard]] constexpr JsonOptions friend + operator&(JsonOptions lh, JsonOptions rh) noexcept + { + return {lh.value & rh.value}; + } + + /// Returns JsonOptions binary negation, can be used with & (above) for set + /// difference e.g. `(options & ~JsonOptions::include_date)` + [[nodiscard]] constexpr JsonOptions friend + operator~(JsonOptions v) noexcept + { + return {~v.value & static_cast(_all)}; + } +}; namespace detail { class STVar; diff --git a/src/ripple/protocol/STObject.h b/src/ripple/protocol/STObject.h index 19d4b264734..3e3862bf6c8 100644 --- a/src/ripple/protocol/STObject.h +++ b/src/ripple/protocol/STObject.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include diff --git a/src/ripple/protocol/STTx.h b/src/ripple/protocol/STTx.h index c6a9e053c3d..e166eb20dd4 100644 --- a/src/ripple/protocol/STTx.h +++ b/src/ripple/protocol/STTx.h @@ -29,6 +29,7 @@ #include #include #include + #include namespace ripple { @@ -108,6 +109,7 @@ class STTx final : public STObject, public CountedObject Json::Value getJson(JsonOptions options) const override; + Json::Value getJson(JsonOptions options, bool binary) const; diff --git a/src/ripple/protocol/impl/STTx.cpp b/src/ripple/protocol/impl/STTx.cpp index 1ce4ddb64b7..8106e997f3a 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -34,6 +34,7 @@ #include #include #include + #include #include #include @@ -236,15 +237,29 @@ Json::Value STTx::getJson(JsonOptions) const Json::Value STTx::getJson(JsonOptions options, bool binary) const { + bool const V1 = !(options & JsonOptions::disable_API_prior_V2); + if (binary) { - Json::Value ret; Serializer s = STObject::getSerializer(); - ret[jss::tx] = strHex(s.peekData()); - ret[jss::hash] = to_string(getTransactionID()); - return ret; + std::string const dataBin = strHex(s.peekData()); + + if (V1) + { + Json::Value ret(Json::objectValue); + ret[jss::tx] = dataBin; + ret[jss::hash] = to_string(getTransactionID()); + return ret; + } + else + return Json::Value{dataBin}; } - return getJson(options); + + Json::Value ret = STObject::getJson(JsonOptions::none); + if (V1) + ret[jss::hash] = to_string(getTransactionID()); + + return ret; } std::string const& diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index d4b213bcb1b..8a701defad8 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -230,6 +230,8 @@ JSS(close); // out: BookChanges JSS(close_flags); // out: LedgerToJson JSS(close_time); // in: Application, out: NetworkOPs, // RCLCxPeerPos, LedgerToJson +JSS(close_time_iso); // out: Tx, NetworkOPs, TransactionEntry + // AccountTx, LedgerToJson JSS(close_time_estimated); // in: Application, out: LedgerToJson JSS(close_time_human); // out: LedgerToJson JSS(close_time_offset); // out: NetworkOPs @@ -460,6 +462,7 @@ JSS(median_fee); // out: TxQ JSS(median_level); // out: TxQ JSS(message); // error. JSS(meta); // out: NetworkOPs, AccountTx*, Tx +JSS(meta_blob); // out: NetworkOPs, AccountTx*, Tx JSS(metaData); JSS(metadata); // out: TransactionEntry JSS(method); // RPC diff --git a/src/ripple/rpc/handlers/AMMInfo.cpp b/src/ripple/rpc/handlers/AMMInfo.cpp index 11e124afb44..a1be636cafd 100644 --- a/src/ripple/rpc/handlers/AMMInfo.cpp +++ b/src/ripple/rpc/handlers/AMMInfo.cpp @@ -16,6 +16,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ //============================================================================== +#include #include #include #include @@ -66,7 +67,7 @@ to_iso8601(NetClock::time_point tp) return date::format( "%Y-%Om-%dT%H:%M:%OS%z", date::sys_time( - system_clock::time_point{tp.time_since_epoch() + 946684800s})); + system_clock::time_point{tp.time_since_epoch() + epoch_offset})); } Json::Value @@ -244,8 +245,7 @@ doAMMInfo(RPC::JsonContext& context) if (!result.isMember(jss::ledger_index) && !result.isMember(jss::ledger_hash)) result[jss::ledger_current_index] = ledger->info().seq; - result[jss::validated] = - RPC::isValidated(context.ledgerMaster, *ledger, context.app); + result[jss::validated] = context.ledgerMaster.isValidated(*ledger); return result; } diff --git a/src/ripple/rpc/handlers/AccountTx.cpp b/src/ripple/rpc/handlers/AccountTx.cpp index bd939a92f1c..addd9ea0f39 100644 --- a/src/ripple/rpc/handlers/AccountTx.cpp +++ b/src/ripple/rpc/handlers/AccountTx.cpp @@ -37,7 +37,6 @@ #include #include #include -#include #include @@ -195,8 +194,8 @@ getLedgerRange( return status; } - bool validated = RPC::isValidated( - context.ledgerMaster, *ledgerView, context.app); + bool validated = + context.ledgerMaster.isValidated(*ledgerView); if (!validated || ledgerView->info().seq > uValidatedMax || ledgerView->info().seq < uValidatedMin) @@ -325,16 +324,39 @@ populateJsonResponse( if (txn) { Json::Value& jvObj = jvTxns.append(Json::objectValue); + jvObj[jss::validated] = true; + + auto const json_tx = + (context.apiVersion > 1 ? jss::tx_json : jss::tx); + if (context.apiVersion > 1) + { + jvObj[json_tx] = txn->getJson( + JsonOptions::include_date | + JsonOptions::disable_API_prior_V2, + false); + jvObj[jss::hash] = to_string(txn->getID()); + jvObj[jss::ledger_index] = txn->getLedger(); + jvObj[jss::ledger_hash] = + to_string(context.ledgerMaster.getHashBySeq( + txn->getLedger())); + + if (auto closeTime = + context.ledgerMaster.getCloseTimeBySeq( + txn->getLedger())) + jvObj[jss::close_time_iso] = + to_string_iso(*closeTime); + } + else + jvObj[json_tx] = + txn->getJson(JsonOptions::include_date); - jvObj[jss::tx] = txn->getJson(JsonOptions::include_date); auto const& sttx = txn->getSTransaction(); RPC::insertDeliverMax( - jvObj[jss::tx], sttx->getTxnType(), context.apiVersion); + jvObj[json_tx], sttx->getTxnType(), context.apiVersion); if (txnMeta) { jvObj[jss::meta] = txnMeta->getJson(JsonOptions::include_date); - jvObj[jss::validated] = true; insertDeliveredAmount( jvObj[jss::meta], context, txn, *txnMeta); insertNFTSyntheticInJson(jvObj, sttx, *txnMeta); @@ -352,7 +374,9 @@ populateJsonResponse( Json::Value& jvObj = jvTxns.append(Json::objectValue); jvObj[jss::tx_blob] = strHex(std::get<0>(binaryData)); - jvObj[jss::meta] = strHex(std::get<1>(binaryData)); + auto const json_meta = + (context.apiVersion > 1 ? jss::meta_blob : jss::meta); + jvObj[json_meta] = strHex(std::get<1>(binaryData)); jvObj[jss::ledger_index] = std::get<2>(binaryData); jvObj[jss::validated] = true; } diff --git a/src/ripple/rpc/handlers/LedgerHandler.cpp b/src/ripple/rpc/handlers/LedgerHandler.cpp index 6b4fc77367b..623cb8d75ac 100644 --- a/src/ripple/rpc/handlers/LedgerHandler.cpp +++ b/src/ripple/rpc/handlers/LedgerHandler.cpp @@ -27,7 +27,6 @@ #include #include #include -#include namespace ripple { namespace RPC { @@ -301,8 +300,7 @@ doLedgerGrpc(RPC::GRPCContext& context) response.set_skiplist_included(true); } - response.set_validated( - RPC::isValidated(context.ledgerMaster, *ledger, context.app)); + response.set_validated(context.ledgerMaster.isValidated(*ledger)); auto end = std::chrono::system_clock::now(); auto duration = diff --git a/src/ripple/rpc/handlers/SignFor.cpp b/src/ripple/rpc/handlers/SignFor.cpp index ac76fa0d8a1..722cf7da157 100644 --- a/src/ripple/rpc/handlers/SignFor.cpp +++ b/src/ripple/rpc/handlers/SignFor.cpp @@ -46,6 +46,7 @@ doSignFor(RPC::JsonContext& context) auto ret = RPC::transactionSignFor( context.params, + context.apiVersion, failType, context.role, context.ledgerMaster.getValidatedLedgerAge(), diff --git a/src/ripple/rpc/handlers/SignHandler.cpp b/src/ripple/rpc/handlers/SignHandler.cpp index 15d433da49c..4d89cdcb2e0 100644 --- a/src/ripple/rpc/handlers/SignHandler.cpp +++ b/src/ripple/rpc/handlers/SignHandler.cpp @@ -45,6 +45,7 @@ doSign(RPC::JsonContext& context) auto ret = RPC::transactionSign( context.params, + context.apiVersion, failType, context.role, context.ledgerMaster.getValidatedLedgerAge(), diff --git a/src/ripple/rpc/handlers/Submit.cpp b/src/ripple/rpc/handlers/Submit.cpp index 8a702c5bd3e..8e998f1ea6c 100644 --- a/src/ripple/rpc/handlers/Submit.cpp +++ b/src/ripple/rpc/handlers/Submit.cpp @@ -63,6 +63,7 @@ doSubmit(RPC::JsonContext& context) auto ret = RPC::transactionSubmit( context.params, + context.apiVersion, failType, context.role, context.ledgerMaster.getValidatedLedgerAge(), diff --git a/src/ripple/rpc/handlers/SubmitMultiSigned.cpp b/src/ripple/rpc/handlers/SubmitMultiSigned.cpp index 9b455a1961f..82fa52a4623 100644 --- a/src/ripple/rpc/handlers/SubmitMultiSigned.cpp +++ b/src/ripple/rpc/handlers/SubmitMultiSigned.cpp @@ -45,6 +45,7 @@ doSubmitMultiSigned(RPC::JsonContext& context) return RPC::transactionSubmitMultiSigned( context.params, + context.apiVersion, failType, context.role, context.ledgerMaster.getValidatedLedgerAge(), diff --git a/src/ripple/rpc/handlers/TransactionEntry.cpp b/src/ripple/rpc/handlers/TransactionEntry.cpp index 677581db6a3..6d157891d1c 100644 --- a/src/ripple/rpc/handlers/TransactionEntry.cpp +++ b/src/ripple/rpc/handlers/TransactionEntry.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -71,11 +72,39 @@ doTransactionEntry(RPC::JsonContext& context) } else { - jvResult[jss::tx_json] = sttx->getJson(JsonOptions::none); + if (context.apiVersion > 1) + { + jvResult[jss::tx_json] = + sttx->getJson(JsonOptions::disable_API_prior_V2); + jvResult[jss::hash] = to_string(sttx->getTransactionID()); + + if (!lpLedger->open()) + jvResult[jss::ledger_hash] = to_string( + context.ledgerMaster.getHashBySeq(lpLedger->seq())); + + bool const validated = + context.ledgerMaster.isValidated(*lpLedger); + + jvResult[jss::validated] = validated; + if (validated) + { + jvResult[jss::ledger_index] = lpLedger->seq(); + if (auto closeTime = context.ledgerMaster.getCloseTimeBySeq( + lpLedger->seq())) + jvResult[jss::close_time_iso] = + to_string_iso(*closeTime); + } + } + else + jvResult[jss::tx_json] = sttx->getJson(JsonOptions::none); + RPC::insertDeliverMax( jvResult[jss::tx_json], sttx->getTxnType(), context.apiVersion); + + auto const json_meta = + (context.apiVersion > 1 ? jss::meta : jss::metadata); if (stobj) - jvResult[jss::metadata] = stobj->getJson(JsonOptions::none); + jvResult[json_meta] = stobj->getJson(JsonOptions::none); // 'accounts' // 'engine_...' // 'ledger_...' diff --git a/src/ripple/rpc/handlers/Tx.cpp b/src/ripple/rpc/handlers/Tx.cpp index 92d0e4dd673..0237fef22ac 100644 --- a/src/ripple/rpc/handlers/Tx.cpp +++ b/src/ripple/rpc/handlers/Tx.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,7 @@ #include #include #include + #include #include @@ -55,6 +57,8 @@ struct TxResult std::variant, Blob> meta; bool validated = false; std::optional ctid; + std::optional closeTime; + std::optional ledgerHash; TxSearched searchedAll; }; @@ -140,6 +144,13 @@ doTxPostgres(RPC::Context& context, TxArgs const& args) *(args.hash), res.txn->getLedger(), *meta); } res.validated = true; + + auto const ledgerInfo = + context.app.getRelationalDatabase().getLedgerInfoByIndex( + locator.getLedgerSequence()); + res.closeTime = ledgerInfo->closeTime; + res.ledgerHash = ledgerInfo->hash; + return {res, rpcSUCCESS}; } else @@ -257,6 +268,9 @@ doTxHelp(RPC::Context& context, TxArgs args) std::shared_ptr ledger = context.ledgerMaster.getLedgerBySeq(txn->getLedger()); + if (ledger && !ledger->open()) + result.ledgerHash = ledger->info().hash; + if (ledger && meta) { if (args.binary) @@ -269,6 +283,9 @@ doTxHelp(RPC::Context& context, TxArgs args) } result.validated = isValidated( context.ledgerMaster, ledger->info().seq, ledger->info().hash); + if (result.validated) + result.closeTime = + context.ledgerMaster.getCloseTimeBySeq(txn->getLedger()); // compute outgoing CTID uint32_t lgrSeq = ledger->info().seq; @@ -311,17 +328,52 @@ populateJsonResponse( // no errors else if (result.txn) { - response = result.txn->getJson(JsonOptions::include_date, args.binary); auto const& sttx = result.txn->getSTransaction(); - if (!args.binary) - RPC::insertDeliverMax( - response, sttx->getTxnType(), context.apiVersion); + if (context.apiVersion > 1) + { + constexpr auto optionsJson = + JsonOptions::include_date | JsonOptions::disable_API_prior_V2; + if (args.binary) + response[jss::tx_blob] = result.txn->getJson(optionsJson, true); + else + { + response[jss::tx_json] = result.txn->getJson(optionsJson); + RPC::insertDeliverMax( + response[jss::tx_json], + sttx->getTxnType(), + context.apiVersion); + } + + // Note, result.ledgerHash is only set in a closed or validated + // ledger - as seen in `doTxHelp` and `doTxPostgres` + if (result.ledgerHash) + response[jss::ledger_hash] = to_string(*result.ledgerHash); + + response[jss::hash] = to_string(result.txn->getID()); + if (result.validated) + { + response[jss::ledger_index] = result.txn->getLedger(); + if (result.closeTime) + response[jss::close_time_iso] = + to_string_iso(*result.closeTime); + } + } + else + { + response = + result.txn->getJson(JsonOptions::include_date, args.binary); + if (!args.binary) + RPC::insertDeliverMax( + response, sttx->getTxnType(), context.apiVersion); + } // populate binary metadata if (auto blob = std::get_if(&result.meta)) { assert(args.binary); - response[jss::meta] = strHex(makeSlice(*blob)); + auto json_meta = + (context.apiVersion > 1 ? jss::meta_blob : jss::meta); + response[json_meta] = strHex(makeSlice(*blob)); } // populate meta data else if (auto m = std::get_if>(&result.meta)) diff --git a/src/ripple/rpc/impl/RPCHelpers.cpp b/src/ripple/rpc/impl/RPCHelpers.cpp index a9cc0f9fffe..672095fe950 100644 --- a/src/ripple/rpc/impl/RPCHelpers.cpp +++ b/src/ripple/rpc/impl/RPCHelpers.cpp @@ -600,59 +600,6 @@ getLedger<>( template Status getLedger<>(std::shared_ptr&, uint256 const&, Context&); -bool -isValidated( - LedgerMaster& ledgerMaster, - ReadView const& ledger, - Application& app) -{ - if (app.config().reporting()) - return true; - - if (ledger.open()) - return false; - - if (ledger.info().validated) - return true; - - auto seq = ledger.info().seq; - try - { - // Use the skip list in the last validated ledger to see if ledger - // comes before the last validated ledger (and thus has been - // validated). - auto hash = - ledgerMaster.walkHashBySeq(seq, InboundLedger::Reason::GENERIC); - - if (!hash || ledger.info().hash != *hash) - { - // This ledger's hash is not the hash of the validated ledger - if (hash) - { - assert(hash->isNonZero()); - uint256 valHash = - app.getRelationalDatabase().getHashByIndex(seq); - if (valHash == ledger.info().hash) - { - // SQL database doesn't match ledger chain - ledgerMaster.clearLedger(seq); - } - } - return false; - } - } - catch (SHAMapMissingNode const& mn) - { - auto stream = app.journal("RPCHandler").warn(); - JLOG(stream) << "Ledger #" << seq << ": " << mn.what(); - return false; - } - - // Mark ledger as validated to save time if we see it again. - ledger.info().validated = true; - return true; -} - // The previous version of the lookupLedger command would accept the // "ledger_index" argument as a string and silently treat it as a request to // return the current ledger which, while not strictly wrong, could cause a lot @@ -693,8 +640,7 @@ lookupLedger( result[jss::ledger_current_index] = info.seq; } - result[jss::validated] = - isValidated(context.ledgerMaster, *ledger, context.app); + result[jss::validated] = context.ledgerMaster.isValidated(*ledger); return Status::OK; } diff --git a/src/ripple/rpc/impl/RPCHelpers.h b/src/ripple/rpc/impl/RPCHelpers.h index 516f66fc620..97015f1a35d 100644 --- a/src/ripple/rpc/impl/RPCHelpers.h +++ b/src/ripple/rpc/impl/RPCHelpers.h @@ -168,12 +168,6 @@ ledgerFromSpecifier( org::xrpl::rpc::v1::LedgerSpecifier const& specifier, Context& context); -bool -isValidated( - LedgerMaster& ledgerMaster, - ReadView const& ledger, - Application& app); - hash_set parseAccountIds(Json::Value const& jvArray); diff --git a/src/ripple/rpc/impl/TransactionSign.cpp b/src/ripple/rpc/impl/TransactionSign.cpp index 5dbfa49aef9..48a9c66d81c 100644 --- a/src/ripple/rpc/impl/TransactionSign.cpp +++ b/src/ripple/rpc/impl/TransactionSign.cpp @@ -645,12 +645,20 @@ transactionConstructImpl( } static Json::Value -transactionFormatResultImpl(Transaction::pointer tpTrans) +transactionFormatResultImpl(Transaction::pointer tpTrans, unsigned apiVersion) { Json::Value jvResult; try { - jvResult[jss::tx_json] = tpTrans->getJson(JsonOptions::none); + if (apiVersion > 1) + { + jvResult[jss::tx_json] = + tpTrans->getJson(JsonOptions::disable_API_prior_V2); + jvResult[jss::hash] = to_string(tpTrans->getID()); + } + else + jvResult[jss::tx_json] = tpTrans->getJson(JsonOptions::none); + jvResult[jss::tx_blob] = strHex(tpTrans->getSTransaction()->getSerializer().peekData()); @@ -777,6 +785,7 @@ checkFee( Json::Value transactionSign( Json::Value jvRequest, + unsigned apiVersion, NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, @@ -807,13 +816,14 @@ transactionSign( if (!txn.second) return txn.first; - return transactionFormatResultImpl(txn.second); + return transactionFormatResultImpl(txn.second, apiVersion); } /** Returns a Json::objectValue. */ Json::Value transactionSubmit( Json::Value jvRequest, + unsigned apiVersion, NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, @@ -853,7 +863,7 @@ transactionSubmit( rpcINTERNAL, "Exception occurred during transaction submission."); } - return transactionFormatResultImpl(txn.second); + return transactionFormatResultImpl(txn.second, apiVersion); } namespace detail { @@ -943,6 +953,7 @@ sortAndValidateSigners(STArray& signers, AccountID const& signingForID) Json::Value transactionSignFor( Json::Value jvRequest, + unsigned apiVersion, NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, @@ -1043,13 +1054,14 @@ transactionSignFor( if (!txn.second) return txn.first; - return transactionFormatResultImpl(txn.second); + return transactionFormatResultImpl(txn.second, apiVersion); } /** Returns a Json::objectValue. */ Json::Value transactionSubmitMultiSigned( Json::Value jvRequest, + unsigned apiVersion, NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, @@ -1236,7 +1248,7 @@ transactionSubmitMultiSigned( rpcINTERNAL, "Exception occurred during transaction submission."); } - return transactionFormatResultImpl(txn.second); + return transactionFormatResultImpl(txn.second, apiVersion); } } // namespace RPC diff --git a/src/ripple/rpc/impl/TransactionSign.h b/src/ripple/rpc/impl/TransactionSign.h index a396e65af52..48d2859ccf5 100644 --- a/src/ripple/rpc/impl/TransactionSign.h +++ b/src/ripple/rpc/impl/TransactionSign.h @@ -96,6 +96,7 @@ getProcessTxnFn(NetworkOPs& netOPs) Json::Value transactionSign( Json::Value params, // Passed by value so it can be modified locally. + unsigned apiVersion, NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, @@ -105,6 +106,7 @@ transactionSign( Json::Value transactionSubmit( Json::Value params, // Passed by value so it can be modified locally. + unsigned apiVersion, NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, @@ -116,6 +118,7 @@ transactionSubmit( Json::Value transactionSignFor( Json::Value params, // Passed by value so it can be modified locally. + unsigned apiVersion, NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, @@ -125,6 +128,7 @@ transactionSignFor( Json::Value transactionSubmitMultiSigned( Json::Value params, // Passed by value so it can be modified locally. + unsigned apiVersion, NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index 8c583ee1254..3834d623dca 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -120,22 +120,56 @@ class AccountTx_test : public beast::unit_test::suite // All other ledgers have no txs auto hasTxs = [apiVersion](Json::Value const& j) { - return j.isMember(jss::result) && - (j[jss::result][jss::status] == "success") && - (j[jss::result][jss::transactions].size() == 2) && - (j[jss::result][jss::transactions][0u][jss::tx] - [jss::TransactionType] == jss::AccountSet) && - (j[jss::result][jss::transactions][1u][jss::tx] - [jss::TransactionType] == jss::Payment) && - (j[jss::result][jss::transactions][1u][jss::tx] - [jss::DeliverMax] == "10000000010") && - ((apiVersion > 1 && - !j[jss::result][jss::transactions][1u][jss::tx].isMember( - jss::Amount)) || - (apiVersion <= 1 && - j[jss::result][jss::transactions][1u][jss::tx][jss::Amount] == - j[jss::result][jss::transactions][1u][jss::tx] - [jss::DeliverMax])); + switch (apiVersion) + { + case 1: + return j.isMember(jss::result) && + (j[jss::result][jss::status] == "success") && + (j[jss::result][jss::transactions].size() == 2) && + (j[jss::result][jss::transactions][0u][jss::tx] + [jss::TransactionType] == jss::AccountSet) && + (j[jss::result][jss::transactions][1u][jss::tx] + [jss::TransactionType] == jss::Payment) && + (j[jss::result][jss::transactions][1u][jss::tx] + [jss::DeliverMax] == "10000000010") && + (j[jss::result][jss::transactions][1u][jss::tx] + [jss::Amount] == + j[jss::result][jss::transactions][1u][jss::tx] + [jss::DeliverMax]); + case 2: + if (j.isMember(jss::result) && + (j[jss::result][jss::status] == "success") && + (j[jss::result][jss::transactions].size() == 2) && + (j[jss::result][jss::transactions][0u][jss::tx_json] + [jss::TransactionType] == jss::AccountSet)) + { + auto const& payment = + j[jss::result][jss::transactions][1u]; + + return (payment.isMember(jss::tx_json)) && + (payment[jss::tx_json][jss::TransactionType] == + jss::Payment) && + (payment[jss::tx_json][jss::DeliverMax] == + "10000000010") && + (!payment[jss::tx_json].isMember(jss::Amount)) && + (!payment[jss::tx_json].isMember(jss::hash)) && + (payment[jss::hash] == + "9F3085D85F472D1CC29627F260DF68EDE59D42D1D0C33E345" + "ECF0D4CE981D0A8") && + (payment[jss::validated] == true) && + (payment[jss::ledger_index] == 3) && + (payment[jss::ledger_hash] == + "5476DCD816EA04CBBA57D47BBF1FC58A5217CC93A5ADD79CB" + "580A5AFDD727E33") && + (payment[jss::close_time_iso] == + "2000-01-01T00:00:10Z"); + } + else + return false; + + default: + return false; + } }; auto noTxs = [](Json::Value const& j) { diff --git a/src/test/rpc/JSONRPC_test.cpp b/src/test/rpc/JSONRPC_test.cpp index 5d4c09ef8d1..1e8ce554be3 100644 --- a/src/test/rpc/JSONRPC_test.cpp +++ b/src/test/rpc/JSONRPC_test.cpp @@ -2536,17 +2536,19 @@ class JSONRPC_test : public beast::unit_test::suite // A list of all the functions we want to test. using signFunc = Json::Value (*)( Json::Value params, + unsigned int apiVersion, NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, - Application & app); + Application& app); using submitFunc = Json::Value (*)( Json::Value params, + unsigned int apiVersion, NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, - Application & app, + Application& app, ProcessTransactionFn const& processTransaction, RPC::SubmitSync sync); @@ -2586,6 +2588,7 @@ class JSONRPC_test : public beast::unit_test::suite assert(get<1>(testFunc) == nullptr); result = signFn( req, + 1, NetworkOPs::FailHard::yes, testRole, 1s, @@ -2597,6 +2600,7 @@ class JSONRPC_test : public beast::unit_test::suite assert(submitFn != nullptr); result = submitFn( req, + 1, NetworkOPs::FailHard::yes, testRole, 1s, diff --git a/src/test/rpc/Subscribe_test.cpp b/src/test/rpc/Subscribe_test.cpp index 7725390f6b6..ea815eb27ed 100644 --- a/src/test/rpc/Subscribe_test.cpp +++ b/src/test/rpc/Subscribe_test.cpp @@ -22,6 +22,7 @@ #include #include #include +#include "ripple/json/json_value.h" #include #include #include @@ -307,6 +308,85 @@ class Subscribe_test : public beast::unit_test::suite BEAST_EXPECT(jv[jss::status] == "success"); } + void + testTransactions_APIv2() + { + testcase("transactions API version 2"); + + using namespace std::chrono_literals; + using namespace jtx; + Env env(*this); + auto wsc = makeWSClient(env.app().config()); + Json::Value stream{Json::objectValue}; + + { + // RPC subscribe to transactions stream + stream[jss::api_version] = 2; + stream[jss::streams] = Json::arrayValue; + stream[jss::streams].append("transactions"); + auto jv = wsc->invoke("subscribe", stream); + if (wsc->version() == 2) + { + BEAST_EXPECT( + jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0"); + BEAST_EXPECT( + jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0"); + BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5); + } + BEAST_EXPECT(jv[jss::status] == "success"); + } + + { + env.fund(XRP(10000), "alice"); + env.close(); + + // Check stream update for payment transaction + BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { + return jv[jss::meta]["AffectedNodes"][1u]["CreatedNode"] + ["NewFields"][jss::Account] // + == Account("alice").human() && + jv[jss::close_time_iso] // + == "2000-01-01T00:00:10Z" && + jv[jss::validated] == true && // + jv[jss::ledger_hash] == + "0F1A9E0C109ADEF6DA2BDE19217C12BBEC57174CBDBD212B0EBDC1CEDB" + "853185" && // + !jv[jss::inLedger] && + jv[jss::ledger_index] == 3 && // + jv[jss::tx_json][jss::TransactionType] // + == jss::Payment && + jv[jss::tx_json][jss::DeliverMax] // + == "10000000010" && + !jv[jss::tx_json].isMember(jss::Amount) && + jv[jss::tx_json][jss::Fee] // + == "10" && + jv[jss::tx_json][jss::Sequence] // + == 1; + })); + + // Check stream update for accountset transaction + BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { + return jv[jss::meta]["AffectedNodes"][0u]["ModifiedNode"] + ["FinalFields"][jss::Account] == + Account("alice").human(); + })); + } + + { + // RPC unsubscribe + auto jv = wsc->invoke("unsubscribe", stream); + if (wsc->version() == 2) + { + BEAST_EXPECT( + jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0"); + BEAST_EXPECT( + jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0"); + BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5); + } + BEAST_EXPECT(jv[jss::status] == "success"); + } + } + void testManifests() { @@ -1223,6 +1303,7 @@ class Subscribe_test : public beast::unit_test::suite testServer(); testLedger(); testTransactions(); + testTransactions_APIv2(); testManifests(); testValidations(all - xrpFees); testValidations(all); diff --git a/src/test/rpc/TransactionEntry_test.cpp b/src/test/rpc/TransactionEntry_test.cpp index 60225f4621d..ae988343a6c 100644 --- a/src/test/rpc/TransactionEntry_test.cpp +++ b/src/test/rpc/TransactionEntry_test.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -143,21 +144,23 @@ class TransactionEntry_test : public beast::unit_test::suite } void - testRequest() + testRequest(unsigned apiVersion) { - testcase("Basic request"); + testcase("Basic request API version " + std::to_string(apiVersion)); using namespace test::jtx; Env env{*this}; - auto check_tx = [this, &env]( + auto check_tx = [this, &env, apiVersion]( int index, std::string const txhash, - std::string const expected_json = "") { + std::string const expected_json = "", + std::string const close_time_iso = "") { // first request using ledger_index to lookup - Json::Value const resIndex{[&env, index, &txhash]() { + Json::Value const resIndex{[&env, index, &txhash, apiVersion]() { Json::Value params{Json::objectValue}; params[jss::ledger_index] = index; params[jss::tx_hash] = txhash; + params[jss::api_version] = apiVersion; return env.client().invoke( "transaction_entry", params)[jss::result]; }()}; @@ -165,7 +168,19 @@ class TransactionEntry_test : public beast::unit_test::suite if (!BEAST_EXPECTS(resIndex.isMember(jss::tx_json), txhash)) return; - BEAST_EXPECT(resIndex[jss::tx_json][jss::hash] == txhash); + if (apiVersion > 1) + { + BEAST_EXPECT(resIndex[jss::hash] == txhash); + BEAST_EXPECT(resIndex[jss::validated] == true); + BEAST_EXPECT(!resIndex[jss::tx_json].isMember(jss::Amount)); + + if (BEAST_EXPECT(!close_time_iso.empty())) + BEAST_EXPECT( + resIndex[jss::close_time_iso] == close_time_iso); + } + else + BEAST_EXPECT(resIndex[jss::tx_json][jss::hash] == txhash); + if (!expected_json.empty()) { Json::Value expected; @@ -198,12 +213,14 @@ class TransactionEntry_test : public beast::unit_test::suite Json::Value params{Json::objectValue}; params[jss::ledger_hash] = resIndex[jss::ledger_hash]; params[jss::tx_hash] = txhash; + params[jss::api_version] = apiVersion; Json::Value const resHash = env.client().invoke( "transaction_entry", params)[jss::result]; BEAST_EXPECT(resHash == resIndex); } // Use the command line form with the index. + if (apiVersion == RPC::apiMaximumSupportedVersion) { Json::Value const clIndex{env.rpc( "transaction_entry", txhash, std::to_string(index))}; @@ -211,6 +228,7 @@ class TransactionEntry_test : public beast::unit_test::suite } // Use the command line form with the ledger_hash. + if (apiVersion == RPC::apiMaximumSupportedVersion) { Json::Value const clHash{env.rpc( "transaction_entry", @@ -235,7 +253,10 @@ class TransactionEntry_test : public beast::unit_test::suite // these are actually AccountSet txs because fund does two txs and // env.tx only reports the last one - check_tx(env.closed()->seq(), fund_1_tx, R"( + check_tx( + env.closed()->seq(), + fund_1_tx, + R"( { "Account" : "r4nmQNH4Fhjfh6cHDbvVSsBv7KySbj4cBf", "Fee" : "10", @@ -244,10 +265,13 @@ class TransactionEntry_test : public beast::unit_test::suite "SigningPubKey" : "0324CAAFA2212D2AEAB9D42D481535614AED486293E1FB1380FF070C3DD7FB4264", "TransactionType" : "AccountSet", "TxnSignature" : "3044022007B35E3B99460534FF6BC3A66FBBA03591C355CC38E38588968E87CCD01BE229022071A443026DE45041B55ABB1CC76812A87EA701E475BBB7E165513B4B242D3474", - "hash" : "F4E9DF90D829A9E8B423FF68C34413E240D8D8BB0EFD080DF08114ED398E2506" } -)"); - check_tx(env.closed()->seq(), fund_2_tx, R"( +)", + "2000-01-01T00:00:10Z"); + check_tx( + env.closed()->seq(), + fund_2_tx, + R"( { "Account" : "rGpeQzUWFu4fMhJHZ1Via5aqFC3A5twZUD", "Fee" : "10", @@ -256,9 +280,9 @@ class TransactionEntry_test : public beast::unit_test::suite "SigningPubKey" : "03CFF28E067A2CCE6CC5A598C0B845CBD3F30A7863BE9C0DD55F4960EFABCCF4D0", "TransactionType" : "AccountSet", "TxnSignature" : "3045022100C8857FC0759A2AC0D2F320684691A66EAD252EAED9EF88C79791BC58BFCC9D860220421722286487DD0ED6BBA626CE6FCBDD14289F7F4726870C3465A4054C2702D7", - "hash" : "6853CD8226A05068C951CB1F54889FF4E40C5B440DC1C5BA38F114C4E0B1E705" } -)"); +)", + "2000-01-01T00:00:10Z"); env.trust(A2["USD"](1000), A1); // the trust tx is actually a payment since the trust method @@ -272,7 +296,10 @@ class TransactionEntry_test : public beast::unit_test::suite boost::lexical_cast(env.tx()->getTransactionID()); env.close(); - check_tx(env.closed()->seq(), trust_tx, R"( + check_tx( + env.closed()->seq(), + trust_tx, + R"( { "Account" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "DeliverMax" : "10", @@ -283,9 +310,9 @@ class TransactionEntry_test : public beast::unit_test::suite "SigningPubKey" : "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020", "TransactionType" : "Payment", "TxnSignature" : "3044022033D9EBF7F02950AF2F6B13C07AEE641C8FEBDD540A338FCB9027A965A4AED35B02206E4E227DCC226A3456C0FEF953449D21645A24EB63CA0BB7C5B62470147FD1D1", - "hash" : "C992D97D88FF444A1AB0C06B27557EC54B7F7DA28254778E60238BEA88E0C101" } -)"); +)", + "2000-01-01T00:00:20Z"); check_tx( env.closed()->seq(), @@ -306,9 +333,9 @@ class TransactionEntry_test : public beast::unit_test::suite "SigningPubKey" : "03CFF28E067A2CCE6CC5A598C0B845CBD3F30A7863BE9C0DD55F4960EFABCCF4D0", "TransactionType" : "Payment", "TxnSignature" : "30450221008A722B7F16EDB2348886E88ED4EC682AE9973CC1EE0FF37C93BB2CEC821D3EDF022059E464472031BA5E0D88A93E944B6A8B8DB3E1D5E5D1399A805F615789DB0BED", - "hash" : "988046D484ACE9F5F6A8C792D89C6EA2DB307B5DDA9864AEBA88E6782ABD0865" } -)"); +)", + "2000-01-01T00:00:20Z"); env(offer(A2, XRP(100), A2["USD"](1))); auto offer_tx = @@ -333,9 +360,9 @@ class TransactionEntry_test : public beast::unit_test::suite "TakerPays" : "100000000", "TransactionType" : "OfferCreate", "TxnSignature" : "304502210093FC93ACB77B4E3DE3315441BD010096734859080C1797AB735EB47EBD541BD102205020BB1A7C3B4141279EE4C287C13671E2450EA78914EFD0C6DB2A18344CD4F2", - "hash" : "5FCC1A27A7664F82A0CC4BE5766FBBB7C560D52B93AA7B550CD33B27AEC7EFFB" } -)"); +)", + "2000-01-01T00:00:30Z"); } public: @@ -343,7 +370,8 @@ class TransactionEntry_test : public beast::unit_test::suite run() override { testBadInput(); - testRequest(); + testRequest(1); + testRequest(2); } }; diff --git a/src/test/rpc/Transaction_test.cpp b/src/test/rpc/Transaction_test.cpp index c16d7bbd004..36de520187b 100644 --- a/src/test/rpc/Transaction_test.cpp +++ b/src/test/rpc/Transaction_test.cpp @@ -694,15 +694,13 @@ class Transaction_test : public beast::unit_test::suite } void - testRequest(FeatureBitset features) + testRequest(FeatureBitset features, unsigned apiVersion) { - testcase("Test Request"); + testcase("Test Request API version " + std::to_string(apiVersion)); using namespace test::jtx; using std::to_string; - const char* COMMAND = jss::tx.c_str(); - Env env{*this}; Account const alice{"alice"}; Account const alie{"alie"}; @@ -725,18 +723,47 @@ class Transaction_test : public beast::unit_test::suite Json::Value expected = txn->getJson(JsonOptions::none); expected[jss::DeliverMax] = expected[jss::Amount]; + if (apiVersion > 1) + { + expected.removeMember(jss::hash); + expected.removeMember(jss::Amount); + } + + Json::Value const result = {[&env, txn, apiVersion]() { + Json::Value params{Json::objectValue}; + params[jss::transaction] = to_string(txn->getTransactionID()); + params[jss::binary] = false; + params[jss::api_version] = apiVersion; + return env.client().invoke("tx", params); + }()}; - auto const result = - env.rpc(COMMAND, to_string(txn->getTransactionID())); BEAST_EXPECT(result[jss::result][jss::status] == jss::success); + if (apiVersion > 1) + { + BEAST_EXPECT( + result[jss::result][jss::close_time_iso] == + "2000-01-01T00:00:20Z"); + BEAST_EXPECT( + result[jss::result][jss::hash] == + to_string(txn->getTransactionID())); + BEAST_EXPECT(result[jss::result][jss::validated] == true); + BEAST_EXPECT(result[jss::result][jss::ledger_index] == 4); + BEAST_EXPECT( + result[jss::result][jss::ledger_hash] == + "B41882E20F0EC6228417D28B9AE0F33833645D35F6799DFB782AC97FC4BB51" + "D2"); + } for (auto memberIt = expected.begin(); memberIt != expected.end(); memberIt++) { std::string const name = memberIt.memberName(); - if (BEAST_EXPECT(result[jss::result].isMember(name))) + auto const& result_transaction = + (apiVersion > 1 ? result[jss::result][jss::tx_json] + : result[jss::result]); + if (BEAST_EXPECT(result_transaction.isMember(name))) { - auto const received = result[jss::result][name]; + auto const received = result_transaction[name]; BEAST_EXPECTS( received == *memberIt, "Transaction contains \n\"" + name + "\": " // @@ -763,7 +790,8 @@ class Transaction_test : public beast::unit_test::suite testRangeCTIDRequest(features); testCTIDValidation(features); testCTIDRPC(features); - testRequest(features); + testRequest(features, 1); + testRequest(features, 2); } };