mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
Refactor the code generation process
This commit is contained in:
216
scripts/codegen/templates/LedgerEntry.h.mako
Normal file
216
scripts/codegen/templates/LedgerEntry.h.mako
Normal file
@@ -0,0 +1,216 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/LedgerEntryBase.h>
|
||||
#include <xrpl/protocol_autogen/LedgerEntryBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::ledger_entries {
|
||||
|
||||
class ${name}Builder;
|
||||
|
||||
/**
|
||||
* @brief Ledger Entry: ${name}
|
||||
*
|
||||
* Type: ${tag} (${value})
|
||||
* RPC Name: ${rpc_name}
|
||||
*
|
||||
* Immutable wrapper around SLE providing type-safe field access.
|
||||
* Use ${name}Builder to construct new ledger entries.
|
||||
*/
|
||||
class ${name} : public LedgerEntryBase
|
||||
{
|
||||
public:
|
||||
static constexpr LedgerEntryType entryType = ${tag};
|
||||
|
||||
/**
|
||||
* @brief Construct a ${name} ledger entry wrapper from an existing SLE object.
|
||||
* @throws std::runtime_error if the ledger entry type doesn't match.
|
||||
*/
|
||||
explicit ${name}(std::shared_ptr<SLE const> sle)
|
||||
: LedgerEntryBase(std::move(sle))
|
||||
{
|
||||
// Verify ledger entry type
|
||||
if (sle_->getType() != entryType)
|
||||
{
|
||||
throw std::runtime_error("Invalid ledger entry type for ${name}");
|
||||
}
|
||||
}
|
||||
|
||||
// Ledger entry-specific field getters
|
||||
% for field in fields:
|
||||
% if field['typed']:
|
||||
|
||||
/**
|
||||
* @brief Get ${field['name']} (${field['requirement']})
|
||||
% if field.get('mpt_support'):
|
||||
* MPT Support: ${field['mpt_support']}
|
||||
% endif
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
* @return The field value.
|
||||
% else:
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
% endif
|
||||
*/
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
return this->sle_->${field['typeData']['getter_method']}(${field['name']});
|
||||
}
|
||||
% else:
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type_optional']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
if (has${field['name'][2:]}())
|
||||
return this->sle_->${field['typeData']['getter_method']}(${field['name']});
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if ${field['name']} is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
has${field['name'][2:]}() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(${field['name']});
|
||||
}
|
||||
% endif
|
||||
% else:
|
||||
|
||||
/**
|
||||
* @brief Get ${field['name']} (${field['requirement']})
|
||||
% if field.get('mpt_support'):
|
||||
* MPT Support: ${field['mpt_support']}
|
||||
% endif
|
||||
* @note This is an untyped field (${field.get('cppType', 'unknown')}).
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
* @return The field value.
|
||||
% else:
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
% endif
|
||||
*/
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
return this->sle_->${field['typeData']['getter_method']}(${field['name']});
|
||||
}
|
||||
% else:
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type_optional']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
if (this->sle_->isFieldPresent(${field['name']}))
|
||||
return this->sle_->${field['typeData']['getter_method']}(${field['name']});
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if ${field['name']} is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
has${field['name'][2:]}() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(${field['name']});
|
||||
}
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
};
|
||||
|
||||
<%
|
||||
required_fields = [f for f in fields if f['requirement'] == 'soeREQUIRED']
|
||||
%>\
|
||||
/**
|
||||
* @brief Builder for ${name} ledger entries.
|
||||
*
|
||||
* Provides a fluent interface for constructing ledger entries with method chaining.
|
||||
* Uses Json::Value internally for flexible ledger entry construction.
|
||||
* Inherits common field setters from LedgerEntryBuilderBase.
|
||||
*/
|
||||
class ${name}Builder : public LedgerEntryBuilderBase<${name}Builder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new ${name}Builder with required fields.
|
||||
% for field in required_fields:
|
||||
* @param ${field['paramName']} The ${field['name']} field value.
|
||||
% endfor
|
||||
*/
|
||||
${name}Builder(\
|
||||
% for i, field in enumerate(required_fields):
|
||||
${field['typeData']['setter_type']} ${field['paramName']}${',' if i < len(required_fields) - 1 else ''}\
|
||||
% endfor
|
||||
)
|
||||
: LedgerEntryBuilderBase<${name}Builder>(${tag})
|
||||
{
|
||||
% for field in required_fields:
|
||||
set${field['name'][2:]}(${field['paramName']});
|
||||
% endfor
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a ${name}Builder from an existing SLE object.
|
||||
* @param sle The existing ledger entry to copy from.
|
||||
* @throws std::runtime_error if the ledger entry type doesn't match.
|
||||
*/
|
||||
${name}Builder(std::shared_ptr<SLE const> sle)
|
||||
{
|
||||
if (sle->at(sfLedgerEntryType) != ${tag})
|
||||
{
|
||||
throw std::runtime_error("Invalid ledger entry type for ${name}");
|
||||
}
|
||||
object_ = *sle;
|
||||
}
|
||||
|
||||
/** @brief Ledger entry-specific field setters */
|
||||
% for field in fields:
|
||||
|
||||
/**
|
||||
* @brief Set ${field['name']} (${field['requirement']})
|
||||
% if field.get('mpt_support'):
|
||||
* MPT Support: ${field['mpt_support']}
|
||||
% endif
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
${name}Builder&
|
||||
set${field['name'][2:]}(${field['typeData']['setter_type']} value)
|
||||
{
|
||||
% if field.get('stiSuffix') == 'ISSUE':
|
||||
object_[${field['name']}] = STIssue(${field['name']}, value);
|
||||
% elif field['typeData'].get('setter_use_brackets'):
|
||||
object_[${field['name']}] = value;
|
||||
% else:
|
||||
object_.${field['typeData']['setter_method']}(${field['name']}, value);
|
||||
% endif
|
||||
return *this;
|
||||
}
|
||||
% endfor
|
||||
|
||||
/**
|
||||
* @brief Build and return the completed ${name} wrapper.
|
||||
* @param index The ledger entry index.
|
||||
* @return The constructed ledger entry wrapper.
|
||||
*/
|
||||
${name}
|
||||
build(uint256 const& index)
|
||||
{
|
||||
return ${name}{std::make_shared<SLE>(std::move(object_), index)};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::ledger_entries
|
||||
231
scripts/codegen/templates/LedgerEntryTests.cpp.mako
Normal file
231
scripts/codegen/templates/LedgerEntryTests.cpp.mako
Normal file
@@ -0,0 +1,231 @@
|
||||
// Auto-generated unit tests for ledger entry ${name}
|
||||
<%
|
||||
required_fields = [f for f in fields if f["requirement"] == "soeREQUIRED"]
|
||||
optional_fields = [f for f in fields if f["requirement"] != "soeREQUIRED"]
|
||||
|
||||
def canonical_expr(field):
|
||||
return f"canonical_{field['stiSuffix']}()"
|
||||
|
||||
# Pick a wrong ledger entry to test type mismatch
|
||||
# Use Ticket as it has minimal required fields (just Account)
|
||||
if name != "Ticket":
|
||||
wrong_le_include = "Ticket"
|
||||
else:
|
||||
wrong_le_include = "Check"
|
||||
%>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <protocol_autogen/TestHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/${name}.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/${wrong_le_include}.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::ledger_entries {
|
||||
|
||||
// 1 & 4) Set fields via builder setters, build, then read them back via
|
||||
// wrapper getters. After build(), validate() should succeed for both the
|
||||
// builder's STObject and the wrapper's SLE.
|
||||
TEST(${name}Tests, BuilderSettersRoundTrip)
|
||||
{
|
||||
uint256 const index{1u};
|
||||
|
||||
% for field in fields:
|
||||
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
|
||||
% endfor
|
||||
|
||||
${name}Builder builder{
|
||||
% for i, field in enumerate(required_fields):
|
||||
${field["paramName"]}Value${"," if i < len(required_fields) - 1 else ""}
|
||||
% endfor
|
||||
};
|
||||
|
||||
% for field in optional_fields:
|
||||
builder.set${field["name"][2:]}(${field["paramName"]}Value);
|
||||
% endfor
|
||||
|
||||
builder.setLedgerIndex(index);
|
||||
builder.setFlags(0x1u);
|
||||
|
||||
EXPECT_TRUE(builder.validate());
|
||||
|
||||
auto const entry = builder.build(index);
|
||||
|
||||
EXPECT_TRUE(entry.validate());
|
||||
|
||||
% for field in required_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
auto const actual = entry.get${field["name"][2:]}();
|
||||
expectEqualField(expected, actual, "${field["name"]}");
|
||||
}
|
||||
|
||||
% endfor
|
||||
% for field in optional_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
auto const actualOpt = entry.get${field["name"][2:]}();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "${field["name"]}");
|
||||
EXPECT_TRUE(entry.has${field["name"][2:]}());
|
||||
}
|
||||
|
||||
% endfor
|
||||
EXPECT_TRUE(entry.hasLedgerIndex());
|
||||
auto const ledgerIndex = entry.getLedgerIndex();
|
||||
ASSERT_TRUE(ledgerIndex.has_value());
|
||||
EXPECT_EQ(*ledgerIndex, index);
|
||||
EXPECT_EQ(entry.getKey(), index);
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an SLE, set fields directly on it, construct a builder
|
||||
// from that SLE, build a new wrapper, and verify all fields (and validate()).
|
||||
TEST(${name}Tests, BuilderFromSleRoundTrip)
|
||||
{
|
||||
uint256 const index{2u};
|
||||
|
||||
% for field in fields:
|
||||
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
|
||||
% endfor
|
||||
|
||||
auto sle = std::make_shared<SLE>(${name}::entryType, index);
|
||||
|
||||
% for field in fields:
|
||||
% if field.get("stiSuffix") == "ISSUE":
|
||||
sle->at(${field["name"]}) = STIssue(${field["name"]}, ${field["paramName"]}Value);
|
||||
% elif field["typeData"].get("setter_use_brackets"):
|
||||
sle->at(${field["name"]}) = ${field["paramName"]}Value;
|
||||
% else:
|
||||
sle->${field["typeData"]["setter_method"]}(${field["name"]}, ${field["paramName"]}Value);
|
||||
% endif
|
||||
% endfor
|
||||
|
||||
${name}Builder builderFromSle{sle};
|
||||
EXPECT_TRUE(builderFromSle.validate());
|
||||
|
||||
auto const entryFromBuilder = builderFromSle.build(index);
|
||||
|
||||
${name} entryFromSle{sle};
|
||||
EXPECT_TRUE(entryFromBuilder.validate());
|
||||
EXPECT_TRUE(entryFromSle.validate());
|
||||
|
||||
% for field in required_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
|
||||
auto const fromSle = entryFromSle.get${field["name"][2:]}();
|
||||
auto const fromBuilder = entryFromBuilder.get${field["name"][2:]}();
|
||||
|
||||
expectEqualField(expected, fromSle, "${field["name"]}");
|
||||
expectEqualField(expected, fromBuilder, "${field["name"]}");
|
||||
}
|
||||
|
||||
% endfor
|
||||
% for field in optional_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.get${field["name"][2:]}();
|
||||
auto const fromBuilderOpt = entryFromBuilder.get${field["name"][2:]}();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "${field["name"]}");
|
||||
expectEqualField(expected, *fromBuilderOpt, "${field["name"]}");
|
||||
}
|
||||
|
||||
% endfor
|
||||
EXPECT_EQ(entryFromSle.getKey(), index);
|
||||
EXPECT_EQ(entryFromBuilder.getKey(), index);
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
|
||||
TEST(${name}Tests, WrapperThrowsOnWrongEntryType)
|
||||
{
|
||||
uint256 const index{3u};
|
||||
|
||||
// Build a valid ledger entry of a different type
|
||||
// Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq
|
||||
// Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq
|
||||
% if wrong_le_include == "Ticket":
|
||||
${wrong_le_include}Builder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
% else:
|
||||
${wrong_le_include}Builder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_ACCOUNT(),
|
||||
canonical_AMOUNT(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
% endif
|
||||
auto wrongEntry = wrongBuilder.build(index);
|
||||
|
||||
EXPECT_THROW(${name}{wrongEntry.getSle()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong ledger entry type.
|
||||
TEST(${name}Tests, BuilderThrowsOnWrongEntryType)
|
||||
{
|
||||
uint256 const index{4u};
|
||||
|
||||
// Build a valid ledger entry of a different type
|
||||
% if wrong_le_include == "Ticket":
|
||||
${wrong_le_include}Builder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
% else:
|
||||
${wrong_le_include}Builder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_ACCOUNT(),
|
||||
canonical_AMOUNT(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
% endif
|
||||
auto wrongEntry = wrongBuilder.build(index);
|
||||
|
||||
EXPECT_THROW(${name}Builder{wrongEntry.getSle()}, std::runtime_error);
|
||||
}
|
||||
|
||||
% if optional_fields:
|
||||
// 5) Build with only required fields and verify optional fields return nullopt.
|
||||
TEST(${name}Tests, OptionalFieldsReturnNullopt)
|
||||
{
|
||||
uint256 const index{3u};
|
||||
|
||||
% for field in required_fields:
|
||||
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
|
||||
% endfor
|
||||
|
||||
${name}Builder builder{
|
||||
% for i, field in enumerate(required_fields):
|
||||
${field["paramName"]}Value${"," if i < len(required_fields) - 1 else ""}
|
||||
% endfor
|
||||
};
|
||||
|
||||
auto const entry = builder.build(index);
|
||||
|
||||
// Verify optional fields are not present
|
||||
% for field in optional_fields:
|
||||
EXPECT_FALSE(entry.has${field["name"][2:]}());
|
||||
EXPECT_FALSE(entry.get${field["name"][2:]}().has_value());
|
||||
% endfor
|
||||
}
|
||||
% endif
|
||||
}
|
||||
226
scripts/codegen/templates/Transaction.h.mako
Normal file
226
scripts/codegen/templates/Transaction.h.mako
Normal file
@@ -0,0 +1,226 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBase.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
class ${name}Builder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: ${name}
|
||||
*
|
||||
* Type: ${tag} (${value})
|
||||
* Delegable: ${delegable}
|
||||
* Amendment: ${amendments}
|
||||
* Privileges: ${privileges}
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use ${name}Builder to construct new transactions.
|
||||
*/
|
||||
class ${name} : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ${tag};
|
||||
|
||||
/**
|
||||
* @brief Construct a ${name} transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit ${name}(std::shared_ptr<STTx const> tx)
|
||||
: TransactionBase(std::move(tx))
|
||||
{
|
||||
// Verify transaction type
|
||||
if (tx_->getTxnType() != txType)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for ${name}");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
% for field in fields:
|
||||
% if field['typed']:
|
||||
|
||||
/**
|
||||
* @brief Get ${field['name']} (${field['requirement']})
|
||||
% if field.get('supports_mpt'):
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
% endif
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
* @return The field value.
|
||||
% else:
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
% endif
|
||||
*/
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
return this->tx_->${field['typeData']['getter_method']}(${field['name']});
|
||||
}
|
||||
% else:
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type_optional']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
if (has${field['name'][2:]}())
|
||||
{
|
||||
return this->tx_->${field['typeData']['getter_method']}(${field['name']});
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if ${field['name']} is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
has${field['name'][2:]}() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(${field['name']});
|
||||
}
|
||||
% endif
|
||||
% else:
|
||||
/**
|
||||
* @brief Get ${field['name']} (${field['requirement']})
|
||||
% if field.get('supports_mpt'):
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
% endif
|
||||
* @note This is an untyped field.
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
* @return The field value.
|
||||
% else:
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
% endif
|
||||
*/
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
return this->tx_->${field['typeData']['getter_method']}(${field['name']});
|
||||
}
|
||||
% else:
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type_optional']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
if (this->tx_->isFieldPresent(${field['name']}))
|
||||
return this->tx_->${field['typeData']['getter_method']}(${field['name']});
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if ${field['name']} is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
has${field['name'][2:]}() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(${field['name']});
|
||||
}
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
};
|
||||
|
||||
<%
|
||||
required_fields = [f for f in fields if f['requirement'] == 'soeREQUIRED']
|
||||
%>\
|
||||
/**
|
||||
* @brief Builder for ${name} transactions.
|
||||
*
|
||||
* Provides a fluent interface for constructing transactions with method chaining.
|
||||
* Uses Json::Value internally for flexible transaction construction.
|
||||
* Inherits common field setters from TransactionBuilderBase.
|
||||
*/
|
||||
class ${name}Builder : public TransactionBuilderBase<${name}Builder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new ${name}Builder with required fields.
|
||||
* @param account The account initiating the transaction.
|
||||
% for field in required_fields:
|
||||
* @param ${field['paramName']} The ${field['name']} field value.
|
||||
% endfor
|
||||
* @param sequence Optional sequence number for the transaction.
|
||||
* @param fee Optional fee for the transaction.
|
||||
*/
|
||||
${name}Builder(SF_ACCOUNT::type::value_type account,
|
||||
% for i, field in enumerate(required_fields):
|
||||
${field['typeData']['setter_type']} ${field['paramName']},\
|
||||
% endfor
|
||||
std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
|
||||
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
|
||||
)
|
||||
: TransactionBuilderBase<${name}Builder>(${tag}, account, sequence, fee)
|
||||
{
|
||||
% for field in required_fields:
|
||||
set${field['name'][2:]}(${field['paramName']});
|
||||
% endfor
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a ${name}Builder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
${name}Builder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ${tag})
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for ${name}Builder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/** @brief Transaction-specific field setters */
|
||||
% for field in fields:
|
||||
|
||||
/**
|
||||
* @brief Set ${field['name']} (${field['requirement']})
|
||||
% if field.get('supports_mpt'):
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
% endif
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
${name}Builder&
|
||||
set${field['name'][2:]}(${field['typeData']['setter_type']} value)
|
||||
{
|
||||
% if field.get('stiSuffix') == 'ISSUE':
|
||||
object_[${field['name']}] = STIssue(${field['name']}, value);
|
||||
% elif field['typeData'].get('setter_use_brackets'):
|
||||
object_[${field['name']}] = value;
|
||||
% else:
|
||||
object_.${field['typeData']['setter_method']}(${field['name']}, value);
|
||||
% endif
|
||||
return *this;
|
||||
}
|
||||
% endfor
|
||||
|
||||
/**
|
||||
* @brief Build and return the ${name} wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
${name}
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return ${name}{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
241
scripts/codegen/templates/TransactionTests.cpp.mako
Normal file
241
scripts/codegen/templates/TransactionTests.cpp.mako
Normal file
@@ -0,0 +1,241 @@
|
||||
// Auto-generated unit tests for transaction ${name}
|
||||
<%
|
||||
required_fields = [f for f in fields if f["requirement"] == "soeREQUIRED"]
|
||||
optional_fields = [f for f in fields if f["requirement"] != "soeREQUIRED"]
|
||||
|
||||
def canonical_expr(field):
|
||||
return f"canonical_{field['stiSuffix']}()"
|
||||
|
||||
# Pick a wrong transaction to test type mismatch
|
||||
if name != "AccountSet":
|
||||
wrong_tx_include = "AccountSet"
|
||||
else:
|
||||
wrong_tx_include = "OfferCancel"
|
||||
%>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <protocol_autogen/TestHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol_autogen/transactions/${name}.h>
|
||||
#include <xrpl/protocol_autogen/transactions/${wrong_tx_include}.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
// 1 & 4) Set fields via builder setters, build, then read them back via
|
||||
// wrapper getters. After build(), validate() should succeed.
|
||||
TEST(Transactions${name}Tests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::secp256k1, generateSeed("test${name}"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 1;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
% for field in fields:
|
||||
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
|
||||
% endfor
|
||||
|
||||
${name}Builder builder{
|
||||
accountValue,
|
||||
% for field in required_fields:
|
||||
${field["paramName"]}Value,
|
||||
% endfor
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
% for field in optional_fields:
|
||||
builder.set${field["name"][2:]}(${field["paramName"]}Value);
|
||||
% endfor
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(tx.validate(reason)) << reason;
|
||||
|
||||
// Verify signing was applied
|
||||
EXPECT_FALSE(tx.getSigningPubKey().empty());
|
||||
EXPECT_TRUE(tx.hasTxnSignature());
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(tx.getAccount(), accountValue);
|
||||
EXPECT_EQ(tx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(tx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
% for field in required_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
auto const actual = tx.get${field["name"][2:]}();
|
||||
expectEqualField(expected, actual, "${field["name"]}");
|
||||
}
|
||||
|
||||
% endfor
|
||||
// Verify optional fields
|
||||
% for field in optional_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
auto const actualOpt = tx.get${field["name"][2:]}();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field ${field["name"]} should be present";
|
||||
expectEqualField(expected, *actualOpt, "${field["name"]}");
|
||||
EXPECT_TRUE(tx.has${field["name"][2:]}());
|
||||
}
|
||||
|
||||
% endfor
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
// and verify all fields match.
|
||||
TEST(Transactions${name}Tests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::secp256k1, generateSeed("test${name}FromTx"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 2;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
% for field in fields:
|
||||
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
|
||||
% endfor
|
||||
|
||||
// Build an initial transaction
|
||||
${name}Builder initialBuilder{
|
||||
accountValue,
|
||||
% for field in required_fields:
|
||||
${field["paramName"]}Value,
|
||||
% endfor
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
% for field in optional_fields:
|
||||
initialBuilder.set${field["name"][2:]}(${field["paramName"]}Value);
|
||||
% endfor
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
${name}Builder builderFromTx{initialTx.getSTTx()};
|
||||
|
||||
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
|
||||
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
% for field in required_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
auto const actual = rebuiltTx.get${field["name"][2:]}();
|
||||
expectEqualField(expected, actual, "${field["name"]}");
|
||||
}
|
||||
|
||||
% endfor
|
||||
// Verify optional fields
|
||||
% for field in optional_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
auto const actualOpt = rebuiltTx.get${field["name"][2:]}();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field ${field["name"]} should be present";
|
||||
expectEqualField(expected, *actualOpt, "${field["name"]}");
|
||||
}
|
||||
|
||||
% endfor
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
TEST(Transactions${name}Tests, WrapperThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::secp256k1, generateSeed("testWrongType"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
% if wrong_tx_include == "AccountSet":
|
||||
${wrong_tx_include}Builder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
% else:
|
||||
${wrong_tx_include}Builder wrongBuilder{account, canonical_UINT32(), 1, canonical_AMOUNT()};
|
||||
% endif
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(${name}{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(Transactions${name}Tests, BuilderThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::secp256k1, generateSeed("testWrongTypeBuilder"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
% if wrong_tx_include == "AccountSet":
|
||||
${wrong_tx_include}Builder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
% else:
|
||||
${wrong_tx_include}Builder wrongBuilder{account, canonical_UINT32(), 1, canonical_AMOUNT()};
|
||||
% endif
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(${name}Builder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
% if optional_fields:
|
||||
// 5) Build with only required fields and verify optional fields return nullopt.
|
||||
TEST(Transactions${name}Tests, OptionalFieldsReturnNullopt)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::secp256k1, generateSeed("test${name}Nullopt"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 3;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific required field values
|
||||
% for field in required_fields:
|
||||
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
|
||||
% endfor
|
||||
|
||||
${name}Builder builder{
|
||||
accountValue,
|
||||
% for field in required_fields:
|
||||
${field["paramName"]}Value,
|
||||
% endfor
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Do NOT set optional fields
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
// Verify optional fields are not present
|
||||
% for field in optional_fields:
|
||||
EXPECT_FALSE(tx.has${field["name"][2:]}());
|
||||
EXPECT_FALSE(tx.get${field["name"][2:]}().has_value());
|
||||
% endfor
|
||||
}
|
||||
% endif
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user