mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-02 01:52:32 +00:00
Compare commits
49 Commits
develop
...
a1q123456/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2e8e37064 | ||
|
|
64469a75f8 | ||
|
|
1406354e36 | ||
|
|
a65fcfd152 | ||
|
|
c5d4d6f2fa | ||
|
|
aebae05e3e | ||
|
|
371f1b8111 | ||
|
|
81f221155e | ||
|
|
9b3b71e7ab | ||
|
|
0e2855c6ba | ||
|
|
0ea073bc6e | ||
|
|
a3d91e0e06 | ||
|
|
62ad0269db | ||
|
|
abb800c9c6 | ||
|
|
b7c61e046f | ||
|
|
62e9277f51 | ||
|
|
e50f8f591b | ||
|
|
7ed3277b89 | ||
|
|
55812aa7cb | ||
|
|
d26f6bfa2b | ||
|
|
72aa773bc4 | ||
|
|
11ab8ec33a | ||
|
|
0d125b15c6 | ||
|
|
5834f0cced | ||
|
|
7f1f5409ad | ||
|
|
368845c992 | ||
|
|
515bd1add5 | ||
|
|
8d3ed36e5c | ||
|
|
70a85c15ae | ||
|
|
199bc0c930 | ||
|
|
ef42a8fe6d | ||
|
|
d3a12746b4 | ||
|
|
0cb5f70628 | ||
|
|
37ea600d12 | ||
|
|
665e607b7d | ||
|
|
423ab87c25 | ||
|
|
b5717e0fcf | ||
|
|
a3eaae1d04 | ||
|
|
424b93089e | ||
|
|
22d93d2b1c | ||
|
|
fb50ce0e64 | ||
|
|
84b3aab930 | ||
|
|
3a5937e6bd | ||
|
|
e4810a69cb | ||
|
|
6675801008 | ||
|
|
474992f9fe | ||
|
|
672f35aed5 | ||
|
|
78d8f89267 | ||
|
|
34c49ff166 |
@@ -176,10 +176,16 @@ test.toplevel > xrpl.json
|
||||
test.unit_test > xrpl.basics
|
||||
test.unit_test > xrpl.protocol
|
||||
tests.libxrpl > xrpl.basics
|
||||
tests.libxrpl > xrpl.core
|
||||
tests.libxrpl > xrpl.json
|
||||
tests.libxrpl > xrpl.ledger
|
||||
tests.libxrpl > xrpl.net
|
||||
tests.libxrpl > xrpl.nodestore
|
||||
tests.libxrpl > xrpl.protocol
|
||||
tests.libxrpl > xrpl.protocol_autogen
|
||||
tests.libxrpl > xrpl.server
|
||||
tests.libxrpl > xrpl.shamap
|
||||
tests.libxrpl > xrpl.tx
|
||||
xrpl.conditions > xrpl.basics
|
||||
xrpl.conditions > xrpl.protocol
|
||||
xrpl.core > xrpl.basics
|
||||
|
||||
@@ -8,9 +8,12 @@ add_custom_target(xrpl.tests)
|
||||
|
||||
# Test helpers
|
||||
add_library(xrpl.helpers.test STATIC)
|
||||
target_sources(xrpl.helpers.test PRIVATE helpers/TestSink.cpp)
|
||||
target_sources(
|
||||
xrpl.helpers.test
|
||||
PRIVATE helpers/Account.cpp helpers/TestSink.cpp helpers/TxTest.cpp
|
||||
)
|
||||
target_include_directories(xrpl.helpers.test PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(xrpl.helpers.test PRIVATE xrpl.libxrpl)
|
||||
target_link_libraries(xrpl.helpers.test PUBLIC xrpl.libxrpl gtest::gtest)
|
||||
|
||||
# Common library dependencies for the rest of the tests.
|
||||
add_library(xrpl.imports.test INTERFACE)
|
||||
@@ -32,6 +35,10 @@ xrpl_add_test(json)
|
||||
target_link_libraries(xrpl.test.json PRIVATE xrpl.imports.test)
|
||||
add_dependencies(xrpl.tests xrpl.test.json)
|
||||
|
||||
xrpl_add_test(tx)
|
||||
target_link_libraries(xrpl.test.tx PRIVATE xrpl.imports.test)
|
||||
add_dependencies(xrpl.tests xrpl.test.tx)
|
||||
|
||||
# protocol_autogen tests use explicit source list (not GLOB) because sources are generated
|
||||
# Mark generated sources so CMake knows they'll be created at build time
|
||||
set_source_files_properties(
|
||||
|
||||
16
src/tests/libxrpl/helpers/Account.cpp
Normal file
16
src/tests/libxrpl/helpers/Account.cpp
Normal file
@@ -0,0 +1,16 @@
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
|
||||
#include <helpers/Account.h>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
Account const Account::master{"masterpassphrase"};
|
||||
|
||||
Account::Account(std::string_view name, KeyType type)
|
||||
: name_(name)
|
||||
, keyPair_(generateKeyPair(type, generateSeed(name_)))
|
||||
, id_(calcAccountID(keyPair_.first))
|
||||
{
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
81
src/tests/libxrpl/helpers/Account.h
Normal file
81
src/tests/libxrpl/helpers/Account.h
Normal file
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
/**
|
||||
* @brief A test account with cryptographic keys.
|
||||
*
|
||||
* Generates keys deterministically from a name, making tests reproducible.
|
||||
* The same name always produces the same AccountID and keys.
|
||||
*/
|
||||
class Account
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief The master account that holds all XRP in genesis.
|
||||
*
|
||||
* This account is created in the genesis ledger with all 100 billion XRP.
|
||||
* It uses the well-known seed "masterpassphrase".
|
||||
*/
|
||||
static Account const master;
|
||||
|
||||
/**
|
||||
* @brief Create an account from a name.
|
||||
*
|
||||
* Keys are derived deterministically from the name.
|
||||
*
|
||||
* @param name Human-readable name for the account.
|
||||
* @param type Key type to use (defaults to secp256k1).
|
||||
*/
|
||||
explicit Account(std::string_view name, KeyType type = KeyType::secp256k1);
|
||||
|
||||
/** @brief Return the human-readable name. */
|
||||
std::string const&
|
||||
name() const noexcept
|
||||
{
|
||||
return name_;
|
||||
}
|
||||
|
||||
/** @brief Return the AccountID. */
|
||||
AccountID const&
|
||||
id() const noexcept
|
||||
{
|
||||
return id_;
|
||||
}
|
||||
|
||||
/** @brief Return the public key. */
|
||||
PublicKey const&
|
||||
pk() const noexcept
|
||||
{
|
||||
return keyPair_.first;
|
||||
}
|
||||
|
||||
/** @brief Return the secret key. */
|
||||
SecretKey const&
|
||||
sk() const noexcept
|
||||
{
|
||||
return keyPair_.second;
|
||||
}
|
||||
|
||||
/** @brief Implicit conversion to AccountID. */
|
||||
operator AccountID const&() const noexcept
|
||||
{
|
||||
return id_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
std::pair<PublicKey, SecretKey> keyPair_;
|
||||
AccountID id_;
|
||||
};
|
||||
|
||||
} // namespace xrpl::test
|
||||
131
src/tests/libxrpl/helpers/IOU.h
Normal file
131
src/tests/libxrpl/helpers/IOU.h
Normal file
@@ -0,0 +1,131 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <helpers/Account.h>
|
||||
|
||||
#include <concepts>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
/**
|
||||
* @brief Represents an IOU (issued currency) for testing.
|
||||
*
|
||||
* Provides a clear, explicit API for creating currencies issued by an account.
|
||||
* This replaces the cryptic `Account::operator[]` from the jtx framework.
|
||||
*
|
||||
* @code
|
||||
* Account gw("gateway");
|
||||
* IOU USD("USD", gw);
|
||||
*
|
||||
* auto issue = USD.issue(); // Get the Issue
|
||||
* auto asset = USD.asset(); // Get the Asset
|
||||
* auto amt = USD.amount(100); // Get STAmount of 100 USD
|
||||
* @endcode
|
||||
*/
|
||||
class IOU
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct an IOU from a currency code and issuing account.
|
||||
* @param currencyCode A 3-character ISO currency code (e.g., "USD").
|
||||
* @param issuer The account that issues this currency.
|
||||
*/
|
||||
IOU(std::string_view currencyCode, Account const& issuer)
|
||||
: currency_(to_currency(std::string(currencyCode))), issuer_(issuer.id())
|
||||
{
|
||||
XRPL_ASSERT(!isXRP(currency_), "IOU: currency code must not resolve to XRP");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct an IOU from a Currency and issuing account.
|
||||
* @param currency The Currency object.
|
||||
* @param issuer The account that issues this currency.
|
||||
*/
|
||||
IOU(Currency currency, Account const& issuer)
|
||||
: currency_(std::move(currency)), issuer_(issuer.id())
|
||||
{
|
||||
XRPL_ASSERT(!isXRP(currency_), "IOU: currency code must not resolve to XRP");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the Issue (currency + issuer pair).
|
||||
* @return An Issue object representing this IOU.
|
||||
*/
|
||||
[[nodiscard]] Issue
|
||||
issue() const
|
||||
{
|
||||
return Issue{currency_, issuer_};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the Asset.
|
||||
* @return An Asset object representing this IOU.
|
||||
*/
|
||||
[[nodiscard]] Asset
|
||||
asset() const
|
||||
{
|
||||
return Asset{issue()};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create an STAmount of this IOU.
|
||||
*
|
||||
* Works with any arithmetic type (int, double, etc.) by converting
|
||||
* to string and parsing. This matches the jtx IOU behaviour.
|
||||
*
|
||||
* @tparam T An arithmetic type.
|
||||
* @param value The amount as any arithmetic type.
|
||||
* @return An STAmount representing value units of this IOU.
|
||||
*/
|
||||
template <typename T>
|
||||
requires std::is_arithmetic_v<T>
|
||||
[[nodiscard]] STAmount
|
||||
amount(T value) const
|
||||
{
|
||||
return amountFromString(issue(), to_string(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create an STAmount of this IOU from a Number.
|
||||
* @param value The amount as a Number.
|
||||
* @return An STAmount representing value units of this IOU.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
amount(Number const& value) const
|
||||
{
|
||||
return STAmount{issue(), value};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the currency.
|
||||
* @return The currency.
|
||||
*/
|
||||
[[nodiscard]] Currency const&
|
||||
currency() const
|
||||
{
|
||||
return currency_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the issuer account ID.
|
||||
* @return The issuer's AccountID.
|
||||
*/
|
||||
[[nodiscard]] AccountID const&
|
||||
issuer() const
|
||||
{
|
||||
return issuer_;
|
||||
}
|
||||
|
||||
private:
|
||||
Currency currency_;
|
||||
AccountID issuer_;
|
||||
};
|
||||
|
||||
} // namespace xrpl::test
|
||||
109
src/tests/libxrpl/helpers/TestFamily.h
Normal file
109
src/tests/libxrpl/helpers/TestFamily.h
Normal file
@@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/nodestore/DummyScheduler.h>
|
||||
#include <xrpl/nodestore/Manager.h>
|
||||
#include <xrpl/shamap/Family.h>
|
||||
|
||||
namespace xrpl {
|
||||
namespace test {
|
||||
|
||||
/** Test implementation of Family for unit tests.
|
||||
|
||||
Uses an in-memory NodeStore database and simple caches.
|
||||
The missingNode methods throw since tests shouldn't encounter missing nodes.
|
||||
*/
|
||||
class TestFamily : public Family
|
||||
{
|
||||
private:
|
||||
std::unique_ptr<NodeStore::Database> db_;
|
||||
TestStopwatch clock_;
|
||||
std::shared_ptr<FullBelowCache> fbCache_;
|
||||
std::shared_ptr<TreeNodeCache> tnCache_;
|
||||
NodeStore::DummyScheduler scheduler_;
|
||||
beast::Journal j_;
|
||||
|
||||
public:
|
||||
explicit TestFamily(beast::Journal j)
|
||||
: fbCache_(std::make_shared<FullBelowCache>("TestFamily full below cache", clock_, j))
|
||||
, tnCache_(
|
||||
std::make_shared<TreeNodeCache>(
|
||||
"TestFamily tree node cache",
|
||||
65536,
|
||||
std::chrono::minutes{1},
|
||||
clock_,
|
||||
j))
|
||||
, j_(j)
|
||||
{
|
||||
Section config;
|
||||
config.set("type", "memory");
|
||||
config.set("path", "TestFamily");
|
||||
db_ = NodeStore::Manager::instance().make_Database(megabytes(4), scheduler_, 1, config, j);
|
||||
}
|
||||
|
||||
NodeStore::Database&
|
||||
db() override
|
||||
{
|
||||
return *db_;
|
||||
}
|
||||
|
||||
NodeStore::Database const&
|
||||
db() const override
|
||||
{
|
||||
return *db_;
|
||||
}
|
||||
|
||||
beast::Journal const&
|
||||
journal() override
|
||||
{
|
||||
return j_;
|
||||
}
|
||||
|
||||
std::shared_ptr<FullBelowCache>
|
||||
getFullBelowCache() override
|
||||
{
|
||||
return fbCache_;
|
||||
}
|
||||
|
||||
std::shared_ptr<TreeNodeCache>
|
||||
getTreeNodeCache() override
|
||||
{
|
||||
return tnCache_;
|
||||
}
|
||||
|
||||
void
|
||||
sweep() override
|
||||
{
|
||||
fbCache_->sweep();
|
||||
tnCache_->sweep();
|
||||
}
|
||||
|
||||
void
|
||||
missingNodeAcquireBySeq(std::uint32_t refNum, uint256 const& nodeHash) override
|
||||
{
|
||||
Throw<std::runtime_error>("TestFamily: missing node (by seq)");
|
||||
}
|
||||
|
||||
void
|
||||
missingNodeAcquireByHash(uint256 const& refHash, std::uint32_t refNum) override
|
||||
{
|
||||
Throw<std::runtime_error>("TestFamily: missing node (by hash)");
|
||||
}
|
||||
|
||||
void
|
||||
reset() override
|
||||
{
|
||||
fbCache_->reset();
|
||||
tnCache_->reset();
|
||||
}
|
||||
|
||||
/** Access the test clock for time manipulation in tests. */
|
||||
TestStopwatch&
|
||||
clock()
|
||||
{
|
||||
return clock_;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
} // namespace xrpl
|
||||
378
src/tests/libxrpl/helpers/TestServiceRegistry.h
Normal file
378
src/tests/libxrpl/helpers/TestServiceRegistry.h
Normal file
@@ -0,0 +1,378 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/core/HashRouter.h>
|
||||
#include <xrpl/core/NetworkIDService.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/PendingSaves.h>
|
||||
#include <xrpl/server/LoadFeeTrack.h>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
#include <helpers/TestFamily.h>
|
||||
#include <helpers/TestSink.h>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace xrpl {
|
||||
namespace test {
|
||||
|
||||
/** Logs implementation that creates TestSink instances. */
|
||||
class TestLogs : public Logs
|
||||
{
|
||||
public:
|
||||
explicit TestLogs(beast::severities::Severity level = beast::severities::kWarning) : Logs(level)
|
||||
{
|
||||
}
|
||||
|
||||
std::unique_ptr<beast::Journal::Sink>
|
||||
makeSink(std::string const& partition, beast::severities::Severity threshold) override
|
||||
{
|
||||
return std::make_unique<TestSink>(threshold);
|
||||
}
|
||||
};
|
||||
|
||||
/** Simple NetworkIDService implementation for tests. */
|
||||
class TestNetworkIDService final : public NetworkIDService
|
||||
{
|
||||
public:
|
||||
explicit TestNetworkIDService(std::uint32_t networkID = 0) : networkID_(networkID)
|
||||
{
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
getNetworkID() const noexcept override
|
||||
{
|
||||
return networkID_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::uint32_t networkID_;
|
||||
};
|
||||
|
||||
/** Test implementation of ServiceRegistry for unit tests.
|
||||
|
||||
This class provides real implementations for services that can be
|
||||
instantiated from libxrpl (such as Logs, io_context, caches), and
|
||||
throws std::logic_error for services that require the full Application.
|
||||
|
||||
Tests can subclass this to provide additional services they need.
|
||||
*/
|
||||
class TestServiceRegistry : public ServiceRegistry
|
||||
{
|
||||
private:
|
||||
TestLogs logs_{beast::severities::kWarning};
|
||||
boost::asio::io_context io_context_;
|
||||
TestFamily family_{logs_.journal("TestFamily")};
|
||||
LoadFeeTrack feeTrack_{logs_.journal("LoadFeeTrack")};
|
||||
TestNetworkIDService networkIDService_;
|
||||
HashRouter hashRouter_{HashRouter::Setup{}, stopwatch()};
|
||||
NodeCache tempNodeCache_{
|
||||
"TempNodeCache",
|
||||
16384,
|
||||
std::chrono::minutes{1},
|
||||
stopwatch(),
|
||||
logs_.journal("TaggedCache")};
|
||||
CachedSLEs cachedSLEs_{
|
||||
"CachedSLEs",
|
||||
16384,
|
||||
std::chrono::minutes{1},
|
||||
stopwatch(),
|
||||
logs_.journal("TaggedCache")};
|
||||
PendingSaves pendingSaves_;
|
||||
std::optional<uint256> trapTxID_;
|
||||
|
||||
public:
|
||||
TestServiceRegistry() = default;
|
||||
~TestServiceRegistry() override = default;
|
||||
|
||||
// Core infrastructure services
|
||||
CollectorManager&
|
||||
getCollectorManager() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getCollectorManager() not implemented");
|
||||
}
|
||||
|
||||
Family&
|
||||
getNodeFamily() override
|
||||
{
|
||||
return family_;
|
||||
}
|
||||
|
||||
TimeKeeper&
|
||||
timeKeeper() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::timeKeeper() not implemented");
|
||||
}
|
||||
|
||||
JobQueue&
|
||||
getJobQueue() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getJobQueue() not implemented");
|
||||
}
|
||||
|
||||
NodeCache&
|
||||
getTempNodeCache() override
|
||||
{
|
||||
return tempNodeCache_;
|
||||
}
|
||||
|
||||
CachedSLEs&
|
||||
cachedSLEs() override
|
||||
{
|
||||
return cachedSLEs_;
|
||||
}
|
||||
|
||||
NetworkIDService&
|
||||
getNetworkIDService() override
|
||||
{
|
||||
return networkIDService_;
|
||||
}
|
||||
|
||||
// Protocol and validation services
|
||||
AmendmentTable&
|
||||
getAmendmentTable() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getAmendmentTable() not implemented");
|
||||
}
|
||||
|
||||
HashRouter&
|
||||
getHashRouter() override
|
||||
{
|
||||
return hashRouter_;
|
||||
}
|
||||
|
||||
LoadFeeTrack&
|
||||
getFeeTrack() override
|
||||
{
|
||||
return feeTrack_;
|
||||
}
|
||||
|
||||
LoadManager&
|
||||
getLoadManager() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getLoadManager() not implemented");
|
||||
}
|
||||
|
||||
RCLValidations&
|
||||
getValidations() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getValidations() not implemented");
|
||||
}
|
||||
|
||||
ValidatorList&
|
||||
validators() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::validators() not implemented");
|
||||
}
|
||||
|
||||
ValidatorSite&
|
||||
validatorSites() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::validatorSites() not implemented");
|
||||
}
|
||||
|
||||
ManifestCache&
|
||||
validatorManifests() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::validatorManifests() not implemented");
|
||||
}
|
||||
|
||||
ManifestCache&
|
||||
publisherManifests() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::publisherManifests() not implemented");
|
||||
}
|
||||
|
||||
// Network services
|
||||
Overlay&
|
||||
overlay() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::overlay() not implemented");
|
||||
}
|
||||
|
||||
Cluster&
|
||||
cluster() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::cluster() not implemented");
|
||||
}
|
||||
|
||||
PeerReservationTable&
|
||||
peerReservations() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::peerReservations() not implemented");
|
||||
}
|
||||
|
||||
Resource::Manager&
|
||||
getResourceManager() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getResourceManager() not implemented");
|
||||
}
|
||||
|
||||
// Storage services
|
||||
NodeStore::Database&
|
||||
getNodeStore() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getNodeStore() not implemented");
|
||||
}
|
||||
|
||||
SHAMapStore&
|
||||
getSHAMapStore() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getSHAMapStore() not implemented");
|
||||
}
|
||||
|
||||
RelationalDatabase&
|
||||
getRelationalDatabase() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getRelationalDatabase() not implemented");
|
||||
}
|
||||
|
||||
// Ledger services
|
||||
InboundLedgers&
|
||||
getInboundLedgers() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getInboundLedgers() not implemented");
|
||||
}
|
||||
|
||||
InboundTransactions&
|
||||
getInboundTransactions() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getInboundTransactions() not implemented");
|
||||
}
|
||||
|
||||
TaggedCache<uint256, AcceptedLedger>&
|
||||
getAcceptedLedgerCache() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getAcceptedLedgerCache() not implemented");
|
||||
}
|
||||
|
||||
LedgerMaster&
|
||||
getLedgerMaster() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getLedgerMaster() not implemented");
|
||||
}
|
||||
|
||||
LedgerCleaner&
|
||||
getLedgerCleaner() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getLedgerCleaner() not implemented");
|
||||
}
|
||||
|
||||
LedgerReplayer&
|
||||
getLedgerReplayer() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getLedgerReplayer() not implemented");
|
||||
}
|
||||
|
||||
PendingSaves&
|
||||
pendingSaves() override
|
||||
{
|
||||
return pendingSaves_;
|
||||
}
|
||||
|
||||
OpenLedger&
|
||||
openLedger() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::openLedger() not implemented");
|
||||
}
|
||||
|
||||
OpenLedger const&
|
||||
openLedger() const override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::openLedger() const not implemented");
|
||||
}
|
||||
|
||||
// Transaction and operation services
|
||||
NetworkOPs&
|
||||
getOPs() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getOPs() not implemented");
|
||||
}
|
||||
|
||||
OrderBookDB&
|
||||
getOrderBookDB() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getOrderBookDB() not implemented");
|
||||
}
|
||||
|
||||
TransactionMaster&
|
||||
getMasterTransaction() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getMasterTransaction() not implemented");
|
||||
}
|
||||
|
||||
TxQ&
|
||||
getTxQ() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getTxQ() not implemented");
|
||||
}
|
||||
|
||||
PathRequestManager&
|
||||
getPathRequestManager() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getPathRequestManager() not implemented");
|
||||
}
|
||||
|
||||
// Server services
|
||||
ServerHandler&
|
||||
getServerHandler() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getServerHandler() not implemented");
|
||||
}
|
||||
|
||||
perf::PerfLog&
|
||||
getPerfLog() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getPerfLog() not implemented");
|
||||
}
|
||||
|
||||
// Configuration and state
|
||||
bool
|
||||
isStopping() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
beast::Journal
|
||||
journal(std::string const& name) override
|
||||
{
|
||||
return logs_.journal(name);
|
||||
}
|
||||
|
||||
boost::asio::io_context&
|
||||
getIOContext() override
|
||||
{
|
||||
return io_context_;
|
||||
}
|
||||
|
||||
Logs&
|
||||
logs() override
|
||||
{
|
||||
return logs_;
|
||||
}
|
||||
|
||||
std::optional<uint256> const&
|
||||
trapTxID() const override
|
||||
{
|
||||
return trapTxID_;
|
||||
}
|
||||
|
||||
DatabaseCon&
|
||||
getWalletDB() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getWalletDB() not implemented");
|
||||
}
|
||||
|
||||
// Temporary: Get the underlying Application
|
||||
Application&
|
||||
app() override
|
||||
{
|
||||
throw std::logic_error(
|
||||
"TestServiceRegistry::app() not implemented - no Application available in tests");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
} // namespace xrpl
|
||||
238
src/tests/libxrpl/helpers/TxTest.cpp
Normal file
238
src/tests/libxrpl/helpers/TxTest.cpp
Normal file
@@ -0,0 +1,238 @@
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/ledger/AmendmentTable.h>
|
||||
#include <xrpl/ledger/CanonicalTXSet.h>
|
||||
#include <xrpl/ledger/Ledger.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/AccountRoot.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/RippleState.h>
|
||||
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
|
||||
#include <xrpl/protocol_autogen/transactions/Payment.h>
|
||||
#include <xrpl/tx/apply.h>
|
||||
|
||||
#include <helpers/TxTest.h>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Feature helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
FeatureBitset
|
||||
allFeatures()
|
||||
{
|
||||
static FeatureBitset const features = [] {
|
||||
auto const& sa = allAmendments();
|
||||
std::vector<uint256> feats;
|
||||
feats.reserve(sa.size());
|
||||
for (auto const& [name, vote] : sa)
|
||||
{
|
||||
(void)vote;
|
||||
if (auto const f = getRegisteredFeature(name))
|
||||
feats.push_back(*f);
|
||||
}
|
||||
return FeatureBitset(feats);
|
||||
}();
|
||||
return features;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// TxTest
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
TxTest::TxTest(std::optional<FeatureBitset> features)
|
||||
{
|
||||
// Convert FeatureBitset to unordered_set for Rules constructor
|
||||
auto const featureBits = features.value_or(allFeatures());
|
||||
foreachFeature(featureBits, [&](uint256 const& f) { featureSet_.insert(f); });
|
||||
|
||||
// Create rules with the specified features
|
||||
rules_ = std::make_unique<Rules>(featureSet_);
|
||||
|
||||
// Default fees for testing
|
||||
Fees const fees{XRPAmount{10}, XRPAmount{10000000}, XRPAmount{2000000}};
|
||||
|
||||
// Create a genesis ledger as the base
|
||||
closedLedger_ = std::make_shared<Ledger>(
|
||||
create_genesis,
|
||||
*rules_,
|
||||
fees,
|
||||
std::vector<uint256>{featureSet_.begin(), featureSet_.end()},
|
||||
registry_.getNodeFamily());
|
||||
|
||||
// Initialize time from the genesis ledger
|
||||
now_ = closedLedger_->header().closeTime;
|
||||
|
||||
// Create an open view on top of the genesis ledger
|
||||
openLedger_ =
|
||||
std::make_shared<OpenView>(open_ledger, closedLedger_.get(), *rules_, closedLedger_);
|
||||
}
|
||||
|
||||
bool
|
||||
TxTest::isEnabled(uint256 const& feature) const
|
||||
{
|
||||
return rules_->enabled(feature);
|
||||
}
|
||||
|
||||
Rules const&
|
||||
TxTest::getRules() const
|
||||
{
|
||||
return *rules_;
|
||||
}
|
||||
|
||||
[[nodiscard]] TxResult
|
||||
TxTest::submit(std::shared_ptr<STTx const> stx)
|
||||
{
|
||||
auto const result = apply(registry_, *openLedger_, *stx, tapNONE, registry_.journal("apply"));
|
||||
|
||||
// Track successfully applied transactions for canonical reordering on close
|
||||
// We make a copy since the TransactionBase doesn't own the STTx
|
||||
if (result.applied)
|
||||
pendingTxs_.push_back(stx);
|
||||
|
||||
return TxResult{
|
||||
.ter = result.ter,
|
||||
.applied = result.applied,
|
||||
.metadata = std::move(result.metadata),
|
||||
.tx = stx};
|
||||
}
|
||||
|
||||
void
|
||||
TxTest::createAccount(Account const& account, XRPAmount xrp, uint32_t accountFlags)
|
||||
{
|
||||
auto const paymentTer =
|
||||
submit(transactions::PaymentBuilder{Account::master, account, xrp}, Account::master).ter;
|
||||
|
||||
if (paymentTer != tesSUCCESS)
|
||||
{
|
||||
throw std::runtime_error("TxTest::createAccount: failed to create account");
|
||||
}
|
||||
|
||||
close();
|
||||
|
||||
if (accountFlags != 0)
|
||||
{
|
||||
auto const accountSetTer =
|
||||
submit(transactions::AccountSetBuilder{account}.setSetFlag(accountFlags), account).ter;
|
||||
if (accountSetTer != tesSUCCESS)
|
||||
{
|
||||
throw std::runtime_error("TxTest::createAccount: failed to set account flags");
|
||||
}
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
ledger_entries::AccountRoot
|
||||
TxTest::getAccountRoot(AccountID const& id) const
|
||||
{
|
||||
auto const sle = getOpenLedger().read(keylet::account(id));
|
||||
if (!sle)
|
||||
Throw<std::runtime_error>("TxTest::getAccountRoot: account not found");
|
||||
return ledger_entries::AccountRoot{std::const_pointer_cast<SLE const>(sle)};
|
||||
}
|
||||
|
||||
OpenView&
|
||||
TxTest::getOpenLedger()
|
||||
{
|
||||
return *openLedger_;
|
||||
}
|
||||
|
||||
OpenView const&
|
||||
TxTest::getOpenLedger() const
|
||||
{
|
||||
return *openLedger_;
|
||||
}
|
||||
|
||||
ReadView const&
|
||||
TxTest::getClosedLedger() const
|
||||
{
|
||||
return *closedLedger_;
|
||||
}
|
||||
|
||||
void
|
||||
TxTest::close()
|
||||
{
|
||||
// Build a new closed ledger from the previous closed ledger,
|
||||
// similar to how buildLedgerImpl works:
|
||||
// 1. Create a new Ledger from the previous closed ledger
|
||||
// 2. Re-apply transactions in canonical order
|
||||
// 3. Mark it as accepted/immutable
|
||||
|
||||
auto const& prevLedger = *closedLedger_;
|
||||
|
||||
// Use provided close time, or advance by resolution
|
||||
auto const ledgerCloseTime = now_ + prevLedger.header().closeTimeResolution;
|
||||
|
||||
// Update our tracked time
|
||||
now_ = ledgerCloseTime;
|
||||
|
||||
// Create new ledger following the previous one
|
||||
auto newLedger = std::make_shared<Ledger>(prevLedger, ledgerCloseTime);
|
||||
|
||||
// Put transactions into canonical order
|
||||
// The salt is the hash of the previous ledger (used to randomize ordering)
|
||||
CanonicalTXSet txSet(prevLedger.header().hash);
|
||||
for (auto const& tx : pendingTxs_)
|
||||
txSet.insert(tx);
|
||||
|
||||
// Create an OpenView on the new ledger and apply transactions in order
|
||||
{
|
||||
OpenView accum(&*newLedger);
|
||||
for (auto const& [key, tx] : txSet)
|
||||
{
|
||||
auto result = apply(registry_, accum, *tx, tapNONE, registry_.journal("apply"));
|
||||
if (!result.applied)
|
||||
{
|
||||
throw std::runtime_error("TxTest::close: failed to apply transaction");
|
||||
}
|
||||
}
|
||||
accum.apply(*newLedger);
|
||||
}
|
||||
|
||||
// Mark the ledger as accepted (makes it immutable)
|
||||
newLedger->setAccepted(
|
||||
ledgerCloseTime,
|
||||
newLedger->header().closeTimeResolution,
|
||||
true); // closeTimeCorrect
|
||||
|
||||
// The new ledger becomes our closed ledger
|
||||
closedLedger_ = newLedger;
|
||||
|
||||
// Clear pending transactions
|
||||
pendingTxs_.clear();
|
||||
|
||||
// Create a fresh open view on top of the new closed ledger
|
||||
// Use our rules_ to ensure consistent feature flags
|
||||
openLedger_ =
|
||||
std::make_shared<OpenView>(open_ledger, closedLedger_.get(), *rules_, closedLedger_);
|
||||
}
|
||||
|
||||
void
|
||||
TxTest::advanceTime(NetClock::duration duration)
|
||||
{
|
||||
now_ += duration;
|
||||
}
|
||||
|
||||
NetClock::time_point
|
||||
TxTest::getCloseTime() const
|
||||
{
|
||||
return now_;
|
||||
}
|
||||
|
||||
STAmount
|
||||
TxTest::getBalance(AccountID const& account, IOU const& iou) const
|
||||
{
|
||||
auto const sle = openLedger_->read(keylet::line(account, iou.issue()));
|
||||
if (!sle)
|
||||
return STAmount{iou.issue(), 0};
|
||||
|
||||
auto const rippleState = ledger_entries::RippleState{sle};
|
||||
|
||||
auto balance = rippleState.getBalance();
|
||||
balance.setIssuer(iou.issue().account);
|
||||
if (account > iou.issue().account)
|
||||
balance.negate();
|
||||
return balance;
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
347
src/tests/libxrpl/helpers/TxTest.h
Normal file
347
src/tests/libxrpl/helpers/TxTest.h
Normal file
@@ -0,0 +1,347 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyViewImpl.h>
|
||||
#include <xrpl/ledger/Ledger.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/AccountRoot.h>
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/Account.h>
|
||||
#include <helpers/IOU.h>
|
||||
#include <helpers/TestServiceRegistry.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <concepts>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Amount helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Convert XRP to drops (integral types).
|
||||
* @param xrp The amount in XRP.
|
||||
* @return The equivalent amount in drops as XRPAmount.
|
||||
*/
|
||||
template <std::integral T>
|
||||
constexpr XRPAmount
|
||||
XRP(T xrp)
|
||||
{
|
||||
return XRPAmount{static_cast<std::int64_t>(xrp) * DROPS_PER_XRP.drops()};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert XRP to drops (floating point types).
|
||||
* @param xrp The amount in XRP (may be fractional).
|
||||
* @return The equivalent amount in drops as XRPAmount.
|
||||
*/
|
||||
template <std::floating_point T>
|
||||
XRPAmount
|
||||
XRP(T xrp)
|
||||
{
|
||||
return XRPAmount{static_cast<std::int64_t>(std::round(xrp * DROPS_PER_XRP.drops()))};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert XRP to drops (Number type).
|
||||
* @param xrp The amount in XRP as a Number.
|
||||
* @return The equivalent amount in drops as XRPAmount.
|
||||
*/
|
||||
inline XRPAmount
|
||||
XRP(Number const& xrp)
|
||||
{
|
||||
return XRPAmount{static_cast<std::int64_t>(xrp * DROPS_PER_XRP.drops())};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Flag helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Convert AccountSet flag (asf) to LedgerState flag (lsf).
|
||||
* @param asf The AccountSet flag value.
|
||||
* @return The corresponding LedgerState flag, or 0 if unknown.
|
||||
*/
|
||||
constexpr std::uint32_t
|
||||
asfToLsf(std::uint32_t asf)
|
||||
{
|
||||
switch (asf)
|
||||
{
|
||||
case asfRequireDest:
|
||||
return lsfRequireDestTag;
|
||||
case asfRequireAuth:
|
||||
return lsfRequireAuth;
|
||||
case asfDisallowXRP:
|
||||
return lsfDisallowXRP;
|
||||
case asfDisableMaster:
|
||||
return lsfDisableMaster;
|
||||
case asfNoFreeze:
|
||||
return lsfNoFreeze;
|
||||
case asfGlobalFreeze:
|
||||
return lsfGlobalFreeze;
|
||||
case asfDefaultRipple:
|
||||
return lsfDefaultRipple;
|
||||
case asfDepositAuth:
|
||||
return lsfDepositAuth;
|
||||
case asfAllowTrustLineClawback:
|
||||
return lsfAllowTrustLineClawback;
|
||||
case asfDisallowIncomingCheck:
|
||||
return lsfDisallowIncomingCheck;
|
||||
case asfDisallowIncomingNFTokenOffer:
|
||||
return lsfDisallowIncomingNFTokenOffer;
|
||||
case asfDisallowIncomingPayChan:
|
||||
return lsfDisallowIncomingPayChan;
|
||||
case asfDisallowIncomingTrustline:
|
||||
return lsfDisallowIncomingTrustline;
|
||||
case asfAllowTrustLineLocking:
|
||||
return lsfAllowTrustLineLocking;
|
||||
default:
|
||||
throw std::runtime_error("Unknown asf flag");
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Feature helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Returns all testable amendments.
|
||||
* @note This is similar to jtx::testable_amendments() but for the TxTest framework.
|
||||
*/
|
||||
FeatureBitset
|
||||
allFeatures();
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// TxResult
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Result of a transaction submission in TxTest.
|
||||
*
|
||||
* Contains the TER code, whether the transaction was applied,
|
||||
* optional metadata, and a reference to the submitted transaction.
|
||||
* Use standard gtest macros (EXPECT_EQ, EXPECT_TRUE, etc.) to verify results.
|
||||
*/
|
||||
struct TxResult
|
||||
{
|
||||
TER ter; /**< The transaction engine result code. */
|
||||
bool applied; /**< Whether the transaction was applied to the ledger. */
|
||||
std::optional<TxMeta> metadata; /**< Transaction metadata, if available. */
|
||||
std::shared_ptr<STTx const> tx; /**< Pointer to the submitted transaction. */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A lightweight transaction testing harness.
|
||||
*
|
||||
* Unlike the JTx framework which requires a full Application and RPC layer,
|
||||
* TxTest applies transactions directly to an OpenView using the transactor
|
||||
* pipeline (preflight -> preclaim -> doApply).
|
||||
*
|
||||
* This makes it suitable for:
|
||||
* - Unit testing individual transactors
|
||||
* - Testing transaction validation logic
|
||||
* - Fast, focused tests without full server infrastructure
|
||||
*
|
||||
* @code
|
||||
* TxTest env;
|
||||
* env.submit(paymentTx).expectSuccess();
|
||||
* env.submit(badTx).expectTer(tecNO_ENTRY);
|
||||
* @endcode
|
||||
*/
|
||||
class TxTest
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a TxTest environment.
|
||||
*
|
||||
* Creates a genesis ledger and an open view on top of it.
|
||||
*
|
||||
* @param features Optional set of features to enable. If not specified,
|
||||
* uses all testable amendments.
|
||||
*/
|
||||
explicit TxTest(std::optional<FeatureBitset> features = std::nullopt);
|
||||
|
||||
/**
|
||||
* @brief Check if a feature is enabled.
|
||||
* @param feature The feature to check.
|
||||
* @return True if the feature is enabled.
|
||||
*/
|
||||
bool
|
||||
isEnabled(uint256 const& feature) const;
|
||||
|
||||
/**
|
||||
* @brief Get the current rules.
|
||||
* @return The current consensus rules.
|
||||
*/
|
||||
Rules const&
|
||||
getRules() const;
|
||||
|
||||
/**
|
||||
* @brief Submit a transaction from a builder.
|
||||
*
|
||||
* Convenience overload that accepts transaction builders.
|
||||
* Automatically sets sequence and fee before submission.
|
||||
*
|
||||
* @tparam T A type derived from TransactionBuilderBase.
|
||||
* @param builder The transaction builder.
|
||||
* @param signer The account to sign with.
|
||||
* @return TxResult containing the result code, applied status, and metadata.
|
||||
*/
|
||||
template <typename T>
|
||||
requires std::
|
||||
derived_from<std::decay_t<T>, transactions::TransactionBuilderBase<std::decay_t<T>>>
|
||||
[[nodiscard]] TxResult
|
||||
submit(T&& builder, Account const& signer)
|
||||
{
|
||||
auto const& obj = builder.getSTObject();
|
||||
auto accountId = obj[sfAccount];
|
||||
// Only set sequence if not using a ticket (ticket sets sequence to 0)
|
||||
if (!obj.isFieldPresent(sfTicketSequence))
|
||||
builder.setSequence(getAccountRoot(accountId).getSequence());
|
||||
builder.setFee(XRPAmount(10));
|
||||
return submit(builder.build(signer.pk(), signer.sk()).getSTTx());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Submit a transaction to the open ledger.
|
||||
*
|
||||
* Applies the transaction through the full transactor pipeline:
|
||||
* preflight -> preclaim -> doApply -> invariant checks
|
||||
*
|
||||
* Invariant checks are automatically run after doApply. If any
|
||||
* invariant fails, the result will be tecINVARIANT_FAILED.
|
||||
*
|
||||
* @param stx The transaction to submit.
|
||||
* @return TxResult containing the result code, applied status, and metadata.
|
||||
*/
|
||||
[[nodiscard]] TxResult
|
||||
submit(std::shared_ptr<STTx const> stx);
|
||||
|
||||
/**
|
||||
* @brief Create a new account in the ledger.
|
||||
*
|
||||
* Sends a Payment from the master account to create and fund the account.
|
||||
* Closes the ledger after creation. If accountFlags is non-zero, submits
|
||||
* an AccountSet transaction and closes again.
|
||||
*
|
||||
* @param account The account to create.
|
||||
* @param xrp The initial XRP balance.
|
||||
* @param accountFlags Optional account flags to set.
|
||||
*/
|
||||
void
|
||||
createAccount(Account const& account, XRPAmount xrp, uint32_t accountFlags = asfDefaultRipple);
|
||||
|
||||
/**
|
||||
* @brief Get the account root object from the current open ledger.
|
||||
* @param id The account ID.
|
||||
* @return The AccountRoot ledger entry.
|
||||
* @throws std::runtime_error if the account does not exist.
|
||||
* @todo Once we make keylet strongly typed, we can ditch this method.
|
||||
*/
|
||||
ledger_entries::AccountRoot
|
||||
getAccountRoot(AccountID const& id) const;
|
||||
|
||||
/**
|
||||
* @brief Get the current open ledger view.
|
||||
* @return A mutable reference to the open ledger.
|
||||
*/
|
||||
OpenView&
|
||||
getOpenLedger();
|
||||
|
||||
/**
|
||||
* @brief Get the current open ledger view (const).
|
||||
* @return A const reference to the open ledger.
|
||||
*/
|
||||
OpenView const&
|
||||
getOpenLedger() const;
|
||||
|
||||
/**
|
||||
* @brief Get the closed (base) ledger view.
|
||||
* @return A const reference to the closed ledger.
|
||||
*/
|
||||
ReadView const&
|
||||
getClosedLedger() const;
|
||||
|
||||
/**
|
||||
* @brief Close the current ledger.
|
||||
*
|
||||
* Creates a new closed ledger from the current open ledger.
|
||||
* All pending transactions are re-applied in canonical order.
|
||||
*/
|
||||
void
|
||||
close();
|
||||
|
||||
/**
|
||||
* @brief Advance time without closing the ledger.
|
||||
*
|
||||
* Useful for testing time-dependent features like escrow release
|
||||
* times or offer expirations.
|
||||
*
|
||||
* @param duration The amount of time to advance.
|
||||
*/
|
||||
void
|
||||
advanceTime(NetClock::duration duration);
|
||||
|
||||
/**
|
||||
* @brief Get the current ledger close time.
|
||||
* @return The current close time.
|
||||
*/
|
||||
NetClock::time_point
|
||||
getCloseTime() const;
|
||||
|
||||
/**
|
||||
* @brief Get the balance of an IOU for an account.
|
||||
*
|
||||
* Returns the balance from the perspective of the specified account.
|
||||
* If the trust line doesn't exist, returns zero.
|
||||
*
|
||||
* @param account The account to check.
|
||||
* @param iou The IOU to check the balance for.
|
||||
* @return The balance as an STAmount.
|
||||
* @todo Once we make keylet strongly typed, we can ditch this method.
|
||||
*/
|
||||
STAmount
|
||||
getBalance(AccountID const& account, IOU const& iou) const;
|
||||
|
||||
/**
|
||||
* @brief Get the service registry.
|
||||
* @return A reference to the service registry.
|
||||
*/
|
||||
ServiceRegistry&
|
||||
getServiceRegistry()
|
||||
{
|
||||
return registry_;
|
||||
}
|
||||
|
||||
private:
|
||||
TestServiceRegistry registry_;
|
||||
std::unordered_set<uint256, beast::uhash<>> featureSet_;
|
||||
std::unique_ptr<Rules> rules_;
|
||||
std::shared_ptr<Ledger const> closedLedger_;
|
||||
std::shared_ptr<OpenView> openLedger_;
|
||||
|
||||
/** Transactions submitted to the open ledger, for canonical reordering on close. */
|
||||
std::vector<std::shared_ptr<STTx const>> pendingTxs_;
|
||||
|
||||
/** Current time (can be advanced arbitrarily for testing). */
|
||||
NetClock::time_point now_;
|
||||
};
|
||||
|
||||
} // namespace xrpl::test
|
||||
771
src/tests/libxrpl/tx/AccountSet.cpp
Normal file
771
src/tests/libxrpl/tx/AccountSet.cpp
Normal file
@@ -0,0 +1,771 @@
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/AccountRoot.h>
|
||||
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
|
||||
#include <xrpl/protocol_autogen/transactions/Payment.h>
|
||||
#include <xrpl/protocol_autogen/transactions/SetRegularKey.h>
|
||||
#include <xrpl/protocol_autogen/transactions/SignerListSet.h>
|
||||
#include <xrpl/protocol_autogen/transactions/TicketCreate.h>
|
||||
#include <xrpl/protocol_autogen/transactions/TrustSet.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/TxTest.h>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
TEST(AccountSet, NullAccountSet)
|
||||
{
|
||||
TxTest env;
|
||||
|
||||
Account const alice("alice");
|
||||
env.createAccount(alice, XRP(10), 0);
|
||||
|
||||
auto& view = env.getOpenLedger();
|
||||
|
||||
// ask for the ledger entry - account root, to check its flags
|
||||
auto sle = view.read(keylet::account(alice));
|
||||
|
||||
EXPECT_NE(sle, nullptr);
|
||||
ledger_entries::AccountRoot accountRoot(sle);
|
||||
EXPECT_EQ(accountRoot.getFlags(), 0);
|
||||
}
|
||||
|
||||
TEST(AccountSet, MostFlags)
|
||||
{
|
||||
Account const alice("alice");
|
||||
|
||||
TxTest env;
|
||||
env.createAccount(alice, XRP(10000), 0);
|
||||
|
||||
// Give alice a regular key so she can legally set and clear
|
||||
// her asfDisableMaster flag.
|
||||
Account const alie{"alie", KeyType::secp256k1};
|
||||
|
||||
env.createAccount(alie, XRP(10000), 0);
|
||||
env.close();
|
||||
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::SetRegularKeyBuilder{alice}.setRegularKey(alie), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
auto testFlags = [&alice, &alie, &env](std::initializer_list<std::uint32_t> goodFlags) {
|
||||
std::uint32_t const orig_flags = env.getAccountRoot(alice).getFlags();
|
||||
for (std::uint32_t flag{1u}; flag < std::numeric_limits<std::uint32_t>::digits; ++flag)
|
||||
{
|
||||
if (flag == asfNoFreeze)
|
||||
{
|
||||
// The asfNoFreeze flag can't be cleared. It is tested
|
||||
// elsewhere.
|
||||
continue;
|
||||
}
|
||||
if (flag == asfAuthorizedNFTokenMinter)
|
||||
{
|
||||
// The asfAuthorizedNFTokenMinter flag requires the
|
||||
// presence or absence of the sfNFTokenMinter field in
|
||||
// the transaction. It is tested elsewhere.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (flag == asfDisallowIncomingCheck || flag == asfDisallowIncomingPayChan ||
|
||||
flag == asfDisallowIncomingNFTokenOffer || flag == asfDisallowIncomingTrustline)
|
||||
{
|
||||
// These flags are part of the DisallowIncoming amendment
|
||||
// and are tested elsewhere
|
||||
continue;
|
||||
}
|
||||
if (flag == asfAllowTrustLineClawback)
|
||||
{
|
||||
// The asfAllowTrustLineClawback flag can't be cleared. It
|
||||
// is tested elsewhere.
|
||||
continue;
|
||||
}
|
||||
if (flag == asfAllowTrustLineLocking)
|
||||
{
|
||||
// These flags are part of the AllowTokenLocking amendment
|
||||
// and are tested elsewhere
|
||||
continue;
|
||||
}
|
||||
if (std::find(goodFlags.begin(), goodFlags.end(), flag) != goodFlags.end())
|
||||
{
|
||||
// Good flag
|
||||
auto aliceAccontRoot = env.getAccountRoot(alice);
|
||||
auto aliceFlags = aliceAccontRoot.getFlags();
|
||||
|
||||
EXPECT_EQ(aliceFlags & asfToLsf(flag), 0);
|
||||
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(flag), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
aliceAccontRoot = env.getAccountRoot(alice);
|
||||
aliceFlags = aliceAccontRoot.getFlags();
|
||||
EXPECT_NE(aliceFlags & asfToLsf(flag), 0);
|
||||
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setClearFlag(flag), alie).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
aliceAccontRoot = env.getAccountRoot(alice);
|
||||
aliceFlags = aliceAccontRoot.getFlags();
|
||||
EXPECT_EQ(aliceFlags & asfToLsf(flag), 0);
|
||||
|
||||
std::uint32_t const now_flags = env.getAccountRoot(alice).getFlags();
|
||||
EXPECT_EQ(now_flags, orig_flags);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Bad flag
|
||||
EXPECT_EQ(env.getAccountRoot(alice).getFlags(), orig_flags);
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(flag), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
EXPECT_EQ(env.getAccountRoot(alice).getFlags(), orig_flags);
|
||||
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setClearFlag(flag), alie).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
EXPECT_EQ(env.getAccountRoot(alice).getFlags(), orig_flags);
|
||||
}
|
||||
}
|
||||
};
|
||||
testFlags(
|
||||
{asfRequireDest,
|
||||
asfRequireAuth,
|
||||
asfDisallowXRP,
|
||||
asfGlobalFreeze,
|
||||
asfDisableMaster,
|
||||
asfDefaultRipple,
|
||||
asfDepositAuth});
|
||||
}
|
||||
|
||||
TEST(AccountSet, SetAndResetAccountTxnID)
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice("alice");
|
||||
|
||||
env.createAccount(alice, XRP(10000), 0);
|
||||
|
||||
std::uint32_t const orig_flags = env.getAccountRoot(alice).getFlags();
|
||||
|
||||
// asfAccountTxnID is special and not actually set as a flag,
|
||||
// so we check the field presence instead
|
||||
EXPECT_FALSE(env.getAccountRoot(alice).hasAccountTxnID());
|
||||
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfAccountTxnID), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
EXPECT_TRUE(env.getAccountRoot(alice).hasAccountTxnID());
|
||||
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setClearFlag(asfAccountTxnID), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
EXPECT_FALSE(env.getAccountRoot(alice).hasAccountTxnID());
|
||||
|
||||
std::uint32_t const now_flags = env.getAccountRoot(alice).getFlags();
|
||||
EXPECT_EQ(now_flags, orig_flags);
|
||||
}
|
||||
|
||||
TEST(AccountSet, SetNoFreeze)
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice("alice");
|
||||
Account const eric("eric");
|
||||
|
||||
env.createAccount(alice, XRP(10000), 0);
|
||||
env.close();
|
||||
|
||||
// Set eric as alice's regular key (eric doesn't need to be funded)
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::SetRegularKeyBuilder{alice}.setRegularKey(eric), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
// Verify alice doesn't have NoFreeze flag
|
||||
EXPECT_EQ(env.getAccountRoot(alice).getFlags() & asfToLsf(asfNoFreeze), 0);
|
||||
|
||||
// Setting NoFreeze with regular key should fail - requires master key
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfNoFreeze), eric).ter,
|
||||
tecNEED_MASTER_KEY);
|
||||
env.close();
|
||||
|
||||
// Setting NoFreeze with master key should succeed
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfNoFreeze), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
// Verify alice now has NoFreeze flag
|
||||
EXPECT_NE(env.getAccountRoot(alice).getFlags() & asfToLsf(asfNoFreeze), 0);
|
||||
|
||||
// Try to clear NoFreeze - transaction succeeds but flag remains set
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setClearFlag(asfNoFreeze), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
// Verify flag is still set (NoFreeze cannot be cleared once set)
|
||||
EXPECT_NE(env.getAccountRoot(alice).getFlags() & asfToLsf(asfNoFreeze), 0);
|
||||
}
|
||||
|
||||
TEST(AccountSet, Domain)
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice("alice");
|
||||
|
||||
env.createAccount(alice, XRP(10000), 0);
|
||||
env.close();
|
||||
|
||||
// The Domain field is represented as the hex string of the lowercase
|
||||
// ASCII of the domain. For example, the domain example.com would be
|
||||
// represented as "6578616d706c652e636f6d".
|
||||
//
|
||||
// To remove the Domain field from an account, send an AccountSet with
|
||||
// the Domain set to an empty string.
|
||||
std::string const domain = "example.com";
|
||||
|
||||
// Set domain
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setDomain(makeSlice(domain)), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
EXPECT_TRUE(env.getAccountRoot(alice).hasDomain());
|
||||
EXPECT_EQ(*env.getAccountRoot(alice).getDomain(), makeSlice(domain));
|
||||
|
||||
// Clear domain by setting empty
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setDomain(Slice{}), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
EXPECT_FALSE(env.getAccountRoot(alice).hasDomain());
|
||||
|
||||
// The upper limit on the length is 256 bytes
|
||||
// (defined as DOMAIN_BYTES_MAX in SetAccount)
|
||||
// test the edge cases: 255, 256, 257.
|
||||
std::size_t const maxLength = 256;
|
||||
for (std::size_t len = maxLength - 1; len <= maxLength + 1; ++len)
|
||||
{
|
||||
std::string domain2 = std::string(len - domain.length() - 1, 'a') + "." + domain;
|
||||
|
||||
EXPECT_EQ(domain2.length(), len);
|
||||
|
||||
if (len <= maxLength)
|
||||
{
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::AccountSetBuilder{alice}.setDomain(makeSlice(domain2)), alice)
|
||||
.ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
EXPECT_EQ(*env.getAccountRoot(alice).getDomain(), makeSlice(domain2));
|
||||
}
|
||||
else
|
||||
{
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::AccountSetBuilder{alice}.setDomain(makeSlice(domain2)), alice)
|
||||
.ter,
|
||||
telBAD_DOMAIN);
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AccountSet, MessageKey)
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice("alice");
|
||||
|
||||
env.createAccount(alice, XRP(10000), 0);
|
||||
env.close();
|
||||
|
||||
// Generate a random ed25519 key pair for the message key
|
||||
auto const rkp = randomKeyPair(KeyType::ed25519);
|
||||
|
||||
// Set the message key
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setMessageKey(rkp.first.slice()), alice)
|
||||
.ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
EXPECT_TRUE(env.getAccountRoot(alice).hasMessageKey());
|
||||
EXPECT_EQ(*env.getAccountRoot(alice).getMessageKey(), rkp.first.slice());
|
||||
|
||||
// Clear the message key by setting to empty
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setMessageKey(Slice{}), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
EXPECT_FALSE(env.getAccountRoot(alice).hasMessageKey());
|
||||
|
||||
// Try to set an invalid public key - should fail
|
||||
using namespace std::string_literals;
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::AccountSetBuilder{alice}.setMessageKey(
|
||||
makeSlice("NOT_REALLY_A_PUBKEY"s)),
|
||||
alice)
|
||||
.ter,
|
||||
telBAD_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
TEST(AccountSet, WalletID)
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice("alice");
|
||||
|
||||
env.createAccount(alice, XRP(10000), 0);
|
||||
env.close();
|
||||
|
||||
std::string_view locator = "9633EC8AF54F16B5286DB1D7B519EF49EEFC050C0C8AC4384F1D88ACD1BFDF05";
|
||||
uint256 locatorHash{};
|
||||
EXPECT_TRUE(locatorHash.parseHex(locator));
|
||||
|
||||
// Set the wallet locator
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setWalletLocator(locatorHash), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
EXPECT_TRUE(env.getAccountRoot(alice).hasWalletLocator());
|
||||
EXPECT_EQ(*env.getAccountRoot(alice).getWalletLocator(), locatorHash);
|
||||
|
||||
// Clear the wallet locator by setting to zero
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setWalletLocator(beast::zero), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
EXPECT_FALSE(env.getAccountRoot(alice).hasWalletLocator());
|
||||
}
|
||||
|
||||
TEST(AccountSet, EmailHash)
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice("alice");
|
||||
|
||||
env.createAccount(alice, XRP(10000), 0);
|
||||
env.close();
|
||||
|
||||
std::string_view mh = "5F31A79367DC3137FADA860C05742EE6";
|
||||
uint128 emailHash{};
|
||||
EXPECT_TRUE(emailHash.parseHex(mh));
|
||||
|
||||
// Set the email hash
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setEmailHash(emailHash), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
EXPECT_TRUE(env.getAccountRoot(alice).hasEmailHash());
|
||||
EXPECT_EQ(*env.getAccountRoot(alice).getEmailHash(), emailHash);
|
||||
|
||||
// Clear the email hash by setting to zero
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setEmailHash(beast::zero), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
EXPECT_FALSE(env.getAccountRoot(alice).hasEmailHash());
|
||||
}
|
||||
|
||||
TEST(AccountSet, TransferRate)
|
||||
{
|
||||
struct TestCase
|
||||
{
|
||||
double set;
|
||||
TER code;
|
||||
double get;
|
||||
};
|
||||
|
||||
// Test data: {rate to set, expected TER, expected stored rate}
|
||||
std::vector<TestCase> const testData = {
|
||||
{1.0, tesSUCCESS, 1.0},
|
||||
{1.1, tesSUCCESS, 1.1},
|
||||
{2.0, tesSUCCESS, 2.0},
|
||||
{2.1, temBAD_TRANSFER_RATE, 2.0}, // > 2.0 is invalid
|
||||
{0.0, tesSUCCESS, 1.0}, // 0 clears the rate (default = 1.0)
|
||||
{2.0, tesSUCCESS, 2.0},
|
||||
{0.9, temBAD_TRANSFER_RATE, 2.0}, // < 1.0 is invalid
|
||||
};
|
||||
|
||||
TxTest env;
|
||||
Account const alice("alice");
|
||||
|
||||
env.createAccount(alice, XRP(10000), 0);
|
||||
env.close();
|
||||
|
||||
for (auto const& r : testData)
|
||||
{
|
||||
auto const rateValue = static_cast<std::uint32_t>(QUALITY_ONE * r.set);
|
||||
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setTransferRate(rateValue), alice)
|
||||
.ter,
|
||||
r.code);
|
||||
env.close();
|
||||
|
||||
// If the field is not present, expect the default value (1.0)
|
||||
if (!env.getAccountRoot(alice).hasTransferRate())
|
||||
{
|
||||
EXPECT_EQ(r.get, 1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
EXPECT_EQ(
|
||||
*env.getAccountRoot(alice).getTransferRate(),
|
||||
static_cast<std::uint32_t>(r.get * QUALITY_ONE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AccountSet, BadInputs)
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice("alice");
|
||||
|
||||
env.createAccount(alice, XRP(10000), 0);
|
||||
env.close();
|
||||
|
||||
// Setting and clearing the same flag is invalid
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::AccountSetBuilder{alice}
|
||||
.setSetFlag(asfDisallowXRP)
|
||||
.setClearFlag(asfDisallowXRP),
|
||||
alice)
|
||||
.ter,
|
||||
temINVALID_FLAG);
|
||||
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::AccountSetBuilder{alice}
|
||||
.setSetFlag(asfRequireAuth)
|
||||
.setClearFlag(asfRequireAuth),
|
||||
alice)
|
||||
.ter,
|
||||
temINVALID_FLAG);
|
||||
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::AccountSetBuilder{alice}
|
||||
.setSetFlag(asfRequireDest)
|
||||
.setClearFlag(asfRequireDest),
|
||||
alice)
|
||||
.ter,
|
||||
temINVALID_FLAG);
|
||||
|
||||
// Setting asf flag while also using corresponding tf flag is invalid
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::AccountSetBuilder{alice}
|
||||
.setSetFlag(asfDisallowXRP)
|
||||
.setFlags(tfAllowXRP),
|
||||
alice)
|
||||
.ter,
|
||||
temINVALID_FLAG);
|
||||
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::AccountSetBuilder{alice}
|
||||
.setSetFlag(asfRequireAuth)
|
||||
.setFlags(tfOptionalAuth),
|
||||
alice)
|
||||
.ter,
|
||||
temINVALID_FLAG);
|
||||
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::AccountSetBuilder{alice}
|
||||
.setSetFlag(asfRequireDest)
|
||||
.setFlags(tfOptionalDestTag),
|
||||
alice)
|
||||
.ter,
|
||||
temINVALID_FLAG);
|
||||
|
||||
// Using invalid flags (mask) is invalid
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::AccountSetBuilder{alice}
|
||||
.setSetFlag(asfRequireDest)
|
||||
.setFlags(tfAccountSetMask),
|
||||
alice)
|
||||
.ter,
|
||||
temINVALID_FLAG);
|
||||
|
||||
// Disabling master key without an alternative key is invalid
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfDisableMaster), alice).ter,
|
||||
tecNO_ALTERNATIVE_KEY);
|
||||
}
|
||||
|
||||
TEST(AccountSet, RequireAuthWithDir)
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
|
||||
env.createAccount(alice, XRP(10000), 0);
|
||||
env.close();
|
||||
|
||||
// alice should have an empty directory
|
||||
EXPECT_TRUE(dirIsEmpty(env.getClosedLedger(), keylet::ownerDir(alice.id())));
|
||||
|
||||
// Give alice a signer list, then there will be stuff in the directory
|
||||
// Build the SignerEntries array
|
||||
STArray signerEntries(1);
|
||||
{
|
||||
signerEntries.push_back(STObject::makeInnerObject(sfSignerEntry));
|
||||
STObject& entry = signerEntries.back();
|
||||
entry[sfAccount] = bob.id();
|
||||
entry[sfSignerWeight] = std::uint16_t{1};
|
||||
}
|
||||
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::SignerListSetBuilder{alice, 1}.setSignerEntries(signerEntries), alice)
|
||||
.ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
EXPECT_FALSE(dirIsEmpty(env.getClosedLedger(), keylet::ownerDir(alice.id())));
|
||||
|
||||
// Setting RequireAuth should fail because alice has owner objects
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfRequireAuth), alice).ter,
|
||||
tecOWNERS);
|
||||
|
||||
// Remove the signer list (quorum = 0, no entries)
|
||||
EXPECT_EQ(env.submit(transactions::SignerListSetBuilder{alice, 0}, alice).ter, tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
EXPECT_TRUE(dirIsEmpty(env.getClosedLedger(), keylet::ownerDir(alice.id())));
|
||||
|
||||
// Now setting RequireAuth should succeed
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfRequireAuth), alice).ter,
|
||||
tesSUCCESS);
|
||||
}
|
||||
|
||||
TEST(AccountSet, Ticket)
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice("alice");
|
||||
|
||||
env.createAccount(alice, XRP(10000), 0);
|
||||
env.close();
|
||||
|
||||
// Get alice's current sequence - the ticket will be created at seq + 1
|
||||
std::uint32_t const aliceSeqBefore = env.getAccountRoot(alice.id()).getSequence();
|
||||
std::uint32_t const ticketSeq = aliceSeqBefore + 1;
|
||||
|
||||
// Create a ticket
|
||||
EXPECT_EQ(env.submit(transactions::TicketCreateBuilder{alice, 1}, alice).ter, tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
// Verify alice has 1 owner object (the ticket)
|
||||
EXPECT_EQ(env.getAccountRoot(alice.id()).getOwnerCount(), 1u);
|
||||
// Verify ticket exists
|
||||
EXPECT_TRUE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
|
||||
|
||||
// Try using a ticket that alice doesn't have
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq + 1), alice)
|
||||
.ter,
|
||||
terPRE_TICKET);
|
||||
env.close();
|
||||
|
||||
// Verify ticket still exists
|
||||
EXPECT_TRUE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
|
||||
|
||||
// Get alice's sequence before using the ticket
|
||||
std::uint32_t const aliceSeq = env.getAccountRoot(alice.id()).getSequence();
|
||||
|
||||
// Actually use alice's ticket (noop AccountSet)
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq), alice).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
// Verify ticket is consumed (no owner objects)
|
||||
EXPECT_EQ(env.getAccountRoot(alice.id()).getOwnerCount(), 0u);
|
||||
EXPECT_FALSE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
|
||||
|
||||
// Verify alice's sequence did NOT advance (ticket use doesn't increment seq)
|
||||
EXPECT_EQ(env.getAccountRoot(alice.id()).getSequence(), aliceSeq);
|
||||
|
||||
// Try re-using a ticket that alice already used
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq), alice).ter,
|
||||
tefNO_TICKET);
|
||||
}
|
||||
|
||||
TEST(AccountSet, BadSigningKey)
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice("alice");
|
||||
|
||||
env.createAccount(alice, XRP(10000), 0);
|
||||
env.close();
|
||||
|
||||
// Build a valid transaction first, then corrupt the signing key
|
||||
auto stx = transactions::AccountSetBuilder{alice}
|
||||
.setSequence(env.getAccountRoot(alice.id()).getSequence())
|
||||
.setFee(XRPAmount{10})
|
||||
.build(alice.pk(), alice.sk())
|
||||
.getSTTx();
|
||||
|
||||
// Create a copy with a bad signing key
|
||||
STObject obj = *stx;
|
||||
obj.setFieldVL(sfSigningPubKey, makeSlice(std::string("badkey")));
|
||||
|
||||
auto result = env.submit(std::make_shared<STTx>(std::move(obj)));
|
||||
EXPECT_EQ(result.ter, temBAD_SIGNATURE);
|
||||
EXPECT_FALSE(result.applied);
|
||||
}
|
||||
|
||||
TEST(AccountSet, Gateway)
|
||||
{
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const gw("gateway");
|
||||
IOU const USD("USD", gw);
|
||||
|
||||
// Test gateway with a variety of allowed transfer rates
|
||||
for (double transferRate = 1.0; transferRate <= 2.0; transferRate += 0.03125)
|
||||
{
|
||||
TxTest env;
|
||||
|
||||
env.createAccount(gw, XRP(10000));
|
||||
env.createAccount(alice, XRP(10000));
|
||||
env.createAccount(bob, XRP(10000));
|
||||
env.close();
|
||||
|
||||
// Set up trust lines: alice and bob trust gw for USD
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::TrustSetBuilder{alice}.setLimitAmount(USD.amount(10)), alice)
|
||||
.ter,
|
||||
tesSUCCESS);
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::TrustSetBuilder{bob}.setLimitAmount(USD.amount(10)), bob).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
// Set transfer rate on the gateway
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::AccountSetBuilder{gw}.setTransferRate(
|
||||
static_cast<std::uint32_t>(transferRate * QUALITY_ONE)),
|
||||
gw)
|
||||
.ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
// Calculate the amount with transfer rate applied
|
||||
auto const amount = USD.amount(1);
|
||||
Rate const rate(static_cast<std::uint32_t>(transferRate * QUALITY_ONE));
|
||||
auto const amountWithRate = multiply(amount, rate);
|
||||
|
||||
// Gateway pays alice 10 USD
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::PaymentBuilder{gw, alice, USD.amount(10)}, gw).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
// Alice pays bob 1 USD (with sendmax to cover transfer fee)
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::PaymentBuilder{alice, bob, USD.amount(1)}.setSendMax(
|
||||
USD.amount(10)),
|
||||
alice)
|
||||
.ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
// Check balances
|
||||
EXPECT_EQ(env.getBalance(alice.id(), USD), USD.amount(10) - amountWithRate);
|
||||
EXPECT_EQ(env.getBalance(bob.id(), USD), USD.amount(1));
|
||||
}
|
||||
|
||||
// Test out-of-bounds legacy transfer rates (4.0 and 4.294967295)
|
||||
// These require direct ledger modification since the transactor blocks them
|
||||
for (double transferRate : {4.0, 4.294967295})
|
||||
{
|
||||
TxTest env;
|
||||
env.createAccount(gw, XRP(10000));
|
||||
env.createAccount(alice, XRP(10000));
|
||||
env.createAccount(bob, XRP(10000));
|
||||
env.close();
|
||||
|
||||
// Set up trust lines
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::TrustSetBuilder{alice}.setLimitAmount(USD.amount(10)), alice)
|
||||
.ter,
|
||||
tesSUCCESS);
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::TrustSetBuilder{bob}.setLimitAmount(USD.amount(10)), bob).ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
// Set an acceptable transfer rate first (we'll hack it later)
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::AccountSetBuilder{gw}.setTransferRate(
|
||||
static_cast<std::uint32_t>(2.0 * QUALITY_ONE)),
|
||||
gw)
|
||||
.ter,
|
||||
tesSUCCESS);
|
||||
env.close();
|
||||
|
||||
// Directly modify the ledger to set an out-of-bounds transfer rate
|
||||
// This bypasses the transactor's validation
|
||||
auto& view = env.getOpenLedger();
|
||||
auto slePtr = view.read(keylet::account(gw.id()));
|
||||
ASSERT_NE(slePtr, nullptr);
|
||||
auto sleCopy = std::make_shared<SLE>(*slePtr);
|
||||
(*sleCopy)[sfTransferRate] = static_cast<std::uint32_t>(transferRate * QUALITY_ONE);
|
||||
view.rawReplace(sleCopy);
|
||||
|
||||
// Calculate the amount with the legacy transfer rate
|
||||
auto const amount = USD.amount(1);
|
||||
auto const amountWithRate =
|
||||
multiply(amount, Rate(static_cast<std::uint32_t>(transferRate * QUALITY_ONE)));
|
||||
|
||||
// Gateway pays alice 10 USD
|
||||
EXPECT_EQ(
|
||||
env.submit(transactions::PaymentBuilder{gw, alice, USD.amount(10)}, gw).ter,
|
||||
tesSUCCESS);
|
||||
|
||||
// Alice pays bob 1 USD
|
||||
EXPECT_EQ(
|
||||
env.submit(
|
||||
transactions::PaymentBuilder{alice, bob, amount}.setSendMax(USD.amount(10)),
|
||||
alice)
|
||||
.ter,
|
||||
tesSUCCESS);
|
||||
|
||||
// Check balances
|
||||
EXPECT_EQ(env.getBalance(alice.id(), USD), USD.amount(10) - amountWithRate);
|
||||
EXPECT_EQ(env.getBalance(bob.id(), USD), amount);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
8
src/tests/libxrpl/tx/main.cpp
Normal file
8
src/tests/libxrpl/tx/main.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
int
|
||||
main(int argc, char** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
Reference in New Issue
Block a user