Unify JSON serialization format of transactions (#4775)

* Remove include <ranges>

* Formatting fix

* Output for subscriptions

* Output from sign, submit etc.

* Output from ledger

* Output from account_tx

* Output from transaction_entry

* Output from tx

* Store close_time_iso in API v2 output

* Add small APIv2 unit test for subscribe

* Add unit test for transaction_entry

* Add unit test for tx

* Remove inLedger from API version 2

* Set ledger_hash and ledger_index

* Move isValidated from RPCHelpers to LedgerMaster

* Store closeTime in LedgerFill

* Time formatting fix

* additional tests for Subscribe unit tests

* Improved comments

* Rename mInLedger to mLedgerIndex

* Minor fixes

* Set ledger_hash on closed ledger, even if not validated

* Update API-CHANGELOG.md

* Add ledger_hash, ledger_index to transaction_entry

* Fix validated and close_time_iso in account_tx

* Fix typos

* Improve getJson for Transaction and STTx

* Minor improvements

* Replace class enum JsonOptions with struct

We may consider turning this into a general-purpose template and using it elsewhere

* simplify the extraction of transactionID from Transaction object

* Remove obsolete comments

* Unconditionally set validated in account_tx output

* Minor improvements

* Minor fixes

---------

Co-authored-by: Chenna Keshava <ckeshavabs@gmail.com>
This commit is contained in:
Bronek Kozicki
2023-11-08 18:36:24 +00:00
committed by GitHub
parent 09e0f103f4
commit 32ced493de
33 changed files with 763 additions and 234 deletions

View File

@@ -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)

View File

@@ -215,6 +215,8 @@ public:
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);

View File

@@ -21,8 +21,10 @@
#define RIPPLE_APP_LEDGER_LEDGERTOJSON_H_INCLUDED
#include <ripple/app/ledger/Ledger.h>
#include <ripple/app/ledger/LedgerMaster.h>
#include <ripple/app/misc/TxQ.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/basics/chrono.h>
#include <ripple/json/Object.h>
#include <ripple/protocol/STTx.h>
#include <ripple/protocol/jss.h>
@@ -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<TxQ::TxDetails> txQueue;
LedgerEntryType type;
RPC::Context* context;
std::optional<NetClock::time_point> closeTime;
};
/** Given a Ledger and options, fill a Json::Object or Json::Value with a

View File

@@ -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(

View File

@@ -17,12 +17,14 @@
*/
//==============================================================================
#include <ripple/app/ledger/LedgerMaster.h>
#include <ripple/app/ledger/LedgerToJson.h>
#include <ripple/app/main/Application.h>
#include <ripple/app/misc/DeliverMax.h>
#include <ripple/app/misc/TxQ.h>
#include <ripple/basics/base_uint.h>
#include <ripple/core/Pg.h>
#include <ripple/protocol/jss.h>
#include <ripple/rpc/Context.h>
#include <ripple/rpc/DeliveredAmount.h>
@@ -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);
}
}

View File

@@ -63,6 +63,7 @@
#include <ripple/protocol/BuildInfo.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/STParsedJSON.h>
#include <ripple/protocol/jss.h>
#include <ripple/resource/Fees.h>
#include <ripple/resource/ResourceManager.h>
#include <ripple/rpc/BookChanges.h>
@@ -74,6 +75,7 @@
#include <algorithm>
#include <mutex>
#include <optional>
#include <string>
#include <tuple>
#include <unordered_map>
@@ -3101,7 +3103,11 @@ NetworkOPsImp::transJson(
transResultInfo(result, sToken, sHuman);
jvObj[jss::type] = "transaction";
jvObj[jss::transaction] = transaction->getJson(JsonOptions::none);
// NOTE jvObj 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)
{
@@ -3110,13 +3116,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
}
@@ -3149,27 +3158,38 @@ 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);
// Beta API version must match last index in MultiApiJson
// Last valid (possibly beta) API ver. must match last index in MultiApiJson
static_assert(
apiVersionSelector(RPC::apiBetaVersion)() + 1 //
apiVersionSelector(RPC::apiMaximumValidVersion)() + 1 //
== MultiApiJson::size);
for (unsigned apiVersion = RPC::apiMinimumSupportedVersion,
lastIndex = MultiApiJson::size;
apiVersion <= RPC::apiBetaVersion;
apiVersion <= RPC::apiMaximumValidVersion;
++apiVersion)
{
unsigned const index = apiVersionSelector(apiVersion)();
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;
}
}
}

