mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-23 07:10:53 +00:00
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1097 lines
30 KiB
C++
1097 lines
30 KiB
C++
#pragma once
|
|
|
|
#include <test/jtx/AbstractClient.h>
|
|
#include <test/jtx/Account.h>
|
|
#include <test/jtx/JTx.h>
|
|
#include <test/jtx/ManualTimeKeeper.h>
|
|
#include <test/jtx/amount.h>
|
|
#include <test/jtx/envconfig.h>
|
|
#include <test/jtx/require.h>
|
|
#include <test/jtx/vault.h>
|
|
#include <test/unit_test/SuiteJournal.h>
|
|
|
|
#include <xrpld/app/ledger/OpenLedger.h>
|
|
#include <xrpld/app/main/Application.h>
|
|
#include <xrpld/core/Config.h>
|
|
#include <xrpld/rpc/detail/Pathfinder.h>
|
|
|
|
#include <xrpl/basics/Log.h>
|
|
#include <xrpl/basics/base_uint.h>
|
|
#include <xrpl/basics/chrono.h>
|
|
#include <xrpl/basics/contract.h>
|
|
#include <xrpl/beast/unit_test/suite.h>
|
|
#include <xrpl/beast/utility/Journal.h>
|
|
#include <xrpl/beast/utility/instrumentation.h>
|
|
#include <xrpl/json/json_value.h>
|
|
#include <xrpl/ledger/OpenView.h>
|
|
#include <xrpl/ledger/ReadView.h>
|
|
#include <xrpl/protocol/AccountID.h>
|
|
#include <xrpl/protocol/ApiVersion.h>
|
|
#include <xrpl/protocol/Asset.h>
|
|
#include <xrpl/protocol/ErrorCodes.h>
|
|
#include <xrpl/protocol/Feature.h>
|
|
#include <xrpl/protocol/Issue.h>
|
|
#include <xrpl/protocol/Keylet.h>
|
|
#include <xrpl/protocol/STAmount.h>
|
|
#include <xrpl/protocol/STObject.h>
|
|
#include <xrpl/protocol/STTx.h>
|
|
#include <xrpl/protocol/TER.h>
|
|
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <future>
|
|
#include <initializer_list>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <source_location>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace xrpl::test::jtx {
|
|
|
|
/**
|
|
* Wrapper that captures std::source_location when implicitly constructed.
|
|
* This solves the problem of combining std::source_location with variadic
|
|
* templates. The std::source_location default argument is evaluated at the
|
|
* call site when the wrapper is constructed via implicit conversion.
|
|
*
|
|
* This is a template struct that holds the value directly, allowing implicit
|
|
* conversion without template argument deduction issues via CTAD.
|
|
*/
|
|
template <class T>
|
|
struct WithSourceLocation
|
|
{
|
|
T value;
|
|
std::source_location loc;
|
|
|
|
// Non-explicit constructor allows implicit conversion.
|
|
// The default argument for loc is evaluated at the call site.
|
|
WithSourceLocation(T v, std::source_location l = std::source_location::current())
|
|
: value(std::move(v)), loc(l)
|
|
{
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Designate accounts as no-ripple in Env::fund
|
|
*/
|
|
template <class... Args>
|
|
std::array<Account, 1 + sizeof...(Args)>
|
|
noripple(Account const& account, Args const&... args)
|
|
{
|
|
return {{account, args...}};
|
|
}
|
|
|
|
inline FeatureBitset
|
|
testableAmendments()
|
|
{
|
|
static FeatureBitset const kIds = [] {
|
|
auto const& sa = allAmendments();
|
|
std::vector<uint256> feats;
|
|
feats.reserve(sa.size());
|
|
for (auto const& [s, vote] : sa)
|
|
{
|
|
(void)vote;
|
|
if (auto const f = getRegisteredFeature(s))
|
|
{
|
|
feats.push_back(*f);
|
|
}
|
|
else
|
|
{
|
|
Throw<std::runtime_error>("Unknown feature: " + s + " in allAmendments.");
|
|
}
|
|
}
|
|
return FeatureBitset(feats);
|
|
}();
|
|
return kIds;
|
|
}
|
|
|
|
/**
|
|
* Returns all 2^N permutations of a seed FeatureBitset with each subset of
|
|
* the given features excluded. The seed is included as the first element.
|
|
*
|
|
* Useful for running a test over every combination of optional amendments
|
|
* so that each case is exercised both with and without each feature.
|
|
*/
|
|
inline std::vector<FeatureBitset>
|
|
amendmentCombinations(std::initializer_list<uint256> features, FeatureBitset seed)
|
|
{
|
|
std::vector<FeatureBitset> result{seed};
|
|
for (auto const& f : features)
|
|
{
|
|
auto const n = result.size();
|
|
for (std::size_t i = 0; i < n; ++i)
|
|
result.push_back(result[i] - f);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
class SuiteLogs : public Logs
|
|
{
|
|
beast::unit_test::Suite& suite_;
|
|
|
|
public:
|
|
explicit SuiteLogs(beast::unit_test::Suite& suite) : Logs(beast::Severity::Error), suite_(suite)
|
|
{
|
|
}
|
|
|
|
~SuiteLogs() override = default;
|
|
|
|
std::unique_ptr<beast::Journal::Sink>
|
|
makeSink(std::string const& partition, beast::Severity threshold) override
|
|
{
|
|
return std::make_unique<SuiteJournalSink>(partition, threshold, suite_);
|
|
}
|
|
};
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* A transaction testing environment.
|
|
*/
|
|
class Env
|
|
{
|
|
public:
|
|
beast::unit_test::Suite& test;
|
|
|
|
Account const& master = Account::kMaster;
|
|
|
|
/**
|
|
* Used by parseResult() and postConditions()
|
|
*/
|
|
struct ParsedResult
|
|
{
|
|
std::optional<TER> ter;
|
|
// RPC errors tend to return either a "code" and a "message" (sometimes
|
|
// with an "error" that corresponds to the "code"), or with an "error"
|
|
// and an "exception". However, this structure allows all possible
|
|
// combinations.
|
|
std::optional<ErrorCodeI> rpcCode;
|
|
std::string rpcMessage;
|
|
std::string rpcError;
|
|
std::string rpcException;
|
|
};
|
|
|
|
private:
|
|
struct AppBundle
|
|
{
|
|
Application* app = nullptr;
|
|
std::unique_ptr<Application> owned;
|
|
ManualTimeKeeper* timeKeeper = nullptr;
|
|
std::thread thread;
|
|
std::unique_ptr<AbstractClient> client;
|
|
|
|
AppBundle() = default;
|
|
AppBundle(
|
|
beast::unit_test::Suite& suite,
|
|
std::unique_ptr<Config> config,
|
|
std::unique_ptr<Logs> logs,
|
|
beast::Severity thresh);
|
|
~AppBundle();
|
|
};
|
|
|
|
AppBundle bundle_;
|
|
|
|
public:
|
|
beast::Journal const journal;
|
|
|
|
Env() = delete;
|
|
Env&
|
|
operator=(Env const&) = delete;
|
|
Env(Env const&) = delete;
|
|
|
|
/**
|
|
* @brief Create Env using suite, Config pointer, and explicit features.
|
|
*
|
|
* This constructor will create an Env with the specified configuration
|
|
* and takes ownership the passed Config pointer. Features will be enabled
|
|
* according to rules described below (see next constructor).
|
|
*
|
|
* @param suite the current unit_test::suite
|
|
* @param config The desired Config - ownership will be taken by moving
|
|
* the pointer. See envconfig and related functions for common config
|
|
* tweaks.
|
|
* @param args with_only_features() to explicitly enable or
|
|
* supported_features_except() to enable all and disable specific features.
|
|
*/
|
|
// VFALCO Could wrap the suite::log in a Journal here
|
|
Env(beast::unit_test::Suite& suite,
|
|
std::unique_ptr<Config> config,
|
|
FeatureBitset features,
|
|
std::unique_ptr<Logs> logs = nullptr,
|
|
beast::Severity thresh = beast::Severity::Error)
|
|
: test(suite)
|
|
, bundle_(suite, std::move(config), std::move(logs), thresh)
|
|
, journal{bundle_.app->getJournal("Env")}
|
|
{
|
|
memoize(Account::kMaster);
|
|
Pathfinder::initPathTable();
|
|
foreachFeature(features, [&appFeats = app().config().features](uint256 const& f) {
|
|
appFeats.insert(f);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @brief Create Env with default config and specified
|
|
* features.
|
|
*
|
|
* This constructor will create an Env with the standard Env configuration
|
|
* (from envconfig()) and features explicitly specified. Use
|
|
* with_only_features(...) or supported_features_except(...) to create a
|
|
* collection of features appropriate for passing here.
|
|
*
|
|
* @param suite the current unit_test::suite
|
|
* @param args collection of features
|
|
*
|
|
*/
|
|
Env(beast::unit_test::Suite& suite,
|
|
FeatureBitset features,
|
|
std::unique_ptr<Logs> logs = nullptr)
|
|
: Env(suite, envconfig(), features, std::move(logs))
|
|
{
|
|
}
|
|
|
|
/**
|
|
* @brief Create Env using suite and Config pointer.
|
|
*
|
|
* This constructor will create an Env with the specified configuration
|
|
* and takes ownership the passed Config pointer. All supported amendments
|
|
* are enabled by this version of the constructor.
|
|
*
|
|
* @param suite the current unit_test::suite
|
|
* @param config The desired Config - ownership will be taken by moving
|
|
* the pointer. See envconfig and related functions for common config
|
|
* tweaks.
|
|
*/
|
|
Env(beast::unit_test::Suite& suite,
|
|
std::unique_ptr<Config> config,
|
|
std::unique_ptr<Logs> logs = nullptr,
|
|
beast::Severity thresh = beast::Severity::Error)
|
|
: Env(suite, std::move(config), testableAmendments(), std::move(logs), thresh)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* @brief Create Env with only the current test suite
|
|
*
|
|
* This constructor will create an Env with the standard
|
|
* test Env configuration (from envconfig()) and all supported
|
|
* amendments enabled.
|
|
*
|
|
* @param suite the current unit_test::suite
|
|
*/
|
|
Env(beast::unit_test::Suite& suite, beast::Severity thresh = beast::Severity::Error)
|
|
: Env(suite, envconfig(), nullptr, thresh)
|
|
{
|
|
}
|
|
|
|
virtual ~Env() = default;
|
|
|
|
Application&
|
|
// NOLINTNEXTLINE(readability-make-member-function-const)
|
|
app()
|
|
{
|
|
return *bundle_.app;
|
|
}
|
|
|
|
[[nodiscard]] Application const&
|
|
app() const
|
|
{
|
|
return *bundle_.app;
|
|
}
|
|
|
|
ManualTimeKeeper&
|
|
// NOLINTNEXTLINE(readability-make-member-function-const)
|
|
timeKeeper()
|
|
{
|
|
return *bundle_.timeKeeper;
|
|
}
|
|
|
|
/**
|
|
* Returns the current network time
|
|
*
|
|
* @note This is manually advanced when ledgers
|
|
* close or by callers.
|
|
*/
|
|
NetClock::time_point
|
|
// NOLINTNEXTLINE(readability-make-member-function-const)
|
|
now()
|
|
{
|
|
return timeKeeper().now();
|
|
}
|
|
|
|
/**
|
|
* Returns the connected client.
|
|
*/
|
|
AbstractClient&
|
|
// NOLINTNEXTLINE(readability-make-member-function-const)
|
|
client()
|
|
{
|
|
return *bundle_.client;
|
|
}
|
|
|
|
/**
|
|
* Execute an RPC command.
|
|
*
|
|
* The command is examined and used to build
|
|
* the correct JSON as per the arguments.
|
|
*/
|
|
template <class... Args>
|
|
json::Value
|
|
rpc(unsigned apiVersion,
|
|
std::unordered_map<std::string, std::string> const& headers,
|
|
std::string const& cmd,
|
|
Args&&... args);
|
|
|
|
template <class... Args>
|
|
json::Value
|
|
rpc(unsigned apiVersion, std::string const& cmd, Args&&... args);
|
|
|
|
template <class... Args>
|
|
json::Value
|
|
rpc(std::unordered_map<std::string, std::string> const& headers,
|
|
std::string const& cmd,
|
|
Args&&... args);
|
|
|
|
template <class... Args>
|
|
json::Value
|
|
rpc(std::string const& cmd, Args&&... args);
|
|
|
|
/**
|
|
* Returns the current ledger.
|
|
*
|
|
* This is a non-modifiable snapshot of the
|
|
* open ledger at the moment of the call.
|
|
* Transactions applied after the call to open()
|
|
* will not be visible.
|
|
*/
|
|
[[nodiscard]] std::shared_ptr<OpenView const>
|
|
current() const
|
|
{
|
|
return app().getOpenLedger().current();
|
|
}
|
|
|
|
/**
|
|
* Returns the last closed ledger.
|
|
*
|
|
* The open ledger is built on top of the
|
|
* last closed ledger. When the open ledger
|
|
* is closed, it becomes the new closed ledger
|
|
* and a new open ledger takes its place.
|
|
*/
|
|
std::shared_ptr<ReadView const>
|
|
closed();
|
|
|
|
/**
|
|
* Close and advance the ledger.
|
|
*
|
|
* The resulting close time will be different and
|
|
* greater than the previous close time, and at or
|
|
* after the passed-in close time.
|
|
*
|
|
* Effects:
|
|
*
|
|
* Creates a new closed ledger from the last
|
|
* closed ledger.
|
|
*
|
|
* All transactions that made it into the open
|
|
* ledger are applied to the closed ledger.
|
|
*
|
|
* The Application network time is set to
|
|
* the close time of the resulting ledger.
|
|
*
|
|
* @return true if no error, false if error
|
|
*/
|
|
bool
|
|
close(
|
|
NetClock::time_point closeTime,
|
|
std::optional<std::chrono::milliseconds> consensusDelay = std::nullopt);
|
|
|
|
/**
|
|
* Close and advance the ledger.
|
|
*
|
|
* The time is calculated as the duration from
|
|
* the previous ledger closing time.
|
|
*
|
|
* @return true if no error, false if error
|
|
*/
|
|
template <class Rep, class Period>
|
|
bool
|
|
close(std::chrono::duration<Rep, Period> const& elapsed)
|
|
{
|
|
// VFALCO Is this the correct time?
|
|
return close(now() + elapsed);
|
|
}
|
|
|
|
/**
|
|
* Close and advance the ledger.
|
|
*
|
|
* The time is calculated as five seconds from
|
|
* the previous ledger closing time.
|
|
*
|
|
* @return true if no error, false if error
|
|
*/
|
|
bool
|
|
close()
|
|
{
|
|
// VFALCO Is this the correct time?
|
|
return close(std::chrono::seconds(5));
|
|
}
|
|
|
|
/**
|
|
* Close and advance the ledger, then synchronize with the server's
|
|
* io_context to ensure all async operations initiated by the close have
|
|
* been started.
|
|
*
|
|
* This function performs the same ledger close as close(), but additionally
|
|
* ensures that all tasks posted to the server's io_context (such as
|
|
* WebSocket subscription message sends) have been initiated before returning.
|
|
*
|
|
* What it guarantees:
|
|
* - All async operations posted before syncClose() have been STARTED
|
|
* - For WebSocket sends: async_write_some() has been called
|
|
* - The actual I/O completion may still be pending (async)
|
|
*
|
|
* What it does NOT guarantee:
|
|
* - Async operations have COMPLETED
|
|
* - WebSocket messages have been received by clients
|
|
* - However, for localhost connections, the remaining latency is typically
|
|
* microseconds, making tests reliable
|
|
*
|
|
* Use this instead of close() when:
|
|
* - Test code immediately checks for subscription messages
|
|
* - Race conditions between test and worker threads must be avoided
|
|
* - Deterministic test behavior is required
|
|
*
|
|
* @param timeout Maximum time to wait for the barrier task to execute
|
|
* @return true if close succeeded and barrier executed within timeout,
|
|
* false otherwise
|
|
*/
|
|
[[nodiscard]] bool
|
|
syncClose(std::chrono::steady_clock::duration timeout = std::chrono::seconds{1})
|
|
{
|
|
XRPL_ASSERT(
|
|
app().getNumberOfThreads() == 1,
|
|
"syncClose() is only useful on an application with a single thread");
|
|
auto const result = close();
|
|
return result && drainServerIo(timeout);
|
|
}
|
|
|
|
/**
|
|
* Disconnect the Env's built-in client and wait for the server to
|
|
* register the dropped connection.
|
|
*
|
|
* Env holds one persistent client connection to the server's RPC port for
|
|
* its whole lifetime (see client()), and that connection counts against
|
|
* the port's connection limit. Tests that need a known starting occupancy
|
|
* can call this to deterministically release that slot instead of waiting
|
|
* out the server's localhost idle timeout.
|
|
*
|
|
* The server decrements its per-port connection count in the peer's
|
|
* destructor, which runs when the io_context processes the end-of-stream
|
|
* on the closed socket. After closing the client this drains the server's
|
|
* io_context twice: the first barrier guarantees the reactor has reaped
|
|
* the closed socket and queued the peer's teardown, and the second
|
|
* guarantees that teardown (and therefore the count decrement) has run.
|
|
*
|
|
* This is only sound when the server uses a single io_context thread, so
|
|
* that draining establishes ordering against the teardown - configure the
|
|
* Env with singleThreadIo() (as syncClose() also requires). Like
|
|
* syncClose(), it relies on loopback teardown latency being negligible.
|
|
*
|
|
* @param timeout Maximum time to wait for each barrier task to execute
|
|
* @return true if both barriers executed within timeout, false otherwise
|
|
*/
|
|
[[nodiscard]] bool
|
|
disconnectClient(std::chrono::steady_clock::duration timeout = std::chrono::seconds{1})
|
|
{
|
|
XRPL_ASSERT(
|
|
app().getNumberOfThreads() == 1,
|
|
"disconnectClient() is only useful on an application with a single "
|
|
"thread");
|
|
|
|
bundle_.client->disconnect();
|
|
|
|
// Drain the server's single io thread twice: the first barrier flushes
|
|
// the reactor's reap of the closed socket (queuing the peer teardown),
|
|
// the second flushes that teardown - and therefore the connection-count
|
|
// decrement. Both run unconditionally so a timed-out first drain does
|
|
// not short-circuit the second.
|
|
bool const reaped = drainServerIo(timeout);
|
|
bool const toreDown = drainServerIo(timeout);
|
|
return reaped && toreDown;
|
|
}
|
|
|
|
/**
|
|
* Turn on JSON tracing.
|
|
* With no arguments, trace all
|
|
*/
|
|
void
|
|
trace(int howMany = -1)
|
|
{
|
|
trace_ = howMany;
|
|
}
|
|
|
|
/**
|
|
* Turn off JSON tracing.
|
|
*/
|
|
void
|
|
notrace()
|
|
{
|
|
trace_ = 0;
|
|
}
|
|
|
|
void
|
|
setParseFailureExpected(bool b)
|
|
{
|
|
parseFailureExpected_ = b;
|
|
}
|
|
|
|
/**
|
|
* RAII class to set and restore the parse failure flag (setParseFailureExpected).
|
|
*
|
|
* Can be created directly, or through the `getParseFailureGuard(bool)` function.
|
|
*/
|
|
class ParseFailureGuard final
|
|
{
|
|
Env& self_;
|
|
bool const oldExpected_;
|
|
|
|
public:
|
|
ParseFailureGuard(Env& self, bool b)
|
|
: self_(self), oldExpected_(self_.parseFailureExpected_)
|
|
{
|
|
self_.setParseFailureExpected(b);
|
|
}
|
|
|
|
~ParseFailureGuard()
|
|
{
|
|
self_.setParseFailureExpected(oldExpected_);
|
|
}
|
|
|
|
// No copy, no move
|
|
ParseFailureGuard(ParseFailureGuard const&) = delete;
|
|
ParseFailureGuard&
|
|
operator=(ParseFailureGuard const&) = delete;
|
|
ParseFailureGuard(ParseFailureGuard&& other) = delete;
|
|
ParseFailureGuard&
|
|
operator=(ParseFailureGuard&&) = delete;
|
|
};
|
|
|
|
/**
|
|
* Gets an RAII guard to set and restore the parse failure flag
|
|
*
|
|
* Usage:
|
|
* auto const guard = env.getParseFailureGuard(true/false);
|
|
*/
|
|
[[nodiscard]] ParseFailureGuard
|
|
getParseFailureGuard(bool b)
|
|
{
|
|
return ParseFailureGuard{*this, b};
|
|
}
|
|
|
|
/**
|
|
* Turn off signature checks.
|
|
*/
|
|
void
|
|
disableSigs()
|
|
{
|
|
app().checkSigs(false);
|
|
}
|
|
|
|
// set rpc retries
|
|
void
|
|
setRetries(unsigned r = 5)
|
|
{
|
|
retries_ = r;
|
|
}
|
|
|
|
// get rpc retries
|
|
[[nodiscard]] unsigned
|
|
retries() const
|
|
{
|
|
return retries_;
|
|
}
|
|
|
|
/**
|
|
* Associate AccountID with account.
|
|
*/
|
|
void
|
|
memoize(Account const& account);
|
|
|
|
/**
|
|
* Returns the Account given the AccountID.
|
|
*/
|
|
/** @{ */
|
|
[[nodiscard]] Account const&
|
|
lookup(AccountID const& id) const;
|
|
|
|
[[nodiscard]] Account const&
|
|
lookup(std::string const& base58ID) const;
|
|
/** @} */
|
|
|
|
/**
|
|
* Returns the XRP balance on an account.
|
|
* Returns 0 if the account does not exist.
|
|
*/
|
|
[[nodiscard]] PrettyAmount
|
|
balance(Account const& account) const;
|
|
|
|
/**
|
|
* Returns the next sequence number on account.
|
|
*
|
|
* @throws if the account does not exist
|
|
*/
|
|
[[nodiscard]] std::uint32_t
|
|
seq(Account const& account) const;
|
|
|
|
/**
|
|
* Return the balance on an account.
|
|
* Returns 0 if the trust line does not exist.
|
|
*/
|
|
// VFALCO NOTE This should return a unit-less amount
|
|
[[nodiscard]] PrettyAmount
|
|
balance(Account const& account, Asset const& asset) const;
|
|
|
|
/**
|
|
* Returns the IOU limit on an account.
|
|
* Returns 0 if the trust line does not exist.
|
|
*/
|
|
[[nodiscard]] PrettyAmount
|
|
limit(Account const& account, Issue const& issue) const;
|
|
|
|
/**
|
|
* Return the number of objects owned by an account.
|
|
* Returns 0 if the account does not exist.
|
|
*/
|
|
[[nodiscard]] std::uint32_t
|
|
ownerCount(Account const& account) const;
|
|
|
|
/**
|
|
* Return the number of sponsored objects owned by an account.
|
|
*
|
|
* @throws if the account does not exist.
|
|
*/
|
|
[[nodiscard]] std::uint32_t
|
|
sponsoredOwnerCount(Account const& account) const;
|
|
|
|
/**
|
|
* Return the number of sponsoring objects owned by an account.
|
|
*
|
|
* @throws if the account does not exist.
|
|
*/
|
|
[[nodiscard]] std::uint32_t
|
|
sponsoringOwnerCount(Account const& account) const;
|
|
|
|
/**
|
|
* Return the number of sponsoring accounts owned by an account.
|
|
*
|
|
* @throws if the account does not exist.
|
|
*/
|
|
[[nodiscard]] std::uint32_t
|
|
sponsoringAccountCount(Account const& account) const;
|
|
|
|
/**
|
|
* Return an account root.
|
|
* @return empty if the account does not exist.
|
|
*/
|
|
[[nodiscard]] SLE::const_pointer
|
|
le(Account const& account) const;
|
|
|
|
/**
|
|
* Return a ledger entry.
|
|
* @return empty if the ledger entry does not exist
|
|
*/
|
|
[[nodiscard]] SLE::const_pointer
|
|
le(Keylet const& k) const;
|
|
|
|
/**
|
|
* Create a JTx from parameters.
|
|
*/
|
|
template <class JsonValue, class... FN>
|
|
JTx
|
|
jt(JsonValue&& jv, FN const&... fN)
|
|
{
|
|
JTx jt(std::forward<JsonValue>(jv));
|
|
invoke(jt, fN...);
|
|
autofill(jt);
|
|
jt.stx = st(jt);
|
|
return jt;
|
|
}
|
|
|
|
/**
|
|
* Create a JTx from parameters.
|
|
*/
|
|
template <class JsonValue, class... FN>
|
|
JTx
|
|
jtnofill(JsonValue&& jv, FN const&... fN)
|
|
{
|
|
JTx jt(std::forward<JsonValue>(jv));
|
|
invoke(jt, fN...);
|
|
autofillSig(jt);
|
|
jt.stx = st(jt);
|
|
return jt;
|
|
}
|
|
|
|
/**
|
|
* Create JSON from parameters.
|
|
* This will apply funclets and autofill.
|
|
*/
|
|
template <class JsonValue, class... FN>
|
|
json::Value
|
|
json(JsonValue&& jv, FN const&... fN)
|
|
{
|
|
auto tj = jt(std::forward<JsonValue>(jv), fN...);
|
|
return std::move(tj.jv);
|
|
}
|
|
|
|
/**
|
|
* Check a set of requirements.
|
|
*
|
|
* The requirements are formed
|
|
* from condition functors.
|
|
*/
|
|
template <class... Args>
|
|
void
|
|
require(Args const&... args)
|
|
{
|
|
jtx::required(args...)(*this);
|
|
}
|
|
|
|
/**
|
|
* Gets the TER result and `didApply` flag from a RPC Json result object.
|
|
*/
|
|
static ParsedResult
|
|
parseResult(json::Value const& jr);
|
|
|
|
/**
|
|
* Submit an existing JTx.
|
|
* This calls postconditions.
|
|
*/
|
|
virtual void
|
|
submit(JTx const& jt, std::source_location const& loc = std::source_location::current());
|
|
|
|
/**
|
|
* Use the submit RPC command with a provided JTx object.
|
|
* This calls postconditions.
|
|
*/
|
|
void
|
|
signAndSubmit(
|
|
JTx const& jt,
|
|
json::Value params = json::ValueType::Null,
|
|
std::source_location const& loc = std::source_location::current());
|
|
|
|
/**
|
|
* Check expected postconditions
|
|
* of JTx submission.
|
|
*/
|
|
void
|
|
postconditions(
|
|
JTx const& jt,
|
|
ParsedResult const& parsed,
|
|
json::Value const& jr = json::Value(),
|
|
std::source_location const& loc = std::source_location::current());
|
|
|
|
/**
|
|
* Apply funclets and submit.
|
|
*/
|
|
/** @{ */
|
|
template <class... FN>
|
|
Env&
|
|
apply(WithSourceLocation<json::Value> jv, FN const&... fN)
|
|
{
|
|
submit(jt(std::move(jv.value), fN...), jv.loc);
|
|
return *this;
|
|
}
|
|
|
|
template <class... FN>
|
|
Env&
|
|
apply(WithSourceLocation<JTx> jv, FN const&... fN)
|
|
{
|
|
submit(jt(std::move(jv.value), fN...), jv.loc);
|
|
return *this;
|
|
}
|
|
|
|
template <class... FN>
|
|
Env&
|
|
operator()(WithSourceLocation<json::Value> jv, FN const&... fN)
|
|
{
|
|
return apply(std::move(jv), fN...);
|
|
}
|
|
|
|
template <class... FN>
|
|
Env&
|
|
operator()(WithSourceLocation<JTx> jv, FN const&... fN)
|
|
{
|
|
return apply(std::move(jv), fN...);
|
|
}
|
|
/** @} */
|
|
|
|
/**
|
|
* Return the TER for the last JTx.
|
|
*/
|
|
[[nodiscard]] TER
|
|
ter() const
|
|
{
|
|
return ter_;
|
|
}
|
|
|
|
/**
|
|
* Return metadata for the last JTx.
|
|
*
|
|
* NOTE: this has a side effect of closing the open ledger.
|
|
* The ledger will only be closed if it includes transactions.
|
|
*
|
|
* Effects:
|
|
*
|
|
* The open ledger is closed as if by a call
|
|
* to close(). The metadata for the last
|
|
* transaction ID, if any, is returned.
|
|
*/
|
|
std::shared_ptr<STObject const>
|
|
meta();
|
|
|
|
/**
|
|
* Return the tx data for the last JTx.
|
|
*
|
|
* Effects:
|
|
*
|
|
* The tx data for the last transaction
|
|
* ID, if any, is returned. No side
|
|
* effects.
|
|
*
|
|
* @note Only necessary for JTx submitted
|
|
* with via sign-and-submit method.
|
|
*/
|
|
[[nodiscard]] std::shared_ptr<STTx const>
|
|
tx() const;
|
|
|
|
void
|
|
enableFeature(uint256 const feature);
|
|
|
|
void
|
|
disableFeature(uint256 const feature);
|
|
|
|
[[nodiscard]] bool
|
|
enabled(uint256 feature) const
|
|
{
|
|
return current()->rules().enabled(feature);
|
|
}
|
|
|
|
private:
|
|
/**
|
|
* Drain the (single) server io_context thread once.
|
|
*
|
|
* Posts a barrier task to the server's io_context and blocks until it
|
|
* runs, so every task queued before it has been processed. Only meaningful
|
|
* with a single io thread (see syncClose()/disconnectClient()).
|
|
*
|
|
* @param timeout Maximum time to wait for the barrier task to execute
|
|
* @return true if the barrier ran within timeout, false otherwise
|
|
*/
|
|
[[nodiscard]] bool
|
|
drainServerIo(std::chrono::steady_clock::duration timeout)
|
|
{
|
|
auto barrier = std::make_shared<std::promise<void>>();
|
|
auto future = barrier->get_future();
|
|
boost::asio::post(app().getIOContext(), [barrier]() { barrier->set_value(); });
|
|
return future.wait_for(timeout) == std::future_status::ready;
|
|
}
|
|
|
|
void
|
|
fund(bool setDefaultRipple, STAmount const& amount, Account const& account);
|
|
|
|
void
|
|
fundArg(STAmount const& amount, Account const& account)
|
|
{
|
|
fund(true, amount, account);
|
|
}
|
|
|
|
template <std::size_t N>
|
|
void
|
|
fundArg(STAmount const& amount, std::array<Account, N> const& list)
|
|
{
|
|
for (auto const& account : list)
|
|
fund(false, amount, account);
|
|
}
|
|
|
|
public:
|
|
/**
|
|
* Create a new account with some XRP.
|
|
*
|
|
* These convenience functions are for easy set-up
|
|
* of the environment, they bypass fee, seq, and sig
|
|
* settings. The XRP is transferred from the master
|
|
* account.
|
|
*
|
|
* Preconditions:
|
|
* The account must not already exist
|
|
*
|
|
* Effects:
|
|
* The asfDefaultRipple on the account is set,
|
|
* and the sequence number is incremented, unless
|
|
* the account is wrapped with a call to noripple.
|
|
*
|
|
* The account's XRP balance is set to amount.
|
|
*
|
|
* Generates a test that the balance is set.
|
|
*
|
|
* @param amount The amount of XRP to transfer to
|
|
* each account.
|
|
*
|
|
* @param args A heterogeneous list of accounts to fund
|
|
* or calls to noripple with lists of accounts
|
|
* to fund.
|
|
*/
|
|
template <class Arg, class... Args>
|
|
void
|
|
fund(STAmount const& amount, Arg const& arg, Args const&... args)
|
|
{
|
|
fundArg(amount, arg);
|
|
if constexpr (sizeof...(args) > 0)
|
|
fund(amount, args...);
|
|
}
|
|
|
|
/**
|
|
* Establish trust lines.
|
|
*
|
|
* These convenience functions are for easy set-up
|
|
* of the environment, they bypass fee, seq, and sig
|
|
* settings.
|
|
*
|
|
* Preconditions:
|
|
* The account must already exist
|
|
*
|
|
* Effects:
|
|
* A trust line is added for the account.
|
|
* The account's sequence number is incremented.
|
|
* The account is refunded for the transaction fee
|
|
* to set the trust line.
|
|
*
|
|
* The refund comes from the master account.
|
|
*/
|
|
/** @{ */
|
|
void
|
|
trust(STAmount const& amount, Account const& account);
|
|
|
|
template <class... Accounts>
|
|
void
|
|
trust(STAmount const& amount, Account const& to0, Account const& to1, Accounts const&... toN)
|
|
{
|
|
trust(amount, to0);
|
|
trust(amount, to1, toN...); // NOLINT(readability-suspicious-call-argument)
|
|
}
|
|
/** @} */
|
|
|
|
/**
|
|
* Create a STTx from a JTx without sanitizing
|
|
* Use to inject bogus values into test transactions by first
|
|
* editing the JSON.
|
|
*/
|
|
std::shared_ptr<STTx const>
|
|
ust(JTx const& jt);
|
|
|
|
protected:
|
|
int trace_ = 0;
|
|
TestStopwatch stopwatch_;
|
|
uint256 txid_;
|
|
TER ter_ = tesSUCCESS;
|
|
bool parseFailureExpected_ = false;
|
|
unsigned retries_ = 5;
|
|
|
|
json::Value
|
|
doRpc(
|
|
unsigned apiVersion,
|
|
std::vector<std::string> const& args,
|
|
std::unordered_map<std::string, std::string> const& headers = {});
|
|
|
|
void
|
|
autofillSig(JTx& jt);
|
|
|
|
virtual void
|
|
autofill(JTx& jt);
|
|
|
|
/**
|
|
* Create a STTx from a JTx
|
|
* The framework requires that JSON is valid.
|
|
* On a parse error, the JSON is logged and
|
|
* an exception thrown.
|
|
*
|
|
* @throws ParseError
|
|
*/
|
|
std::shared_ptr<STTx const>
|
|
st(JTx const& jt);
|
|
|
|
// Invoke funclets on stx
|
|
// Note: The STTx may not be modified
|
|
template <class... FN>
|
|
void
|
|
invoke(STTx& stx, FN const&... fN)
|
|
{
|
|
(fN(*this, stx), ...);
|
|
}
|
|
|
|
// Invoke funclets on jt
|
|
template <class... FN>
|
|
void
|
|
invoke(JTx& jt, FN const&... fN)
|
|
{
|
|
(fN(*this, jt), ...);
|
|
}
|
|
|
|
// Map of account IDs to Account
|
|
std::unordered_map<AccountID, Account> map_;
|
|
};
|
|
|
|
template <class... Args>
|
|
json::Value
|
|
Env::rpc(
|
|
unsigned apiVersion,
|
|
std::unordered_map<std::string, std::string> const& headers,
|
|
std::string const& cmd,
|
|
Args&&... args)
|
|
{
|
|
return doRpc(apiVersion, std::vector<std::string>{cmd, std::forward<Args>(args)...}, headers);
|
|
}
|
|
|
|
template <class... Args>
|
|
json::Value
|
|
Env::rpc(unsigned apiVersion, std::string const& cmd, Args&&... args)
|
|
{
|
|
return rpc(
|
|
apiVersion,
|
|
std::unordered_map<std::string, std::string>(),
|
|
cmd,
|
|
std::forward<Args>(args)...);
|
|
}
|
|
|
|
template <class... Args>
|
|
json::Value
|
|
Env::rpc(
|
|
std::unordered_map<std::string, std::string> const& headers,
|
|
std::string const& cmd,
|
|
Args&&... args)
|
|
{
|
|
return doRpc(
|
|
rpc::kApiCommandLineVersion,
|
|
std::vector<std::string>{cmd, std::forward<Args>(args)...},
|
|
headers);
|
|
}
|
|
|
|
template <class... Args>
|
|
json::Value
|
|
Env::rpc(std::string const& cmd, Args&&... args)
|
|
{
|
|
return rpc(std::unordered_map<std::string, std::string>(), cmd, std::forward<Args>(args)...);
|
|
}
|
|
|
|
} // namespace xrpl::test::jtx
|