Compare commits

..

8 Commits

Author SHA1 Message Date
Ayaz Salikhov
2432e46114 Merge branch 'develop' into mathbunnyru/pure_cpp_23 2026-07-01 09:14:30 +01:00
Vito Tumas
ecf7f805c9 feat: Introduce lending 1.1 amendment and add MemoData field to VaultDelete transaction (#6324) 2026-06-30 23:51:41 +00:00
Ayaz Salikhov
909836ffc2 Better comment 2026-07-01 00:06:42 +01:00
Ayaz Salikhov
2af67d8f6f Hardcode 23 2026-07-01 00:05:33 +01:00
Ayaz Salikhov
9171fef02c Use user.package:cppstd_version 2026-07-01 00:04:12 +01:00
Ayaz Salikhov
273c1862ac Fix style 2026-06-30 23:56:22 +01:00
Ayaz Salikhov
49e9c2acb0 Use conf section 2026-06-30 23:51:47 +01:00
Ayaz Salikhov
cabafec58d build: Don't reuse binaries between different C++ versions 2026-06-30 23:42:40 +01:00
15 changed files with 176 additions and 553 deletions

View File

@@ -160,7 +160,6 @@ test.peerfinder > xrpl.protocol
test.protocol > test.jtx
test.protocol > test.unit_test
test.protocol > xrpl.basics
test.protocol > xrpld.core
test.protocol > xrpl.json
test.protocol > xrpl.protocol
test.resource > test.unit_test

View File

@@ -10,16 +10,16 @@
os={{ os }}
arch={{ arch }}
build_type=Debug
compiler={{compiler}}
compiler={{ compiler }}
compiler.version={{ compiler_version }}
compiler.cppstd=23
{% if os == "Windows" %}
compiler.runtime=static
{% else %}
compiler.libcxx={{detect_api.detect_libcxx(compiler, version, compiler_exe)}}
compiler.libcxx={{ detect_api.detect_libcxx(compiler, version, compiler_exe) }}
{% endif %}
[conf]
{% if compiler == "gcc" and compiler_version < 13 %}
tools.build:cxxflags+=['-Wno-restrict']
{% endif %}
{# By default, conan tries compatibility mode to reuse binaries built with different cppstd versions #}
user.package:cppstd_version=23
tools.info.package_id:confs+=["user.package:cppstd_version"]

View File

@@ -87,15 +87,15 @@ include(default)
{% endif %}
[conf]
tools.build:defines+={{defines}}
tools.build:cxxflags+={{sanitizer_compiler_flags}}
tools.build:sharedlinkflags+={{sanitizer_linker_flags}}
tools.build:exelinkflags+={{sanitizer_linker_flags}}
tools.build:defines+={{ defines }}
tools.build:cxxflags+={{ sanitizer_compiler_flags }}
tools.build:sharedlinkflags+={{ sanitizer_linker_flags }}
tools.build:exelinkflags+={{ sanitizer_linker_flags }}
tools.info.package_id:confs+=["tools.build:cxxflags", "tools.build:exelinkflags", "tools.build:sharedlinkflags", "tools.build:defines"]
# &: means "apply only to the consumer/root package"
&:tools.cmake.cmaketoolchain:extra_variables={"SANITIZERS": "{{sanitizers}}", "SANITIZERS_COMPILER_FLAGS": "{{sanitizer_compiler_flags | join(' ')}}", "SANITIZERS_LINKER_FLAGS": "{{sanitizer_linker_flags | join(' ')}}"}
&:tools.cmake.cmaketoolchain:extra_variables={"SANITIZERS": "{{ sanitizers }}", "SANITIZERS_COMPILER_FLAGS": "{{ sanitizer_compiler_flags | join(' ') }}", "SANITIZERS_LINKER_FLAGS": "{{ sanitizer_linker_flags | join(' ') }}"}
[options]
{% if enable_asan %}

View File

@@ -14,6 +14,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(ConfidentialTransfer, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_3_0, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo)

View File

@@ -889,6 +889,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete,
MustDeleteAcct | DestroyMptIssuance | MustModifyVault,
({
{sfVaultID, SoeRequired},
{sfMemoData, SoeOptional},
}))
/** This transaction trades assets for shares with a vault. */

View File

@@ -57,6 +57,32 @@ public:
{
return this->tx_->at(sfVaultID);
}
/**
* @brief Get sfMemoData (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getMemoData() const
{
if (hasMemoData())
{
return this->tx_->at(sfMemoData);
}
return std::nullopt;
}
/**
* @brief Check if sfMemoData is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasMemoData() const
{
return this->tx_->isFieldPresent(sfMemoData);
}
};
/**
@@ -112,6 +138,17 @@ public:
return *this;
}
/**
* @brief Set sfMemoData (SoeOptional)
* @return Reference to this builder for method chaining.
*/
VaultDeleteBuilder&
setMemoData(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfMemoData] = value;
return *this;
}
/**
* @brief Build and return the VaultDelete wrapper.
* @param publicKey The public key for signing.

View File

@@ -7,8 +7,10 @@
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
@@ -28,6 +30,12 @@ VaultDelete::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfMemoData) && !ctx.rules.enabled(featureLendingProtocolV1_1))
return temDISABLED;
if (!validDataLength(ctx.tx[~sfMemoData], kMaxDataPayloadLength))
return temMALFORMED;
return tesSUCCESS;
}

View File

@@ -939,46 +939,6 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
BEAST_EXPECT(!reply->has_error());
BEAST_EXPECT(server.msgHandler.processProofPathResponse(reply));
{
// bad reply: invalid hash/key sizes
{
// reply with undersized ledgerhash (31 bytes)
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_ledgerhash(std::string(31, '\x01'));
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
}
{
// reply with oversized ledgerhash (33 bytes)
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_ledgerhash(std::string(33, '\x01'));
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
}
{
// reply with empty ledgerhash
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_ledgerhash(std::string());
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
}
{
// reply with undersized key (31 bytes)
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_key(std::string(31, '\x01'));
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
}
{
// reply with oversized key (33 bytes)
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_key(std::string(33, '\x01'));
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
}
{
// reply with empty key
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_key(std::string());
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
}
}
{
// bad reply
// bad header
@@ -1028,28 +988,6 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
BEAST_EXPECT(!reply->has_error());
BEAST_EXPECT(server.msgHandler.processReplayDeltaResponse(reply));
{
// bad reply: invalid hash sizes
{
// reply with undersized ledgerhash (31 bytes)
auto bad = std::make_shared<protocol::TMReplayDeltaResponse>(*reply);
bad->set_ledgerhash(std::string(31, '\x01'));
BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad));
}
{
// reply with oversized ledgerhash (33 bytes)
auto bad = std::make_shared<protocol::TMReplayDeltaResponse>(*reply);
bad->set_ledgerhash(std::string(33, '\x01'));
BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad));
}
{
// reply with empty ledgerhash
auto bad = std::make_shared<protocol::TMReplayDeltaResponse>(*reply);
bad->set_ledgerhash(std::string());
BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad));
}
}
{
// bad reply
// bad header

View File

@@ -7511,6 +7511,74 @@ class Vault_test : public beast::unit_test::Suite
}
}
void
testVaultDeleteMemoData()
{
using namespace test::jtx;
Env env{*this};
Account const owner{"owner"};
env.fund(XRP(1'000'000), owner);
env.close();
Vault const vault{env};
auto const keylet = keylet::vault(owner.id(), 1);
auto delTx = vault.del({.owner = owner, .id = keylet.key});
// Test VaultDelete with featureLendingProtocolV1_1 disabled
// Transaction fails if the data field is provided
{
testcase("VaultDelete memo data featureLendingProtocolV1_1 disabled");
env.disableFeature(featureLendingProtocolV1_1);
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
env(delTx, Ter(temDISABLED));
env.enableFeature(featureLendingProtocolV1_1);
env.close();
}
// Transaction fails if the data field is too large
{
testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data too large");
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength + 1, 'A'));
env(delTx, Ter(temMALFORMED));
env.close();
}
// Transaction fails if the data field is set, but is empty
{
testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data empty");
delTx[sfMemoData] = strHex(std::string(0, 'A'));
env(delTx, Ter(temMALFORMED));
env.close();
}
{
testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled no vault");
auto const keylet = keylet::vault(owner.id(), env.seq(owner));
// Recreate the transaction as the vault keylet changed
auto delTx = vault.del({.owner = owner, .id = keylet.key});
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
env(delTx, Ter(tecNO_ENTRY));
env.close();
}
{
testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data valid");
PrettyAsset const xrpAsset = xrpIssue();
auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
env(tx, Ter(tesSUCCESS));
env.close();
// Recreate the transaction as the vault keylet changed
auto delTx = vault.del({.owner = owner, .id = keylet.key});
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
env(delTx, Ter(tesSUCCESS));
env.close();
}
}
void
testVaultDepositFreeze()
{
@@ -8082,6 +8150,7 @@ class Vault_test : public beast::unit_test::Suite
runTests();
env.disableFeature(fixCleanup3_3_0);
runTests();
env.enableFeature(fixCleanup3_3_0);
}
@@ -8115,6 +8184,7 @@ public:
testVaultClawbackAssets();
testVaultEscrowedMPT();
testAssetsMaximum();
testVaultDeleteMemoData();
testBug6LimitBypassWithShares();
testRemoveEmptyHoldingLockedAmount();
testRemoveEmptyHoldingConfidentialBalances();

View File

@@ -117,24 +117,6 @@ struct base_uint_test : beast::unit_test::Suite
}
}
#ifdef NDEBUG
void
testFromRawSizeMismatch()
{
testcase("base_uint: fromRaw size mismatch");
// Container larger than the base_uint (16 bytes vs 12 bytes for
// test96). Only the first 12 bytes are copied; the extra bytes are
// ignored.
{
Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
test96 const result = test96::fromRaw(tooBig);
auto const resultText = to_string(result);
BEAST_EXPECTS(resultText == "0102030405060708090A0B0C", resultText);
}
}
#endif
void
run() override
{
@@ -143,10 +125,6 @@ struct base_uint_test : beast::unit_test::Suite
static_assert(!std::is_constructible_v<test96, std::complex<double>>);
static_assert(!std::is_assignable_v<test96&, std::complex<double>>);
#ifdef NDEBUG
testFromRawSizeMismatch();
#endif
testComparisons();
// used to verify set insertion (hashing required)
@@ -216,19 +194,6 @@ struct base_uint_test : beast::unit_test::Suite
BEAST_EXPECT(d == 0);
}
{
// There are several ways to create a zero. beast::kZero is tested above. Test some
// others.
test96 const z1;
BEAST_EXPECTS(z1 == z, to_string(z1));
test96 const z2{};
BEAST_EXPECTS(z2 == z, to_string(z2));
test96 const z3{0u};
BEAST_EXPECTS(z3 == z, to_string(z3));
}
test96 n{z};
n++;
BEAST_EXPECT(n == test96(1));

View File

@@ -1,12 +1,10 @@
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <functional>
#include <map>
@@ -865,101 +863,6 @@ public:
//--------------------------------------------------------------------------
void
testIssueFromJson()
{
testcase("issueFromJson");
// Valid XRP — no issuer field
{
json::Value jv;
jv[jss::currency] = "XRP";
auto const issue = issueFromJson(jv);
BEAST_EXPECT(isXRP(issue));
}
// Valid IOU — legitimate issuer
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
auto const issue = issueFromJson(jv);
BEAST_EXPECT(!isXRP(issue));
BEAST_EXPECT(issue.account != noAccount());
}
// noAccount() is the MPT sentinel in binary serialization - must be
// rejected
try
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = to_string(noAccount());
issueFromJson(jv);
fail("noAccount() accepted as IOU issuer");
}
catch (...)
{
pass();
}
// xrpAccount() is the XRP sentinel (all zeros) - must be rejected
// as IOU issuer
try
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = to_string(xrpAccount());
issueFromJson(jv);
fail("xrpAccount() accepted as IOU issuer");
}
catch (...)
{
pass();
}
// Invalid base58 — must be rejected
try
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = "not_a_valid_address";
issueFromJson(jv);
fail("invalid base58 accepted as IOU issuer");
}
catch (...)
{
pass();
}
// Non-XRP currency with no issuer field — must be rejected
try
{
json::Value jv;
jv[jss::currency] = "USD";
issueFromJson(jv);
fail("missing issuer accepted");
}
catch (...)
{
pass();
}
// XRP with an issuer field — must be rejected
try
{
json::Value jv;
jv[jss::currency] = "XRP";
jv[jss::issuer] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
issueFromJson(jv);
fail("XRP with issuer accepted");
}
catch (...)
{
pass();
}
}
void
run() override
{
@@ -994,9 +897,6 @@ public:
// ---
testIssueDomainSets();
testIssueDomainMaps();
// ---
testIssueFromJson();
}
};

View File

@@ -1,24 +1,15 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h> // IWYU pragma: keep
#include <test/jtx/envconfig.h>
#include <xrpld/core/Config.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STIssue.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <memory>
namespace xrpl::test {
@@ -146,143 +137,12 @@ public:
"000000000000000000000000000000000000000000000002");
}
void
testNoAccountIssuerRpc()
{
testcase("noAccount issuer rejected via RPC sign");
using namespace jtx;
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->loadFromString("[signing_support]\ntrue");
return cfg;
})};
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
json::Value txJson;
txJson[jss::TransactionType] = "AMMDelete";
txJson[jss::Account] = alice.human();
txJson[jss::Asset][jss::currency] = "USD";
txJson[jss::Asset][jss::issuer] = to_string(noAccount());
txJson[jss::Asset2][jss::currency] = "XRP";
json::Value req;
req[jss::tx_json] = txJson;
req[jss::secret] = alice.name();
auto const result = env.rpc("json", "sign", to_string(req))[jss::result];
BEAST_EXPECT(result[jss::status] == "error");
BEAST_EXPECT(result.isMember(jss::error));
}
void
testNoAccountIssuer()
{
testcase("noAccount issuer rejection");
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = to_string(noAccount());
try
{
issueFromJson(sfAsset, jv);
fail("issueFromJson accepted noAccount() as IOU issuer");
}
catch (...)
{
pass();
}
}
{
Serializer s;
s.addBitString(toCurrency("USD"));
s.addBitString(noAccount());
SerialIter iter(s.slice());
try
{
STIssue const stissue(iter, sfAsset);
fail(
"STIssue deserialization of [USD][noAccount()] should "
"throw");
}
catch (...)
{
pass();
}
}
}
void
testXrpAccountIssuerRpc()
{
testcase("xrpAccount issuer rejected via RPC sign");
using namespace jtx;
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->loadFromString("[signing_support]\ntrue");
return cfg;
})};
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
json::Value txJson;
txJson[jss::TransactionType] = "AMMDelete";
txJson[jss::Account] = alice.human();
txJson[jss::Asset][jss::currency] = "USD";
txJson[jss::Asset][jss::issuer] = to_string(xrpAccount());
txJson[jss::Asset2][jss::currency] = "XRP";
json::Value req;
req[jss::tx_json] = txJson;
req[jss::secret] = alice.name();
auto const result = env.rpc("json", "sign", to_string(req))[jss::result];
BEAST_EXPECT(result[jss::status] == "error");
BEAST_EXPECT(result.isMember(jss::error));
}
void
testXrpAccountIssuer()
{
testcase("xrpAccount issuer rejection");
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = to_string(xrpAccount());
try
{
issueFromJson(sfAsset, jv);
fail("issueFromJson accepted xrpAccount() as IOU issuer");
}
catch (...)
{
pass();
}
}
}
void
run() override
{
// compliments other unit tests to ensure complete coverage
testConstructor();
testCompare();
testNoAccountIssuerRpc();
testNoAccountIssuer();
testXrpAccountIssuerRpc();
testXrpAccountIssuer();
}
};

View File

@@ -23,7 +23,6 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/nft.h>
@@ -33,7 +32,6 @@
#include <cstdint>
#include <iterator>
#include <optional>
#include <ranges>
#include <vector>
namespace xrpl::test {
@@ -1352,86 +1350,6 @@ public:
}
}
void
testAccountObjectDoesntShowCancelledOffers()
{
testcase("AccountObjectDoesntShowCancelledOffers");
using namespace jtx;
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
auto const eur = bob["EUR"];
env.fund(XRP(10000), alice, bob);
env.close();
auto const rpcAccountObjects = [&](std::optional<uint32_t> limit = std::nullopt) {
json::Value params;
params[jss::account] = alice.human();
if (limit.has_value())
{
params[jss::limit] = *limit;
}
return env.rpc("json", "account_objects", to_string(params));
};
auto const numEntries = 33;
std::vector<uint32_t> seqs;
seqs.reserve(numEntries);
for ([[maybe_unused]] auto _ : std::ranges::iota_view{0, numEntries})
{
json::Value params;
params[jss::secret] = toBase58(generateSeed("alice"));
params[jss::tx_json] = offer(alice, eur(1), XRP(2));
auto const res = env.rpc("json", "submit", to_string(params))[jss::result];
BEAST_EXPECT(res[jss::engine_result].asString() == "tesSUCCESS");
seqs.push_back(env.seq(alice));
}
auto res = rpcAccountObjects();
BEAST_EXPECT(res[jss::result][jss::account_objects].size() == numEntries);
BEAST_EXPECT(not res[jss::result].isMember(jss::limit));
BEAST_EXPECT(not res[jss::result].isMember(jss::marker));
for (auto const s : std::views::all(seqs) | std::views::take(numEntries - 1))
{
json::Value params;
params[jss::secret] = toBase58(generateSeed("alice"));
params[jss::tx_json] = offerCancel(alice, s - 1);
auto const res = env.rpc("json", "submit", to_string(params))[jss::result];
BEAST_EXPECT(res[jss::engine_result].asString() == "tesSUCCESS");
}
res = rpcAccountObjects();
BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 1);
BEAST_EXPECT(not res[jss::result].isMember(jss::limit));
BEAST_EXPECT(not res[jss::result].isMember(jss::marker));
{
json::Value params;
params[jss::secret] = toBase58(generateSeed("alice"));
json::Value txJson;
txJson[jss::TransactionType] = jss::NFTokenMint;
txJson[jss::Account] = to_string(alice.id());
txJson["NFTokenTaxon"] = 1;
params[jss::tx_json] = txJson;
auto const res = env.rpc("json", "submit", to_string(params))[jss::result];
BEAST_EXPECT(res[jss::engine_result].asString() == "tesSUCCESS");
}
env.close();
res = rpcAccountObjects();
BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 2);
BEAST_EXPECT(not res[jss::result].isMember(jss::limit));
BEAST_EXPECT(not res[jss::result].isMember(jss::marker));
res = rpcAccountObjects(1);
BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 1);
BEAST_EXPECT(res[jss::result][jss::limit].asUInt() == 1);
BEAST_EXPECT(res[jss::result].isMember(jss::marker));
}
void
run() override
{
@@ -1442,7 +1360,6 @@ public:
testNFTsMarker();
testAccountNFTs();
testAccountObjectMarker();
testAccountObjectDoesntShowCancelledOffers();
}
};

View File

@@ -2,7 +2,6 @@
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/multisign.h>
#include <test/jtx/noop.h>
#include <test/jtx/pay.h>
#include <test/jtx/seq.h>
@@ -882,125 +881,6 @@ class Transaction_test : public beast::unit_test::Suite
}
}
void
testSignForNetworkIDValidation()
{
testcase("SignFor NetworkID validation");
using namespace test::jtx;
Account const owner{"owner"};
Account const signer{"signer"};
auto makeConfig = [](std::uint32_t networkID) {
return envconfig([networkID](std::unique_ptr<Config> cfg) {
cfg->networkId = networkID;
return cfg;
});
};
auto setupEnv = [&](Env& env) {
env.fund(XRP(10'000), owner, signer);
env.close();
env(signers(owner, 1, {{signer, 1}}));
env.close();
};
auto makeTx = [&](Env& env) {
json::Value tx;
tx[jss::TransactionType] = jss::AccountSet;
tx[jss::Account] = owner.human();
tx[jss::Sequence] = env.seq(owner);
tx[jss::Fee] = "100";
tx[jss::SigningPubKey] = "";
return tx;
};
auto signFor = [&](Env& env, json::Value const& tx) {
json::Value signReq;
signReq[jss::tx_json] = tx;
signReq[jss::account] = signer.human();
signReq[jss::secret] = signer.name();
return env.rpc("json", "sign_for", to_string(signReq))[jss::result];
};
// Test case: NetworkID < 1024 - field is not required
{
Env env{*this, makeConfig(500)};
setupEnv(env);
auto tx = makeTx(env);
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::status] == "success");
BEAST_EXPECT(!result[jss::tx_json].isMember(jss::NetworkID));
}
// Test case: NetworkID > 1024 - missing NetworkID field
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Missing field 'tx_json.NetworkID'.");
}
// Test case: NetworkID > 1024 - NetworkID field is not a number
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
tx[jss::NetworkID] = "not_a_number";
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'.");
}
// Test case: NetworkID > 1024 - NetworkID field is not integral
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
tx[jss::NetworkID] = 2040.1;
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'.");
}
// Test case: NetworkID > 1024 - NetworkID field is different from
// actual NetworkID
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
tx[jss::NetworkID] = 9999;
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'.");
}
// Test case: NetworkID > 1024 - NetworkID field is correct
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
tx[jss::NetworkID] = 2040;
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::status] == "success");
BEAST_EXPECT(result[jss::tx_json][jss::NetworkID].asUInt() == 2040);
}
}
public:
void
run() override
@@ -1010,8 +890,6 @@ public:
FeatureBitset const all{testableAmendments()};
testWithFeats(all);
testSignForNetworkIDValidation();
}
void

View File

@@ -30,6 +30,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
// Transaction-specific field values
auto const vaultIDValue = canonical_UINT256();
auto const memoDataValue = canonical_VL();
VaultDeleteBuilder builder{
accountValue,
@@ -39,6 +40,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
};
// Set optional fields
builder.setMemoData(memoDataValue);
auto tx = builder.build(publicKey, secretKey);
@@ -62,6 +64,14 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
}
// Verify optional fields
{
auto const& expected = memoDataValue;
auto const actualOpt = tx.getMemoData();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present";
expectEqualField(expected, *actualOpt, "sfMemoData");
EXPECT_TRUE(tx.hasMemoData());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
@@ -79,6 +89,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
// Transaction-specific field values
auto const vaultIDValue = canonical_UINT256();
auto const memoDataValue = canonical_VL();
// Build an initial transaction
VaultDeleteBuilder initialBuilder{
@@ -88,6 +99,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
feeValue
};
initialBuilder.setMemoData(memoDataValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
@@ -112,6 +124,13 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
}
// Verify optional fields
{
auto const& expected = memoDataValue;
auto const actualOpt = rebuiltTx.getMemoData();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present";
expectEqualField(expected, *actualOpt, "sfMemoData");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
@@ -142,5 +161,35 @@ TEST(TransactionsVaultDeleteTests, BuilderThrowsOnWrongTxType)
EXPECT_THROW(VaultDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(TransactionsVaultDeleteTests, OptionalFieldsReturnNullopt)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testVaultDeleteNullopt"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 3;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific required field values
auto const vaultIDValue = canonical_UINT256();
VaultDeleteBuilder builder{
accountValue,
vaultIDValue,
sequenceValue,
feeValue
};
// Do NOT set optional fields
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasMemoData());
EXPECT_FALSE(tx.getMemoData().has_value());
}
}