View File

@@ -24,10 +24,11 @@
#include <ripple/beast/utility/Journal.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/Protocol.h>
#include <ripple/protocol/STBase.h>
#include <ripple/protocol/STTx.h>
#include <ripple/protocol/TER.h>
#include <ripple/protocol/TxMeta.h>
#include <boost/optional.hpp>
#include <optional>
#include <variant>
@@ -99,13 +100,13 @@ public:
LedgerIndex
getLedger() const
{
return mInLedger;
return mLedgerIndex;
}
bool
isValidated() const
{
return mInLedger != 0;
return mLedgerIndex != 0;
}
TransStatus
@@ -138,7 +139,7 @@ public:
void
setLedger(LedgerIndex ledger)
{
mInLedger = ledger;
mLedgerIndex = ledger;
}
/**
@@ -386,7 +387,7 @@ private:
uint256 mTransactionID;
LedgerIndex mInLedger = 0;
LedgerIndex mLedgerIndex = 0;
TransStatus mStatus = INVALID;
TER mResult = temUNCERTAIN;
bool mApplying = false;

View File

@@ -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::include_date)
if (!(options & JsonOptions::disable_API_prior_V2))
{
auto ct = mApp.getLedgerMaster().getCloseTimeBySeq(mInLedger);
// 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)
{
auto ct = mApp.getLedgerMaster().getCloseTimeBySeq(mLedgerIndex);
if (ct)
ret[jss::date] = ct->time_since_epoch().count();
}

View File

@@ -25,9 +25,12 @@
#include <ripple/beast/clock/abstract_clock.h>
#include <ripple/beast/clock/basic_seconds_clock.h>
#include <ripple/beast/clock/manual_clock.h>
#include <chrono>
#include <cstdint>
#include <ratio>
#include <string>
#include <type_traits>
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 <class Duration>
std::string
to_string_iso(date::sys_time<Duration> 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<NetClock::duration::period, std::ratio<1>>);
return to_string_iso(date::sys_time<NetClock::duration>{
tp.time_since_epoch() + epoch_offset});
}
/** A clock for measuring elapsed time.

View File

@@ -22,6 +22,7 @@
#include <ripple/basics/chrono.h>
#include <ripple/beast/clock/abstract_clock.h>
#include <atomic>
namespace ripple {
@@ -37,7 +38,7 @@ private:
adjust(std::chrono::system_clock::time_point when)
{
return time_point(std::chrono::duration_cast<duration>(
when.time_since_epoch() - days(10957)));
when.time_since_epoch() - epoch_offset));
}
public:

View File

@@ -31,7 +31,62 @@
#include <utility>
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<underlying_t>(_all)};
}
};
namespace detail {
class STVar;

View File

@@ -28,6 +28,7 @@
#include <ripple/protocol/HashPrefix.h>
#include <ripple/protocol/SOTemplate.h>
#include <ripple/protocol/STAmount.h>
#include <ripple/protocol/STBase.h>
#include <ripple/protocol/STIssue.h>
#include <ripple/protocol/STPathSet.h>
#include <ripple/protocol/STVector256.h>

View File

@@ -29,6 +29,7 @@
#include <ripple/protocol/SeqProxy.h>
#include <ripple/protocol/TxFormats.h>
#include <boost/container/flat_set.hpp>
#include <functional>
namespace ripple {
@@ -108,6 +109,7 @@ public:
Json::Value
getJson(JsonOptions options) const override;
Json::Value
getJson(JsonOptions options, bool binary) const;

View File

@@ -34,6 +34,7 @@
#include <ripple/protocol/UintTypes.h>
#include <ripple/protocol/jss.h>
#include <boost/format.hpp>
#include <array>
#include <memory>
#include <type_traits>
@@ -226,25 +227,41 @@ STTx::checkSign(
return Unexpected("Internal signature check failure.");
}
Json::Value STTx::getJson(JsonOptions) const
Json::Value
STTx::getJson(JsonOptions options) const
{
Json::Value ret = STObject::getJson(JsonOptions::none);
ret[jss::hash] = to_string(getTransactionID());
if (!(options & JsonOptions::disable_API_prior_V2))
ret[jss::hash] = to_string(getTransactionID());
return ret;
}
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&

View File

@@ -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

View File

@@ -16,6 +16,7 @@
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/app/ledger/LedgerMaster.h>
#include <ripple/app/misc/AMMUtils.h>
#include <ripple/json/json_value.h>
#include <ripple/ledger/ReadView.h>
@@ -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::duration>(
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;
}

View File

@@ -37,7 +37,6 @@
#include <ripple/rpc/Context.h>
#include <ripple/rpc/DeliveredAmount.h>
#include <ripple/rpc/Role.h>
#include <ripple/rpc/impl/RPCHelpers.h>
#include <grpcpp/grpcpp.h>
@@ -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)
@@ -320,25 +319,51 @@ populateJsonResponse(
if (auto txnsData = std::get_if<TxnsData>(&result.transactions))
{
assert(!args.binary);
for (auto const& [txn, txnMeta] : *txnsData)
{
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);
}
else
assert(false && "Missing transaction medatata");
}
}
}
@@ -352,7 +377,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;
}

View File

@@ -27,7 +27,6 @@
#include <ripple/rpc/GRPCHandlers.h>
#include <ripple/rpc/Role.h>
#include <ripple/rpc/handlers/LedgerHandler.h>
#include <ripple/rpc/impl/RPCHelpers.h>
namespace ripple {
namespace RPC {
@@ -301,8 +300,7 @@ doLedgerGrpc(RPC::GRPCContext<org::xrpl::rpc::v1::GetLedgerRequest>& 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 =

View File

@@ -46,6 +46,7 @@ doSignFor(RPC::JsonContext& context)
auto ret = RPC::transactionSignFor(
context.params,
context.apiVersion,
failType,
context.role,
context.ledgerMaster.getValidatedLedgerAge(),

View File

@@ -45,6 +45,7 @@ doSign(RPC::JsonContext& context)
auto ret = RPC::transactionSign(
context.params,
context.apiVersion,
failType,
context.role,
context.ledgerMaster.getValidatedLedgerAge(),

View File

@@ -63,6 +63,7 @@ doSubmit(RPC::JsonContext& context)
auto ret = RPC::transactionSubmit(
context.params,
context.apiVersion,
failType,
context.role,
context.ledgerMaster.getValidatedLedgerAge(),

View File

@@ -45,6 +45,7 @@ doSubmitMultiSigned(RPC::JsonContext& context)
return RPC::transactionSubmitMultiSigned(
context.params,
context.apiVersion,
failType,
context.role,
context.ledgerMaster.getValidatedLedgerAge(),

View File

@@ -17,6 +17,7 @@
*/
//==============================================================================
#include <ripple/app/ledger/LedgerMaster.h>
#include <ripple/app/main/Application.h>
#include <ripple/app/misc/DeliverMax.h>
#include <ripple/ledger/ReadView.h>
@@ -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_...'

View File

@@ -22,6 +22,7 @@
#include <ripple/app/misc/DeliverMax.h>
#include <ripple/app/misc/NetworkOPs.h>
#include <ripple/app/misc/Transaction.h>
#include <ripple/app/rdb/RelationalDatabase.h>
#include <ripple/basics/ToString.h>
#include <ripple/net/RPCErr.h>
#include <ripple/protocol/ErrorCodes.h>
@@ -32,6 +33,7 @@
#include <ripple/rpc/DeliveredAmount.h>
#include <ripple/rpc/GRPCHandlers.h>
#include <ripple/rpc/impl/RPCHelpers.h>
#include <charconv>
#include <regex>
@@ -55,6 +57,8 @@ struct TxResult
std::variant<std::shared_ptr<TxMeta>, Blob> meta;
bool validated = false;
std::optional<std::string> ctid;
std::optional<NetClock::time_point> closeTime;
std::optional<uint256> 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 const> 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<Blob>(&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<std::shared_ptr<TxMeta>>(&result.meta))

View File

@@ -600,59 +600,6 @@ getLedger<>(
template Status
getLedger<>(std::shared_ptr<ReadView const>&, 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;
}

View File

@@ -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<AccountID>
parseAccountIds(Json::Value const& jvArray);

View File

@@ -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

View File

@@ -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,

View File

@@ -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) {

View File

@@ -2536,17 +2536,19 @@ public:
// 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 @@ public:
assert(get<1>(testFunc) == nullptr);
result = signFn(
req,
1,
NetworkOPs::FailHard::yes,
testRole,
1s,
@@ -2597,6 +2600,7 @@ public:
assert(submitFn != nullptr);
result = submitFn(
req,
1,
NetworkOPs::FailHard::yes,
testRole,
1s,

View File

@@ -20,6 +20,7 @@
#include <ripple/app/misc/NetworkOPs.h>
#include <ripple/beast/unit_test.h>
#include <ripple/core/ConfigSections.h>
#include <ripple/json/json_value.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
@@ -163,7 +164,7 @@ public:
}
void
testTransactions()
testTransactions_APIv1()
{
using namespace std::chrono_literals;
using namespace jtx;
@@ -307,6 +308,85 @@ public:
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()
{
@@ -1222,7 +1302,8 @@ public:
testServer();
testLedger();
testTransactions();
testTransactions_APIv1();
testTransactions_APIv2();
testManifests();
testValidations(all - xrpFees);
testValidations(all);

View File

@@ -20,9 +20,12 @@
#include <ripple/json/json_reader.h>
#include <ripple/json/json_value.h>
#include <ripple/protocol/jss.h>
#include <ripple/rpc/impl/RPCHelpers.h>
#include <test/jtx.h>
#include <test/jtx/Env.h>
#include <functional>
namespace ripple {
class TransactionEntry_test : public beast::unit_test::suite
@@ -143,29 +146,51 @@ 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 expected_ledger_hash = "",
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];
}()};
if (!BEAST_EXPECTS(resIndex.isMember(jss::tx_json), txhash))
if (!BEAST_EXPECT(resIndex.isMember(jss::tx_json)))
return;
BEAST_EXPECT(resIndex[jss::tx_json][jss::hash] == txhash);
BEAST_EXPECT(resIndex[jss::validated] == true);
BEAST_EXPECT(resIndex[jss::ledger_index] == index);
BEAST_EXPECT(resIndex[jss::ledger_hash] == expected_ledger_hash);
if (apiVersion > 1)
{
BEAST_EXPECT(resIndex[jss::hash] == txhash);
BEAST_EXPECT(!resIndex[jss::tx_json].isMember(jss::hash));
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);
BEAST_EXPECT(!resIndex.isMember(jss::hash));
BEAST_EXPECT(!resIndex.isMember(jss::close_time_iso));
}
if (!expected_json.empty())
{
Json::Value expected;
@@ -198,12 +223,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 +238,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",
@@ -226,39 +254,49 @@ class TransactionEntry_test : public beast::unit_test::suite
env.fund(XRP(10000), A1);
auto fund_1_tx =
boost::lexical_cast<std::string>(env.tx()->getTransactionID());
BEAST_EXPECT(
fund_1_tx ==
"F4E9DF90D829A9E8B423FF68C34413E240D8D8BB0EFD080DF08114ED398E2506");
env.fund(XRP(10000), A2);
auto fund_2_tx =
boost::lexical_cast<std::string>(env.tx()->getTransactionID());
BEAST_EXPECT(
fund_2_tx ==
"6853CD8226A05068C951CB1F54889FF4E40C5B440DC1C5BA38F114C4E0B1E705");
env.close();
// 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"(
{
"Account" : "r4nmQNH4Fhjfh6cHDbvVSsBv7KySbj4cBf",
"Fee" : "10",
"Sequence" : 3,
"SetFlag" : 8,
"SigningPubKey" : "0324CAAFA2212D2AEAB9D42D481535614AED486293E1FB1380FF070C3DD7FB4264",
"TransactionType" : "AccountSet",
"TxnSignature" : "3044022007B35E3B99460534FF6BC3A66FBBA03591C355CC38E38588968E87CCD01BE229022071A443026DE45041B55ABB1CC76812A87EA701E475BBB7E165513B4B242D3474",
"hash" : "F4E9DF90D829A9E8B423FF68C34413E240D8D8BB0EFD080DF08114ED398E2506"
}
)");
check_tx(env.closed()->seq(), fund_2_tx, R"(
{
"Account" : "rGpeQzUWFu4fMhJHZ1Via5aqFC3A5twZUD",
"Fee" : "10",
"Sequence" : 3,
"SetFlag" : 8,
"SigningPubKey" : "03CFF28E067A2CCE6CC5A598C0B845CBD3F30A7863BE9C0DD55F4960EFABCCF4D0",
"TransactionType" : "AccountSet",
"TxnSignature" : "3045022100C8857FC0759A2AC0D2F320684691A66EAD252EAED9EF88C79791BC58BFCC9D860220421722286487DD0ED6BBA626CE6FCBDD14289F7F4726870C3465A4054C2702D7",
"hash" : "6853CD8226A05068C951CB1F54889FF4E40C5B440DC1C5BA38F114C4E0B1E705"
}
)");
check_tx(
env.closed()->seq(),
fund_1_tx,
R"({
"Account" : "r4nmQNH4Fhjfh6cHDbvVSsBv7KySbj4cBf",
"Fee" : "10",
"Sequence" : 3,
"SetFlag" : 8,
"SigningPubKey" : "0324CAAFA2212D2AEAB9D42D481535614AED486293E1FB1380FF070C3DD7FB4264",
"TransactionType" : "AccountSet",
"TxnSignature" : "3044022007B35E3B99460534FF6BC3A66FBBA03591C355CC38E38588968E87CCD01BE229022071A443026DE45041B55ABB1CC76812A87EA701E475BBB7E165513B4B242D3474",
})",
"ADB727BCC74B29421BB01B847740B179B8A0ED3248D76A89ED2E39B02C427784",
"2000-01-01T00:00:10Z");
check_tx(
env.closed()->seq(),
fund_2_tx,
R"({
"Account" : "rGpeQzUWFu4fMhJHZ1Via5aqFC3A5twZUD",
"Fee" : "10",
"Sequence" : 3,
"SetFlag" : 8,
"SigningPubKey" : "03CFF28E067A2CCE6CC5A598C0B845CBD3F30A7863BE9C0DD55F4960EFABCCF4D0",
"TransactionType" : "AccountSet",
"TxnSignature" : "3045022100C8857FC0759A2AC0D2F320684691A66EAD252EAED9EF88C79791BC58BFCC9D860220421722286487DD0ED6BBA626CE6FCBDD14289F7F4726870C3465A4054C2702D7",
})",
"ADB727BCC74B29421BB01B847740B179B8A0ED3248D76A89ED2E39B02C427784",
"2000-01-01T00:00:10Z");
env.trust(A2["USD"](1000), A1);
// the trust tx is actually a payment since the trust method
@@ -266,76 +304,85 @@ class TransactionEntry_test : public beast::unit_test::suite
// in the check below
auto trust_tx =
boost::lexical_cast<std::string>(env.tx()->getTransactionID());
BEAST_EXPECT(
trust_tx ==
"C992D97D88FF444A1AB0C06B27557EC54B7F7DA28254778E60238BEA88E0C101");
env(pay(A2, A1, A2["USD"](5)));
auto pay_tx =
boost::lexical_cast<std::string>(env.tx()->getTransactionID());
env.close();
BEAST_EXPECT(
pay_tx ==
"988046D484ACE9F5F6A8C792D89C6EA2DB307B5DDA9864AEBA88E6782ABD0865");
check_tx(env.closed()->seq(), trust_tx, R"(
{
"Account" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"DeliverMax" : "10",
"Destination" : "r4nmQNH4Fhjfh6cHDbvVSsBv7KySbj4cBf",
"Fee" : "10",
"Flags" : 2147483648,
"Sequence" : 3,
"SigningPubKey" : "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020",
"TransactionType" : "Payment",
"TxnSignature" : "3044022033D9EBF7F02950AF2F6B13C07AEE641C8FEBDD540A338FCB9027A965A4AED35B02206E4E227DCC226A3456C0FEF953449D21645A24EB63CA0BB7C5B62470147FD1D1",
"hash" : "C992D97D88FF444A1AB0C06B27557EC54B7F7DA28254778E60238BEA88E0C101"
}
)");
check_tx(
env.closed()->seq(),
trust_tx,
R"({
"Account" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"DeliverMax" : "10",
"Destination" : "r4nmQNH4Fhjfh6cHDbvVSsBv7KySbj4cBf",
"Fee" : "10",
"Flags" : 2147483648,
"Sequence" : 3,
"SigningPubKey" : "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020",
"TransactionType" : "Payment",
"TxnSignature" : "3044022033D9EBF7F02950AF2F6B13C07AEE641C8FEBDD540A338FCB9027A965A4AED35B02206E4E227DCC226A3456C0FEF953449D21645A24EB63CA0BB7C5B62470147FD1D1",
})",
"39AA166131D56622EFD96CB4B2BD58003ACD37091C90977FF6B81419DB451775",
"2000-01-01T00:00:20Z");
check_tx(
env.closed()->seq(),
pay_tx,
R"(
{
"Account" : "rGpeQzUWFu4fMhJHZ1Via5aqFC3A5twZUD",
"DeliverMax" :
{
"currency" : "USD",
"issuer" : "rGpeQzUWFu4fMhJHZ1Via5aqFC3A5twZUD",
"value" : "5"
},
"Destination" : "r4nmQNH4Fhjfh6cHDbvVSsBv7KySbj4cBf",
"Fee" : "10",
"Flags" : 2147483648,
"Sequence" : 4,
"SigningPubKey" : "03CFF28E067A2CCE6CC5A598C0B845CBD3F30A7863BE9C0DD55F4960EFABCCF4D0",
"TransactionType" : "Payment",
"TxnSignature" : "30450221008A722B7F16EDB2348886E88ED4EC682AE9973CC1EE0FF37C93BB2CEC821D3EDF022059E464472031BA5E0D88A93E944B6A8B8DB3E1D5E5D1399A805F615789DB0BED",
"hash" : "988046D484ACE9F5F6A8C792D89C6EA2DB307B5DDA9864AEBA88E6782ABD0865"
}
)");
R"({
"Account" : "rGpeQzUWFu4fMhJHZ1Via5aqFC3A5twZUD",
"DeliverMax" :
{
"currency" : "USD",
"issuer" : "rGpeQzUWFu4fMhJHZ1Via5aqFC3A5twZUD",
"value" : "5"
},
"Destination" : "r4nmQNH4Fhjfh6cHDbvVSsBv7KySbj4cBf",
"Fee" : "10",
"Flags" : 2147483648,
"Sequence" : 4,
"SigningPubKey" : "03CFF28E067A2CCE6CC5A598C0B845CBD3F30A7863BE9C0DD55F4960EFABCCF4D0",
"TransactionType" : "Payment",
"TxnSignature" : "30450221008A722B7F16EDB2348886E88ED4EC682AE9973CC1EE0FF37C93BB2CEC821D3EDF022059E464472031BA5E0D88A93E944B6A8B8DB3E1D5E5D1399A805F615789DB0BED",
})",
"39AA166131D56622EFD96CB4B2BD58003ACD37091C90977FF6B81419DB451775",
"2000-01-01T00:00:20Z");
env(offer(A2, XRP(100), A2["USD"](1)));
auto offer_tx =
boost::lexical_cast<std::string>(env.tx()->getTransactionID());
BEAST_EXPECT(
offer_tx ==
"5FCC1A27A7664F82A0CC4BE5766FBBB7C560D52B93AA7B550CD33B27AEC7EFFB");
env.close();
check_tx(
env.closed()->seq(),
offer_tx,
R"(
{
"Account" : "rGpeQzUWFu4fMhJHZ1Via5aqFC3A5twZUD",
"Fee" : "10",
"Sequence" : 5,
"SigningPubKey" : "03CFF28E067A2CCE6CC5A598C0B845CBD3F30A7863BE9C0DD55F4960EFABCCF4D0",
"TakerGets" :
{
"currency" : "USD",
"issuer" : "rGpeQzUWFu4fMhJHZ1Via5aqFC3A5twZUD",
"value" : "1"
},
"TakerPays" : "100000000",
"TransactionType" : "OfferCreate",
"TxnSignature" : "304502210093FC93ACB77B4E3DE3315441BD010096734859080C1797AB735EB47EBD541BD102205020BB1A7C3B4141279EE4C287C13671E2450EA78914EFD0C6DB2A18344CD4F2",
"hash" : "5FCC1A27A7664F82A0CC4BE5766FBBB7C560D52B93AA7B550CD33B27AEC7EFFB"
}
)");
R"({
"Account" : "rGpeQzUWFu4fMhJHZ1Via5aqFC3A5twZUD",
"Fee" : "10",
"Sequence" : 5,
"SigningPubKey" : "03CFF28E067A2CCE6CC5A598C0B845CBD3F30A7863BE9C0DD55F4960EFABCCF4D0",
"TakerGets" :
{
"currency" : "USD",
"issuer" : "rGpeQzUWFu4fMhJHZ1Via5aqFC3A5twZUD",
"value" : "1"
},
"TakerPays" : "100000000",
"TransactionType" : "OfferCreate",
"TxnSignature" : "304502210093FC93ACB77B4E3DE3315441BD010096734859080C1797AB735EB47EBD541BD102205020BB1A7C3B4141279EE4C287C13671E2450EA78914EFD0C6DB2A18344CD4F2",
})",
"0589B876DF5AFE335781E8FC12C2EC62A80151DF13BBAFE9EB2DA62E798ED434",
"2000-01-01T00:00:30Z");
}
public:
@@ -343,7 +390,8 @@ public:
run() override
{
testBadInput();
testRequest();
test::jtx::forAllApiVersions(
std::bind_front(&TransactionEntry_test::testRequest, this));
}
};

View File

@@ -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 @@ public:
testRangeCTIDRequest(features);
testCTIDValidation(features);
testCTIDRPC(features);
testRequest(features);
test::jtx::forAllApiVersions(
std::bind_front(&Transaction_test::testRequest, this, features));
}
};