mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 23:38:08 +00:00
featureFirewall
This commit is contained in:
37
src/libxrpl/protocol/Firewall.cpp
Normal file
37
src/libxrpl/protocol/Firewall.cpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#include <xrpl/protocol/Firewall.h>
|
||||
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/TxSettings.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
#pragma push_macro("UNWRAP")
|
||||
#undef UNWRAP
|
||||
#pragma push_macro("TRANSACTION")
|
||||
#undef TRANSACTION
|
||||
|
||||
#define UNWRAP(...) __VA_ARGS__
|
||||
#define TRANSACTION(tag, value, name, settings, ...) \
|
||||
case tag: \
|
||||
return (TxSettings UNWRAP settings).firewall;
|
||||
|
||||
FirewallAction
|
||||
firewallAction(TxType txType) noexcept
|
||||
{
|
||||
switch (txType)
|
||||
{
|
||||
#include <xrpl/protocol/detail/transactions.macro>
|
||||
|
||||
// Deprecated types
|
||||
default:
|
||||
return FirewallAction::Allow;
|
||||
}
|
||||
}
|
||||
|
||||
#undef TRANSACTION
|
||||
#pragma pop_macro("TRANSACTION")
|
||||
#undef UNWRAP
|
||||
#pragma pop_macro("UNWRAP")
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -104,6 +104,8 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
LoanBroker = 'l', // lower-case L
|
||||
Loan = 'L',
|
||||
Sponsorship = '>',
|
||||
Firewall = 'F',
|
||||
WithdrawPreauth = 'G',
|
||||
|
||||
// No longer used or supported. Left here to reserve the space to avoid accidental reuse.
|
||||
Contract [[deprecated]] = 'c',
|
||||
@@ -610,6 +612,20 @@ permissionedDomain(uint256 const& domainID) noexcept
|
||||
return {ltPERMISSIONED_DOMAIN, domainID};
|
||||
}
|
||||
|
||||
Keylet
|
||||
firewall(AccountID const& account) noexcept
|
||||
{
|
||||
return {ltFIREWALL, indexHash(LedgerNameSpace::Firewall, account)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
withdrawPreauth(AccountID const& owner, AccountID const& preauthorized, std::uint32_t dtag) noexcept
|
||||
{
|
||||
return {
|
||||
ltWITHDRAW_PREAUTH,
|
||||
indexHash(LedgerNameSpace::WithdrawPreauth, owner, preauthorized, dtag)};
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -137,6 +137,7 @@ transResults()
|
||||
MAKE_ERROR(tefBAD_PATH_COUNT, "Malformed: Too many paths."),
|
||||
MAKE_ERROR(tefNO_BYTECODE, "There is no WASM code to run, but a WASM-specific field was included."),
|
||||
MAKE_ERROR(tefBYTECODE_NOT_INCLUDED, "WASM code requires a field that was not included."),
|
||||
MAKE_ERROR(tefFIREWALL_BLOCK, "Transaction was blocked by the account's firewall."),
|
||||
|
||||
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
|
||||
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Firewall.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Permissions.h>
|
||||
@@ -1015,6 +1016,66 @@ Transactor::checkSign(PreclaimContext const& ctx)
|
||||
return checkSign(ctx.view, ctx.flags, ctx.parentBatchId, idAccount, ctx.tx, ctx.j);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
Transactor::checkFirewall(PreclaimContext const& ctx)
|
||||
{
|
||||
if (!ctx.view.rules().enabled(featureFirewall))
|
||||
return tesSUCCESS;
|
||||
|
||||
auto const account = ctx.tx.getAccountID(sfAccount);
|
||||
auto const sleFirewall = ctx.view.read(keylet::firewall(account));
|
||||
if (!sleFirewall)
|
||||
return tesSUCCESS;
|
||||
|
||||
if (sleFirewall->isFieldPresent(sfMaxFee) &&
|
||||
ctx.tx.getFieldAmount(sfFee) > sleFirewall->getFieldAmount(sfMaxFee))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "Firewall: the fee exceeds the firewall's MaxFee";
|
||||
return tefFIREWALL_BLOCK;
|
||||
}
|
||||
|
||||
switch (firewallAction(ctx.tx.getTxnType()))
|
||||
{
|
||||
case FirewallAction::Allow:
|
||||
return tesSUCCESS;
|
||||
|
||||
case FirewallAction::Block:
|
||||
JLOG(ctx.j.trace()) << "Firewall: transaction type " << ctx.tx.getTxnType()
|
||||
<< " is blocked while a firewall is set";
|
||||
return tefFIREWALL_BLOCK;
|
||||
|
||||
case FirewallAction::Check:
|
||||
break;
|
||||
}
|
||||
|
||||
// A payment to itself, or one carrying paths, can deliver to an account the
|
||||
// preauthorization check below would not see.
|
||||
if (ctx.tx.getTxnType() == ttPAYMENT &&
|
||||
(ctx.tx.getAccountID(sfDestination) == account || ctx.tx.isFieldPresent(sfPaths)))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "Firewall: a self payment or a payment with paths is blocked";
|
||||
return tefFIREWALL_BLOCK;
|
||||
}
|
||||
|
||||
if (!ctx.tx.isFieldPresent(sfDestination))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "Firewall: a checked transaction without a destination is blocked";
|
||||
return tefFIREWALL_BLOCK;
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(
|
||||
keylet::withdrawPreauth(
|
||||
account,
|
||||
ctx.tx.getAccountID(sfDestination),
|
||||
ctx.tx[~sfDestinationTag].value_or(0))))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "Firewall: the destination is not preauthorized";
|
||||
return tefFIREWALL_BLOCK;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
Transactor::checkSingleSign(
|
||||
ReadView const& view,
|
||||
|
||||
@@ -191,6 +191,9 @@ invokePreclaim(PreclaimContext const& ctx)
|
||||
if (NotTEC const result = T::checkSign(ctx))
|
||||
return result;
|
||||
|
||||
if (NotTEC const result = Transactor::checkFirewall(ctx))
|
||||
return result;
|
||||
|
||||
return tesSUCCESS;
|
||||
}())
|
||||
return preSigResult;
|
||||
|
||||
53
src/libxrpl/tx/invariants/FirewallInvariant.cpp
Normal file
53
src/libxrpl/tx/invariants/FirewallInvariant.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include <xrpl/tx/invariants/FirewallInvariant.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAccount.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
void
|
||||
ValidFirewall::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
if (after && after->getType() == ltFIREWALL)
|
||||
firewalls_.emplace_back(before, after);
|
||||
}
|
||||
|
||||
bool
|
||||
ValidFirewall::finalize(
|
||||
STTx const& tx,
|
||||
TER const,
|
||||
XRPAmount const,
|
||||
ReadView const&,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
// A firewall cannot exist unless the amendment is enabled, so the amendment
|
||||
// needs no separate check here.
|
||||
for (auto const& [before, after] : firewalls_)
|
||||
{
|
||||
if (!after->isFieldPresent(sfOwner) || !after->isFieldPresent(sfCounterparty))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: a firewall is missing its owner "
|
||||
"or its counterparty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (after->getAccountID(sfOwner) == after->getAccountID(sfCounterparty))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: a firewall's counterparty is its owner";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (before && before->getAccountID(sfOwner) != after->getAccountID(sfOwner))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: a firewall's owner changed";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <xrpl/tx/transactors/account/SignerListSet.h>
|
||||
#include <xrpl/tx/transactors/delegate/DelegateSet.h>
|
||||
#include <xrpl/tx/transactors/did/DIDDelete.h>
|
||||
#include <xrpl/tx/transactors/firewall/WithdrawPreauth.h>
|
||||
#include <xrpl/tx/transactors/oracle/OracleDelete.h>
|
||||
#include <xrpl/tx/transactors/payment/DepositPreauth.h>
|
||||
|
||||
@@ -122,6 +123,18 @@ removeDepositPreauthFromLedger(
|
||||
return DepositPreauth::removeFromLedger(view, delIndex, j);
|
||||
}
|
||||
|
||||
TER
|
||||
removeWithdrawPreauthFromLedger(
|
||||
ServiceRegistry&,
|
||||
ApplyView& view,
|
||||
AccountID const&,
|
||||
uint256 const& delIndex,
|
||||
SLE::ref,
|
||||
beast::Journal j)
|
||||
{
|
||||
return WithdrawPreauth::removeFromLedger(view, delIndex, j);
|
||||
}
|
||||
|
||||
TER
|
||||
removeNFTokenOfferFromLedger(
|
||||
ServiceRegistry&,
|
||||
@@ -211,6 +224,8 @@ nonObligationDeleter(LedgerEntryType t)
|
||||
return removeCredentialFromLedger;
|
||||
case ltDELEGATE:
|
||||
return removeDelegateFromLedger;
|
||||
case ltWITHDRAW_PREAUTH:
|
||||
return removeWithdrawPreauthFromLedger;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
145
src/libxrpl/tx/transactors/firewall/FirewallDelete.cpp
Normal file
145
src/libxrpl/tx/transactors/firewall/FirewallDelete.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
#include <xrpl/tx/transactors/firewall/FirewallDelete.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/transactors/firewall/WithdrawPreauth.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
std::uint32_t
|
||||
FirewallDelete::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
return tfUniversalMask;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
FirewallDelete::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const normalCost = Transactor::calculateBaseFee(view, tx);
|
||||
|
||||
auto const counterSig = tx.getFieldObject(sfCounterpartySignature);
|
||||
std::size_t const signerCount = [&counterSig]() -> std::size_t {
|
||||
if (counterSig.isFieldPresent(sfSigners))
|
||||
return counterSig.getFieldArray(sfSigners).size();
|
||||
return counterSig.isFieldPresent(sfTxnSignature) ? 1 : 0;
|
||||
}();
|
||||
|
||||
return normalCost + (view.fees().base * signerCount);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
FirewallDelete::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
auto const counterSig = ctx.tx.getFieldObject(sfCounterpartySignature);
|
||||
if (auto const ret = detail::preflightCheckSigningKey(counterSig, ctx.j))
|
||||
return ret;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
FirewallDelete::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const sleFirewall = ctx.view.read(keylet::firewall(ctx.tx.getFieldH256(sfFirewallID)));
|
||||
if (!sleFirewall)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallDelete: the firewall was not found";
|
||||
return tecNO_TARGET;
|
||||
}
|
||||
|
||||
if (sleFirewall->getAccountID(sfOwner) != ctx.tx.getAccountID(sfAccount))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallDelete: the account does not own the firewall";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
return Transactor::checkSign(
|
||||
ctx.view,
|
||||
ctx.flags,
|
||||
ctx.parentBatchId,
|
||||
sleFirewall->getAccountID(sfCounterparty),
|
||||
ctx.tx.getFieldObject(sfCounterpartySignature),
|
||||
ctx.j);
|
||||
}
|
||||
|
||||
TER
|
||||
FirewallDelete::doApply()
|
||||
{
|
||||
auto applyViewContext = ctx_.getApplyViewContext();
|
||||
|
||||
auto const sleOwner = view().peek(keylet::account(accountID_));
|
||||
if (!sleOwner)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
uint256 const firewallID = ctx_.tx.getFieldH256(sfFirewallID);
|
||||
auto const sleFirewall = view().peek(keylet::firewall(firewallID));
|
||||
if (!sleFirewall)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// The preauthorizations exist only to let value past the firewall, so they
|
||||
// go with it.
|
||||
auto const ter = cleanupOnAccountDelete(
|
||||
view(),
|
||||
keylet::ownerDir(accountID_),
|
||||
[&](LedgerEntryType nodeType,
|
||||
uint256 const& dirEntry,
|
||||
std::shared_ptr<SLE>& sleItem) -> std::pair<TER, SkipEntry> {
|
||||
if (nodeType == ltWITHDRAW_PREAUTH)
|
||||
return {WithdrawPreauth::removeFromLedger(view(), dirEntry, j_), SkipEntry::No};
|
||||
return {tesSUCCESS, SkipEntry::Yes};
|
||||
},
|
||||
j_);
|
||||
if (ter != tesSUCCESS)
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
|
||||
std::uint64_t const page{(*sleFirewall)[sfOwnerNode]};
|
||||
if (!view().dirRemove(keylet::ownerDir(accountID_), page, firewallID, false))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.fatal()) << "FirewallDelete: could not remove the firewall from the owner dir";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
decreaseOwnerCountForObject(view(), sleOwner, sleFirewall, 1, j_);
|
||||
view().erase(sleFirewall);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
FirewallDelete::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
FirewallDelete::finalizeInvariants(
|
||||
STTx const&,
|
||||
TER,
|
||||
XRPAmount,
|
||||
ReadView const&,
|
||||
beast::Journal const&)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
303
src/libxrpl/tx/transactors/firewall/FirewallSet.cpp
Normal file
303
src/libxrpl/tx/transactors/firewall/FirewallSet.cpp
Normal file
@@ -0,0 +1,303 @@
|
||||
#include <xrpl/tx/transactors/firewall/FirewallSet.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
std::uint32_t
|
||||
FirewallSet::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
return tfUniversalMask;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
FirewallSet::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const normalCost = Transactor::calculateBaseFee(view, tx);
|
||||
|
||||
// Each signature in the counterparty's signature, single or multi, adds one
|
||||
// base fee. getFieldObject returns an empty object when the field is
|
||||
// absent, which is the create case.
|
||||
auto const counterSig = tx.getFieldObject(sfCounterpartySignature);
|
||||
std::size_t const signerCount = [&counterSig]() -> std::size_t {
|
||||
if (counterSig.isFieldPresent(sfSigners))
|
||||
return counterSig.getFieldArray(sfSigners).size();
|
||||
return counterSig.isFieldPresent(sfTxnSignature) ? 1 : 0;
|
||||
}();
|
||||
|
||||
return normalCost + (view.fees().base * signerCount);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
FirewallSet::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
AccountID const account = ctx.tx.getAccountID(sfAccount);
|
||||
bool const isCreate = !ctx.tx.isFieldPresent(sfFirewallID);
|
||||
|
||||
if (isCreate)
|
||||
{
|
||||
if (!ctx.tx.isFieldPresent(sfCounterparty))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: sfCounterparty is required for creation";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (account == ctx.tx.getAccountID(sfCounterparty))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: sfCounterparty must not be the account";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (!ctx.tx.isFieldPresent(sfBackup))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: sfBackup is required for creation";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (account == ctx.tx.getAccountID(sfBackup))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: sfBackup must not be the account";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfCounterpartySignature))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "FirewallSet: sfCounterpartySignature is not allowed for creation";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ctx.tx.isFieldPresent(sfBackup))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: sfBackup is not allowed for an update";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfCounterparty) && account == ctx.tx.getAccountID(sfCounterparty))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: sfCounterparty must not be the account";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (!ctx.tx.isFieldPresent(sfCounterpartySignature))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: sfCounterpartySignature is required for an update";
|
||||
return temBAD_SIGNER;
|
||||
}
|
||||
|
||||
auto const counterSig = ctx.tx.getFieldObject(sfCounterpartySignature);
|
||||
if (auto const ret = detail::preflightCheckSigningKey(counterSig, ctx.j))
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfMaxFee))
|
||||
{
|
||||
auto const maxFee = ctx.tx.getFieldAmount(sfMaxFee);
|
||||
if (!maxFee.native() || maxFee.negative() || !isLegalNet(maxFee))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: sfMaxFee is invalid";
|
||||
return temBAD_AMOUNT;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
FirewallSet::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
AccountID const account = ctx.tx.getAccountID(sfAccount);
|
||||
|
||||
if (!ctx.tx.isFieldPresent(sfFirewallID))
|
||||
{
|
||||
if (ctx.view.exists(keylet::firewall(account)))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: a firewall already exists for the account";
|
||||
return tecDUPLICATE;
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(keylet::account(ctx.tx.getAccountID(sfCounterparty))))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: the counterparty account does not exist";
|
||||
return tecNO_DST;
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(keylet::account(ctx.tx.getAccountID(sfBackup))))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: the backup account does not exist";
|
||||
return tecNO_DST;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
auto const sleFirewall = ctx.view.read(keylet::firewall(ctx.tx.getFieldH256(sfFirewallID)));
|
||||
if (!sleFirewall)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: the firewall was not found";
|
||||
return tecNO_TARGET;
|
||||
}
|
||||
|
||||
if (sleFirewall->getAccountID(sfOwner) != account)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: the account does not own the firewall";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfCounterparty))
|
||||
{
|
||||
AccountID const newCounterparty = ctx.tx.getAccountID(sfCounterparty);
|
||||
if (sleFirewall->getAccountID(sfCounterparty) == newCounterparty)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: sfCounterparty matches the recorded counterparty";
|
||||
return tecDUPLICATE;
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(keylet::account(newCounterparty)))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "FirewallSet: the new counterparty account does not exist";
|
||||
return tecNO_DST;
|
||||
}
|
||||
}
|
||||
|
||||
// The counterparty recorded on the firewall authorizes the update, not
|
||||
// whoever the transaction names.
|
||||
return Transactor::checkSign(
|
||||
ctx.view,
|
||||
ctx.flags,
|
||||
ctx.parentBatchId,
|
||||
sleFirewall->getAccountID(sfCounterparty),
|
||||
ctx.tx.getFieldObject(sfCounterpartySignature),
|
||||
ctx.j);
|
||||
}
|
||||
|
||||
TER
|
||||
FirewallSet::createFirewall(std::shared_ptr<SLE> const& sleOwner)
|
||||
{
|
||||
auto applyViewContext = ctx_.getApplyViewContext();
|
||||
|
||||
// A create inserts the firewall and the backup preauthorization.
|
||||
if (auto const ret =
|
||||
checkReserve(applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 2}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
auto const sleFirewall = std::make_shared<SLE>(keylet::firewall(accountID_));
|
||||
sleFirewall->setAccountID(sfOwner, accountID_);
|
||||
sleFirewall->setAccountID(sfCounterparty, ctx_.tx.getAccountID(sfCounterparty));
|
||||
if (ctx_.tx.isFieldPresent(sfMaxFee))
|
||||
sleFirewall->setFieldAmount(sfMaxFee, ctx_.tx.getFieldAmount(sfMaxFee));
|
||||
view().insert(sleFirewall);
|
||||
|
||||
if (auto const page = view().dirInsert(
|
||||
keylet::ownerDir(accountID_), sleFirewall->key(), describeOwnerDir(accountID_)))
|
||||
{
|
||||
sleFirewall->setFieldU64(sfOwnerNode, *page);
|
||||
}
|
||||
else
|
||||
{
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
increaseOwnerCount(applyViewContext, sleOwner, 1, j_);
|
||||
addSponsorToLedgerEntry(applyViewContext, sleFirewall);
|
||||
|
||||
// The backup destination is preauthorized as the firewall is created, so
|
||||
// the account always has one destination it can still reach.
|
||||
AccountID const backup = ctx_.tx.getAccountID(sfBackup);
|
||||
std::uint32_t const dtag = ctx_.tx[~sfDestinationTag].value_or(0);
|
||||
Keylet const preauthKeylet = keylet::withdrawPreauth(accountID_, backup, dtag);
|
||||
auto const slePreauth = std::make_shared<SLE>(preauthKeylet);
|
||||
slePreauth->setAccountID(sfAccount, accountID_);
|
||||
slePreauth->setAccountID(sfAuthorize, backup);
|
||||
slePreauth->setFieldU32(sfDestinationTag, dtag);
|
||||
view().insert(slePreauth);
|
||||
|
||||
if (auto const page = view().dirInsert(
|
||||
keylet::ownerDir(accountID_), preauthKeylet, describeOwnerDir(accountID_)))
|
||||
{
|
||||
slePreauth->setFieldU64(sfOwnerNode, *page);
|
||||
}
|
||||
else
|
||||
{
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
increaseOwnerCount(applyViewContext, sleOwner, 1, j_);
|
||||
addSponsorToLedgerEntry(applyViewContext, slePreauth);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
FirewallSet::updateFirewall()
|
||||
{
|
||||
auto const sleFirewall = view().peek(keylet::firewall(ctx_.tx.getFieldH256(sfFirewallID)));
|
||||
if (!sleFirewall)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (ctx_.tx.isFieldPresent(sfCounterparty))
|
||||
sleFirewall->setAccountID(sfCounterparty, ctx_.tx.getAccountID(sfCounterparty));
|
||||
|
||||
// A zero MaxFee removes the cap.
|
||||
if (ctx_.tx.isFieldPresent(sfMaxFee))
|
||||
{
|
||||
if (ctx_.tx.getFieldAmount(sfMaxFee) == beast::kZero)
|
||||
sleFirewall->makeFieldAbsent(sfMaxFee);
|
||||
else
|
||||
sleFirewall->setFieldAmount(sfMaxFee, ctx_.tx.getFieldAmount(sfMaxFee));
|
||||
}
|
||||
|
||||
view().update(sleFirewall);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
FirewallSet::doApply()
|
||||
{
|
||||
auto const sleOwner = view().peek(keylet::account(accountID_));
|
||||
if (!sleOwner)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (!ctx_.tx.isFieldPresent(sfFirewallID))
|
||||
return createFirewall(sleOwner);
|
||||
|
||||
return updateFirewall();
|
||||
}
|
||||
|
||||
void
|
||||
FirewallSet::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
FirewallSet::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
223
src/libxrpl/tx/transactors/firewall/WithdrawPreauth.cpp
Normal file
223
src/libxrpl/tx/transactors/firewall/WithdrawPreauth.cpp
Normal file
@@ -0,0 +1,223 @@
|
||||
#include <xrpl/tx/transactors/firewall/WithdrawPreauth.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
std::uint32_t
|
||||
WithdrawPreauth::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
return tfUniversalMask;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
WithdrawPreauth::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const normalCost = Transactor::calculateBaseFee(view, tx);
|
||||
|
||||
auto const counterSig = tx.getFieldObject(sfCounterpartySignature);
|
||||
std::size_t const signerCount = [&counterSig]() -> std::size_t {
|
||||
if (counterSig.isFieldPresent(sfSigners))
|
||||
return counterSig.getFieldArray(sfSigners).size();
|
||||
return counterSig.isFieldPresent(sfTxnSignature) ? 1 : 0;
|
||||
}();
|
||||
|
||||
return normalCost + (view.fees().base * signerCount);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
WithdrawPreauth::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
auto const& tx = ctx.tx;
|
||||
auto const& j = ctx.j;
|
||||
|
||||
auto const optAuth = tx[~sfAuthorize];
|
||||
auto const optUnauth = tx[~sfUnauthorize];
|
||||
if (static_cast<bool>(optAuth) == static_cast<bool>(optUnauth))
|
||||
{
|
||||
JLOG(j.trace()) << "WithdrawPreauth: exactly one of sfAuthorize and "
|
||||
"sfUnauthorize is required";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
AccountID const target{optAuth ? *optAuth : *optUnauth};
|
||||
if (target == beast::kZero)
|
||||
{
|
||||
JLOG(j.trace()) << "WithdrawPreauth: the authorized account is zeroed";
|
||||
return temINVALID_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
if (optAuth && (target == tx[sfAccount]))
|
||||
{
|
||||
JLOG(j.trace()) << "WithdrawPreauth: an account may not preauthorize itself";
|
||||
return temCANNOT_PREAUTH_SELF;
|
||||
}
|
||||
|
||||
auto const counterSig = tx.getFieldObject(sfCounterpartySignature);
|
||||
if (auto const ret = detail::preflightCheckSigningKey(counterSig, j))
|
||||
return ret;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
WithdrawPreauth::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
AccountID const accountID = ctx.tx[sfAccount];
|
||||
std::uint32_t const dtag = ctx.tx[~sfDestinationTag].value_or(0);
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfAuthorize))
|
||||
{
|
||||
AccountID const auth{ctx.tx[sfAuthorize]};
|
||||
if (!ctx.view.exists(keylet::account(auth)))
|
||||
return tecNO_TARGET;
|
||||
|
||||
if (ctx.view.exists(keylet::withdrawPreauth(accountID, auth, dtag)))
|
||||
return tecDUPLICATE;
|
||||
}
|
||||
else
|
||||
{
|
||||
AccountID const unauth{ctx.tx[sfUnauthorize]};
|
||||
if (!ctx.view.exists(keylet::withdrawPreauth(accountID, unauth, dtag)))
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
|
||||
auto const sleFirewall = ctx.view.read(keylet::firewall(accountID));
|
||||
if (!sleFirewall)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "WithdrawPreauth: the account has no firewall";
|
||||
return tecNO_TARGET;
|
||||
}
|
||||
|
||||
if (sleFirewall->key() != ctx.tx.getFieldH256(sfFirewallID))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "WithdrawPreauth: sfFirewallID is not the account's firewall";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
return Transactor::checkSign(
|
||||
ctx.view,
|
||||
ctx.flags,
|
||||
ctx.parentBatchId,
|
||||
sleFirewall->getAccountID(sfCounterparty),
|
||||
ctx.tx.getFieldObject(sfCounterpartySignature),
|
||||
ctx.j);
|
||||
}
|
||||
|
||||
TER
|
||||
WithdrawPreauth::doApply()
|
||||
{
|
||||
std::uint32_t const dtag = ctx_.tx[~sfDestinationTag].value_or(0);
|
||||
|
||||
if (!ctx_.tx.isFieldPresent(sfAuthorize))
|
||||
{
|
||||
auto const preauth = keylet::withdrawPreauth(accountID_, ctx_.tx[sfUnauthorize], dtag);
|
||||
return WithdrawPreauth::removeFromLedger(view(), preauth.key, j_);
|
||||
}
|
||||
|
||||
auto applyViewContext = ctx_.getApplyViewContext();
|
||||
|
||||
auto const sleOwner = view().peek(keylet::account(accountID_));
|
||||
if (!sleOwner)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// The preauthorization counts against the owner's reserve, checked against
|
||||
// the starting balance so the fee may still dip into it.
|
||||
if (auto const ret =
|
||||
checkReserve(applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
AccountID const auth{ctx_.tx[sfAuthorize]};
|
||||
Keylet const preauthKeylet = keylet::withdrawPreauth(accountID_, auth, dtag);
|
||||
auto const slePreauth = std::make_shared<SLE>(preauthKeylet);
|
||||
|
||||
slePreauth->setAccountID(sfAccount, accountID_);
|
||||
slePreauth->setAccountID(sfAuthorize, auth);
|
||||
slePreauth->setFieldU32(sfDestinationTag, dtag);
|
||||
view().insert(slePreauth);
|
||||
|
||||
auto const page =
|
||||
view().dirInsert(keylet::ownerDir(accountID_), preauthKeylet, describeOwnerDir(accountID_));
|
||||
|
||||
JLOG(j_.trace()) << "WithdrawPreauth: adding " << to_string(preauthKeylet.key)
|
||||
<< " to the owner directory: " << (page ? "success" : "failure");
|
||||
|
||||
if (!page)
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
|
||||
slePreauth->setFieldU64(sfOwnerNode, *page);
|
||||
increaseOwnerCount(applyViewContext, sleOwner, 1, j_);
|
||||
addSponsorToLedgerEntry(applyViewContext, slePreauth);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
WithdrawPreauth::removeFromLedger(ApplyView& view, uint256 const& preauthIndex, beast::Journal j)
|
||||
{
|
||||
auto const slePreauth = view.peek(keylet::withdrawPreauth(preauthIndex));
|
||||
if (!slePreauth)
|
||||
{
|
||||
JLOG(j.warn()) << "WithdrawPreauth: the preauthorization does not exist";
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
|
||||
AccountID const account{(*slePreauth)[sfAccount]};
|
||||
std::uint64_t const page{(*slePreauth)[sfOwnerNode]};
|
||||
if (!view.dirRemove(keylet::ownerDir(account), page, preauthIndex, false))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "WithdrawPreauth: could not remove it from the owner directory";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const sleOwner = view.peek(keylet::account(account));
|
||||
if (!sleOwner)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
decreaseOwnerCountForObject(view, sleOwner, slePreauth, 1, j);
|
||||
view.erase(slePreauth);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
WithdrawPreauth::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
WithdrawPreauth::finalizeInvariants(
|
||||
STTx const&,
|
||||
TER,
|
||||
XRPAmount,
|
||||
ReadView const&,
|
||||
beast::Journal const&)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
428
src/test/app/Firewall_test.cpp
Normal file
428
src/test/app/Firewall_test.cpp
Normal file
@@ -0,0 +1,428 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/acctdelete.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/noop.h>
|
||||
#include <test/jtx/offer.h>
|
||||
#include <test/jtx/owners.h>
|
||||
#include <test/jtx/paths.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/sig.h>
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/trust.h>
|
||||
#include <test/jtx/utility.h>
|
||||
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class Firewall_test : public beast::unit_test::Suite
|
||||
{
|
||||
// A counterparty signature adds one base fee to the minimum.
|
||||
static inline jtx::Fee const kSignedFee{jtx::drops(20)};
|
||||
|
||||
static uint256
|
||||
firewallID(jtx::Account const& account)
|
||||
{
|
||||
return keylet::firewall(account.id()).key;
|
||||
}
|
||||
|
||||
static bool
|
||||
hasFirewall(jtx::Env const& env, jtx::Account const& account)
|
||||
{
|
||||
return env.le(keylet::firewall(account.id())) != nullptr;
|
||||
}
|
||||
|
||||
void
|
||||
testEnabled(FeatureBitset features)
|
||||
{
|
||||
testcase("enabled");
|
||||
using namespace jtx;
|
||||
|
||||
// Without the amendment the transactions are disabled.
|
||||
{
|
||||
Env env{*this, features - featureFirewall};
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
Account const backup{"backup"};
|
||||
env.fund(XRP(10000), alice, carol, backup);
|
||||
env.close();
|
||||
|
||||
env(firewall::set(alice.id(), carol.id(), backup.id()), Ter(temDISABLED));
|
||||
env.close();
|
||||
BEAST_EXPECT(!hasFirewall(env, alice));
|
||||
}
|
||||
|
||||
// With it, a firewall is created.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
Account const backup{"backup"};
|
||||
env.fund(XRP(10000), alice, carol, backup);
|
||||
env.close();
|
||||
|
||||
auto const ownersBefore = ownerCount(env, alice);
|
||||
env(firewall::set(alice.id(), carol.id(), backup.id()));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(hasFirewall(env, alice));
|
||||
// The firewall and the backup preauthorization.
|
||||
BEAST_EXPECT(ownerCount(env, alice) == ownersBefore + 2);
|
||||
|
||||
auto const sle = env.le(keylet::firewall(alice.id()));
|
||||
BEAST_EXPECT(sle && sle->getAccountID(sfOwner) == alice.id());
|
||||
BEAST_EXPECT(sle && sle->getAccountID(sfCounterparty) == carol.id());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCreatePreflight(FeatureBitset features)
|
||||
{
|
||||
testcase("create preflight");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
Account const backup{"backup"};
|
||||
env.fund(XRP(10000), alice, carol, backup);
|
||||
env.close();
|
||||
|
||||
// The counterparty is required.
|
||||
{
|
||||
auto jv = firewall::set(alice.id(), carol.id(), backup.id());
|
||||
jv.removeMember(sfCounterparty.jsonName);
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// The backup is required.
|
||||
{
|
||||
auto jv = firewall::set(alice.id(), carol.id(), backup.id());
|
||||
jv.removeMember(sfBackup.jsonName);
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// Neither may be the account itself.
|
||||
env(firewall::set(alice.id(), alice.id(), backup.id()), Ter(temMALFORMED));
|
||||
env(firewall::set(alice.id(), carol.id(), alice.id()), Ter(temMALFORMED));
|
||||
|
||||
// A creation carries no counterparty signature.
|
||||
env(firewall::set(alice.id(), carol.id(), backup.id()),
|
||||
Sig(sfCounterpartySignature, carol),
|
||||
kSignedFee,
|
||||
Ter(temMALFORMED));
|
||||
|
||||
env.close();
|
||||
BEAST_EXPECT(!hasFirewall(env, alice));
|
||||
}
|
||||
|
||||
void
|
||||
testCreatePreclaim(FeatureBitset features)
|
||||
{
|
||||
testcase("create preclaim");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
Account const backup{"backup"};
|
||||
Account const absent{"absent"};
|
||||
env.fund(XRP(10000), alice, carol, backup);
|
||||
env.close();
|
||||
|
||||
// The counterparty and the backup must exist.
|
||||
env(firewall::set(alice.id(), absent.id(), backup.id()), Ter(tecNO_DST));
|
||||
env(firewall::set(alice.id(), carol.id(), absent.id()), Ter(tecNO_DST));
|
||||
env.close();
|
||||
|
||||
env(firewall::set(alice.id(), carol.id(), backup.id()));
|
||||
env.close();
|
||||
BEAST_EXPECT(hasFirewall(env, alice));
|
||||
|
||||
// Only one firewall per account.
|
||||
env(firewall::set(alice.id(), carol.id(), backup.id()), Ter(tecDUPLICATE));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testUpdate(FeatureBitset features)
|
||||
{
|
||||
testcase("update");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
Account const dave{"dave"};
|
||||
Account const backup{"backup"};
|
||||
env.fund(XRP(10000), alice, carol, dave, backup);
|
||||
env.close();
|
||||
|
||||
env(firewall::set(alice.id(), carol.id(), backup.id()));
|
||||
env.close();
|
||||
auto const fwID = firewallID(alice);
|
||||
|
||||
// An update needs the counterparty's signature.
|
||||
env(firewall::set(alice.id(), fwID), Ter(temBAD_SIGNER));
|
||||
|
||||
// Signed by someone other than the recorded counterparty.
|
||||
env(firewall::set(alice.id(), fwID),
|
||||
Sig(sfCounterpartySignature, dave),
|
||||
kSignedFee,
|
||||
Ter(tefBAD_AUTH));
|
||||
env.close();
|
||||
|
||||
// Signed by the counterparty, changing the counterparty.
|
||||
env(firewall::set(alice.id(), fwID),
|
||||
firewall::kCounterparty(dave),
|
||||
Sig(sfCounterpartySignature, carol),
|
||||
kSignedFee);
|
||||
env.close();
|
||||
|
||||
auto const sle = env.le(keylet::firewall(alice.id()));
|
||||
BEAST_EXPECT(sle && sle->getAccountID(sfCounterparty) == dave.id());
|
||||
|
||||
// The old counterparty can no longer authorize.
|
||||
env(firewall::set(alice.id(), fwID),
|
||||
Sig(sfCounterpartySignature, carol),
|
||||
kSignedFee,
|
||||
Ter(tefBAD_AUTH));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testBlockedAndAllowed(FeatureBitset features)
|
||||
{
|
||||
testcase("blocked and allowed");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
Account const backup{"backup"};
|
||||
Account const stranger{"stranger"};
|
||||
env.fund(XRP(10000), alice, carol, backup, stranger);
|
||||
env.close();
|
||||
|
||||
env(firewall::set(alice.id(), carol.id(), backup.id()));
|
||||
env.close();
|
||||
|
||||
// The backup was preauthorized when the firewall was created.
|
||||
env(pay(alice, backup, XRP(10)));
|
||||
env.close();
|
||||
|
||||
// An unauthorized destination is blocked.
|
||||
env(pay(alice, stranger, XRP(10)), Ter(tefFIREWALL_BLOCK));
|
||||
|
||||
// Paths are blocked even to a preauthorized destination, because they
|
||||
// can deliver somewhere the preauthorization check never sees. A
|
||||
// cross-currency send is used so the pathfinder actually sets sfPaths.
|
||||
Account const gw{"gw"};
|
||||
env.fund(XRP(10000), gw);
|
||||
env.close();
|
||||
env.trust(gw["USD"](1000), backup);
|
||||
env.close();
|
||||
env(offer(gw, XRP(100), gw["USD"](100)));
|
||||
env.close();
|
||||
|
||||
env(pay(alice, backup, gw["USD"](10)), Paths(XRP), Ter(tefFIREWALL_BLOCK));
|
||||
|
||||
// A blocked transaction type is rejected whatever its destination.
|
||||
env(offer(alice, XRP(10), alice["USD"](10)), Ter(tefFIREWALL_BLOCK));
|
||||
|
||||
// A transaction the firewall allows still works.
|
||||
env(noop(alice));
|
||||
env.close();
|
||||
|
||||
// An account without a firewall is unaffected.
|
||||
env(pay(stranger, alice, XRP(10)));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testPreauth(FeatureBitset features)
|
||||
{
|
||||
testcase("preauth");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
Account const backup{"backup"};
|
||||
Account const stranger{"stranger"};
|
||||
env.fund(XRP(10000), alice, carol, backup, stranger);
|
||||
env.close();
|
||||
|
||||
env(firewall::set(alice.id(), carol.id(), backup.id()));
|
||||
env.close();
|
||||
auto const fwID = firewallID(alice);
|
||||
|
||||
env(pay(alice, stranger, XRP(10)), Ter(tefFIREWALL_BLOCK));
|
||||
|
||||
// Preauthorizing needs the counterparty's signature.
|
||||
env(firewall::authorize(alice.id(), fwID, stranger.id()), Ter(temMALFORMED));
|
||||
|
||||
env(firewall::authorize(alice.id(), fwID, stranger.id()),
|
||||
Sig(sfCounterpartySignature, carol),
|
||||
kSignedFee);
|
||||
env.close();
|
||||
|
||||
// Now the payment goes through.
|
||||
env(pay(alice, stranger, XRP(10)));
|
||||
env.close();
|
||||
|
||||
// Authorizing the same destination twice fails.
|
||||
env(firewall::authorize(alice.id(), fwID, stranger.id()),
|
||||
Sig(sfCounterpartySignature, carol),
|
||||
kSignedFee,
|
||||
Ter(tecDUPLICATE));
|
||||
|
||||
// An account may not preauthorize itself.
|
||||
env(firewall::authorize(alice.id(), fwID, alice.id()),
|
||||
Sig(sfCounterpartySignature, carol),
|
||||
kSignedFee,
|
||||
Ter(temCANNOT_PREAUTH_SELF));
|
||||
env.close();
|
||||
|
||||
// Removing the preauthorization blocks it again.
|
||||
env(firewall::unauthorize(alice.id(), fwID, stranger.id()),
|
||||
Sig(sfCounterpartySignature, carol),
|
||||
kSignedFee);
|
||||
env.close();
|
||||
|
||||
env(pay(alice, stranger, XRP(10)), Ter(tefFIREWALL_BLOCK));
|
||||
env.close();
|
||||
|
||||
// Removing one that does not exist fails.
|
||||
env(firewall::unauthorize(alice.id(), fwID, stranger.id()),
|
||||
Sig(sfCounterpartySignature, carol),
|
||||
kSignedFee,
|
||||
Ter(tecNO_ENTRY));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testMaxFee(FeatureBitset features)
|
||||
{
|
||||
testcase("max fee");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
Account const backup{"backup"};
|
||||
env.fund(XRP(10000), alice, carol, backup);
|
||||
env.close();
|
||||
|
||||
auto jv = firewall::set(alice.id(), carol.id(), backup.id());
|
||||
jv[sfMaxFee.jsonName] = to_string(XRP(1).value());
|
||||
env(jv);
|
||||
env.close();
|
||||
|
||||
// A fee above the cap is blocked, one at or below it is not.
|
||||
env(noop(alice), Fee(XRP(2)), Ter(tefFIREWALL_BLOCK));
|
||||
env(noop(alice), Fee(XRP(1)));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testAccountDelete(FeatureBitset features)
|
||||
{
|
||||
testcase("account delete");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
Account const backup{"backup"};
|
||||
env.fund(XRP(10000), alice, carol, backup);
|
||||
env.close();
|
||||
|
||||
env(firewall::set(alice.id(), carol.id(), backup.id()));
|
||||
env.close();
|
||||
|
||||
// The ledger sequence must advance far enough for AccountDelete.
|
||||
for (int i = 0; i < 256; ++i)
|
||||
env.close();
|
||||
|
||||
// A firewall is an obligation, so the account cannot be deleted while
|
||||
// one is set.
|
||||
env(acctdelete(alice, backup),
|
||||
Fee(drops(env.current()->fees().increment)),
|
||||
Ter(tecHAS_OBLIGATIONS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testDelete(FeatureBitset features)
|
||||
{
|
||||
testcase("delete");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
Account const backup{"backup"};
|
||||
Account const stranger{"stranger"};
|
||||
env.fund(XRP(10000), alice, carol, backup, stranger);
|
||||
env.close();
|
||||
|
||||
auto const ownersBefore = ownerCount(env, alice);
|
||||
env(firewall::set(alice.id(), carol.id(), backup.id()));
|
||||
env.close();
|
||||
auto const fwID = firewallID(alice);
|
||||
|
||||
env(firewall::authorize(alice.id(), fwID, stranger.id()),
|
||||
Sig(sfCounterpartySignature, carol),
|
||||
kSignedFee);
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == ownersBefore + 3);
|
||||
|
||||
// A delete needs the counterparty's signature.
|
||||
env(firewall::del(alice.id(), fwID), Ter(temMALFORMED));
|
||||
env(firewall::del(alice.id(), fwID),
|
||||
Sig(sfCounterpartySignature, stranger),
|
||||
kSignedFee,
|
||||
Ter(tefBAD_AUTH));
|
||||
env.close();
|
||||
|
||||
env(firewall::del(alice.id(), fwID), Sig(sfCounterpartySignature, carol), kSignedFee);
|
||||
env.close();
|
||||
|
||||
// The firewall and every preauthorization it owned are gone, and the
|
||||
// reserve comes back.
|
||||
BEAST_EXPECT(!hasFirewall(env, alice));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == ownersBefore);
|
||||
|
||||
// Value moves freely again.
|
||||
env(pay(alice, stranger, XRP(10)));
|
||||
env.close();
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace jtx;
|
||||
auto const all = jtx::testableAmendments();
|
||||
testEnabled(all);
|
||||
testCreatePreflight(all);
|
||||
testCreatePreclaim(all);
|
||||
testUpdate(all);
|
||||
testBlockedAndAllowed(all);
|
||||
testPreauth(all);
|
||||
testMaxFee(all);
|
||||
testAccountDelete(all);
|
||||
testDelete(all);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Firewall, app, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -913,6 +913,48 @@ auto const kDestination = JTxFieldWrapper<AccountIdField>(sfDestination);
|
||||
|
||||
} // namespace loan_broker
|
||||
|
||||
/* Firewall */
|
||||
/******************************************************************************/
|
||||
namespace firewall {
|
||||
|
||||
/**
|
||||
* Create a firewall. The counterparty and backup are required on creation.
|
||||
*/
|
||||
json::Value
|
||||
set(AccountID const& account, AccountID const& counterparty, AccountID const& backup);
|
||||
|
||||
/**
|
||||
* Update the firewall named by firewallID. Needs a counterparty signature.
|
||||
*/
|
||||
json::Value
|
||||
set(AccountID const& account, uint256 const& firewallID);
|
||||
|
||||
/**
|
||||
* Delete a firewall. Needs a counterparty signature.
|
||||
*/
|
||||
json::Value
|
||||
del(AccountID const& account, uint256 const& firewallID);
|
||||
|
||||
/**
|
||||
* Preauthorize a destination. Needs a counterparty signature.
|
||||
*/
|
||||
json::Value
|
||||
authorize(AccountID const& account, uint256 const& firewallID, AccountID const& authorized);
|
||||
|
||||
/**
|
||||
* Remove a preauthorized destination. Needs a counterparty signature.
|
||||
*/
|
||||
json::Value
|
||||
unauthorize(AccountID const& account, uint256 const& firewallID, AccountID const& unauthorized);
|
||||
|
||||
auto const kCounterparty = JTxFieldWrapper<AccountIdField>(sfCounterparty);
|
||||
|
||||
auto const kBackup = JTxFieldWrapper<AccountIdField>(sfBackup);
|
||||
|
||||
// For `CounterpartySignature`, use `Sig(sfCounterpartySignature, ...)`
|
||||
|
||||
} // namespace firewall
|
||||
|
||||
/* Loan */
|
||||
/******************************************************************************/
|
||||
namespace loan {
|
||||
|
||||
@@ -815,6 +815,63 @@ coverClawback(AccountID const& account, std::uint32_t flags)
|
||||
|
||||
/* Loan */
|
||||
/******************************************************************************/
|
||||
namespace firewall {
|
||||
|
||||
json::Value
|
||||
set(AccountID const& account, AccountID const& counterparty, AccountID const& backup)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[sfTransactionType] = jss::FirewallSet;
|
||||
jv[sfAccount] = to_string(account);
|
||||
jv[sfCounterparty] = to_string(counterparty);
|
||||
jv[sfBackup] = to_string(backup);
|
||||
return jv;
|
||||
}
|
||||
|
||||
json::Value
|
||||
set(AccountID const& account, uint256 const& firewallID)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[sfTransactionType] = jss::FirewallSet;
|
||||
jv[sfAccount] = to_string(account);
|
||||
jv[sfFirewallID] = to_string(firewallID);
|
||||
return jv;
|
||||
}
|
||||
|
||||
json::Value
|
||||
del(AccountID const& account, uint256 const& firewallID)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[sfTransactionType] = jss::FirewallDelete;
|
||||
jv[sfAccount] = to_string(account);
|
||||
jv[sfFirewallID] = to_string(firewallID);
|
||||
return jv;
|
||||
}
|
||||
|
||||
json::Value
|
||||
authorize(AccountID const& account, uint256 const& firewallID, AccountID const& authorized)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[sfTransactionType] = jss::WithdrawPreauth;
|
||||
jv[sfAccount] = to_string(account);
|
||||
jv[sfFirewallID] = to_string(firewallID);
|
||||
jv[sfAuthorize] = to_string(authorized);
|
||||
return jv;
|
||||
}
|
||||
|
||||
json::Value
|
||||
unauthorize(AccountID const& account, uint256 const& firewallID, AccountID const& unauthorized)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[sfTransactionType] = jss::WithdrawPreauth;
|
||||
jv[sfAccount] = to_string(account);
|
||||
jv[sfFirewallID] = to_string(firewallID);
|
||||
jv[sfUnauthorize] = to_string(unauthorized);
|
||||
return jv;
|
||||
}
|
||||
|
||||
} // namespace firewall
|
||||
|
||||
namespace loan {
|
||||
|
||||
json::Value
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
// Auto-generated unit tests for ledger entry Firewall
|
||||
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <protocol_autogen/TestHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/Firewall.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/Ticket.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(FirewallTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
uint256 const index{1u};
|
||||
|
||||
auto const ownerValue = canonical_ACCOUNT();
|
||||
auto const counterpartyValue = canonical_ACCOUNT();
|
||||
auto const maxFeeValue = canonical_AMOUNT();
|
||||
auto const ownerNodeValue = canonical_UINT64();
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
|
||||
FirewallBuilder builder{
|
||||
ownerValue,
|
||||
counterpartyValue,
|
||||
ownerNodeValue,
|
||||
previousTxnIDValue,
|
||||
previousTxnLgrSeqValue
|
||||
};
|
||||
|
||||
builder.setMaxFee(maxFeeValue);
|
||||
|
||||
builder.setLedgerIndex(index);
|
||||
builder.setFlags(0x1u);
|
||||
|
||||
EXPECT_TRUE(builder.validate());
|
||||
|
||||
auto const entry = builder.build(index);
|
||||
|
||||
EXPECT_TRUE(entry.validate());
|
||||
|
||||
{
|
||||
auto const& expected = ownerValue;
|
||||
auto const actual = entry.getOwner();
|
||||
expectEqualField(expected, actual, "sfOwner");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = counterpartyValue;
|
||||
auto const actual = entry.getCounterparty();
|
||||
expectEqualField(expected, actual, "sfCounterparty");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = ownerNodeValue;
|
||||
auto const actual = entry.getOwnerNode();
|
||||
expectEqualField(expected, actual, "sfOwnerNode");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnIDValue;
|
||||
auto const actual = entry.getPreviousTxnID();
|
||||
expectEqualField(expected, actual, "sfPreviousTxnID");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnLgrSeqValue;
|
||||
auto const actual = entry.getPreviousTxnLgrSeq();
|
||||
expectEqualField(expected, actual, "sfPreviousTxnLgrSeq");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = maxFeeValue;
|
||||
auto const actualOpt = entry.getMaxFee();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfMaxFee");
|
||||
EXPECT_TRUE(entry.hasMaxFee());
|
||||
}
|
||||
|
||||
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(FirewallTests, BuilderFromSleRoundTrip)
|
||||
{
|
||||
uint256 const index{2u};
|
||||
|
||||
auto const ownerValue = canonical_ACCOUNT();
|
||||
auto const counterpartyValue = canonical_ACCOUNT();
|
||||
auto const maxFeeValue = canonical_AMOUNT();
|
||||
auto const ownerNodeValue = canonical_UINT64();
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
|
||||
auto sle = std::make_shared<SLE>(Firewall::entryType, index);
|
||||
|
||||
sle->at(sfOwner) = ownerValue;
|
||||
sle->at(sfCounterparty) = counterpartyValue;
|
||||
sle->at(sfMaxFee) = maxFeeValue;
|
||||
sle->at(sfOwnerNode) = ownerNodeValue;
|
||||
sle->at(sfPreviousTxnID) = previousTxnIDValue;
|
||||
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
|
||||
|
||||
FirewallBuilder builderFromSle{sle};
|
||||
EXPECT_TRUE(builderFromSle.validate());
|
||||
|
||||
auto const entryFromBuilder = builderFromSle.build(index);
|
||||
|
||||
Firewall entryFromSle{sle};
|
||||
EXPECT_TRUE(entryFromBuilder.validate());
|
||||
EXPECT_TRUE(entryFromSle.validate());
|
||||
|
||||
{
|
||||
auto const& expected = ownerValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getOwner();
|
||||
auto const fromBuilder = entryFromBuilder.getOwner();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfOwner");
|
||||
expectEqualField(expected, fromBuilder, "sfOwner");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = counterpartyValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getCounterparty();
|
||||
auto const fromBuilder = entryFromBuilder.getCounterparty();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfCounterparty");
|
||||
expectEqualField(expected, fromBuilder, "sfCounterparty");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = ownerNodeValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getOwnerNode();
|
||||
auto const fromBuilder = entryFromBuilder.getOwnerNode();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfOwnerNode");
|
||||
expectEqualField(expected, fromBuilder, "sfOwnerNode");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnIDValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getPreviousTxnID();
|
||||
auto const fromBuilder = entryFromBuilder.getPreviousTxnID();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfPreviousTxnID");
|
||||
expectEqualField(expected, fromBuilder, "sfPreviousTxnID");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnLgrSeqValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getPreviousTxnLgrSeq();
|
||||
auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq");
|
||||
expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = maxFeeValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getMaxFee();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getMaxFee();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfMaxFee");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfMaxFee");
|
||||
}
|
||||
|
||||
EXPECT_EQ(entryFromSle.getKey(), index);
|
||||
EXPECT_EQ(entryFromBuilder.getKey(), index);
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
|
||||
TEST(FirewallTests, 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
|
||||
TicketBuilder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
auto wrongEntry = wrongBuilder.build(index);
|
||||
|
||||
EXPECT_THROW(Firewall{wrongEntry.getSle()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong ledger entry type.
|
||||
TEST(FirewallTests, BuilderThrowsOnWrongEntryType)
|
||||
{
|
||||
uint256 const index{4u};
|
||||
|
||||
// Build a valid ledger entry of a different type
|
||||
TicketBuilder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
auto wrongEntry = wrongBuilder.build(index);
|
||||
|
||||
EXPECT_THROW(FirewallBuilder{wrongEntry.getSle()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 5) Build with only required fields and verify optional fields return nullopt.
|
||||
TEST(FirewallTests, OptionalFieldsReturnNullopt)
|
||||
{
|
||||
uint256 const index{3u};
|
||||
|
||||
auto const ownerValue = canonical_ACCOUNT();
|
||||
auto const counterpartyValue = canonical_ACCOUNT();
|
||||
auto const ownerNodeValue = canonical_UINT64();
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
|
||||
FirewallBuilder builder{
|
||||
ownerValue,
|
||||
counterpartyValue,
|
||||
ownerNodeValue,
|
||||
previousTxnIDValue,
|
||||
previousTxnLgrSeqValue
|
||||
};
|
||||
|
||||
auto const entry = builder.build(index);
|
||||
|
||||
// Verify optional fields are not present
|
||||
EXPECT_FALSE(entry.hasMaxFee());
|
||||
EXPECT_FALSE(entry.getMaxFee().has_value());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// Auto-generated unit tests for ledger entry WithdrawPreauth
|
||||
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <protocol_autogen/TestHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/WithdrawPreauth.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/Ticket.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(WithdrawPreauthTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
uint256 const index{1u};
|
||||
|
||||
auto const accountValue = canonical_ACCOUNT();
|
||||
auto const authorizeValue = canonical_ACCOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const ownerNodeValue = canonical_UINT64();
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
|
||||
WithdrawPreauthBuilder builder{
|
||||
accountValue,
|
||||
authorizeValue,
|
||||
ownerNodeValue,
|
||||
previousTxnIDValue,
|
||||
previousTxnLgrSeqValue
|
||||
};
|
||||
|
||||
builder.setDestinationTag(destinationTagValue);
|
||||
|
||||
builder.setLedgerIndex(index);
|
||||
builder.setFlags(0x1u);
|
||||
|
||||
EXPECT_TRUE(builder.validate());
|
||||
|
||||
auto const entry = builder.build(index);
|
||||
|
||||
EXPECT_TRUE(entry.validate());
|
||||
|
||||
{
|
||||
auto const& expected = accountValue;
|
||||
auto const actual = entry.getAccount();
|
||||
expectEqualField(expected, actual, "sfAccount");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = authorizeValue;
|
||||
auto const actual = entry.getAuthorize();
|
||||
expectEqualField(expected, actual, "sfAuthorize");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = ownerNodeValue;
|
||||
auto const actual = entry.getOwnerNode();
|
||||
expectEqualField(expected, actual, "sfOwnerNode");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnIDValue;
|
||||
auto const actual = entry.getPreviousTxnID();
|
||||
expectEqualField(expected, actual, "sfPreviousTxnID");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnLgrSeqValue;
|
||||
auto const actual = entry.getPreviousTxnLgrSeq();
|
||||
expectEqualField(expected, actual, "sfPreviousTxnLgrSeq");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = destinationTagValue;
|
||||
auto const actualOpt = entry.getDestinationTag();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfDestinationTag");
|
||||
EXPECT_TRUE(entry.hasDestinationTag());
|
||||
}
|
||||
|
||||
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(WithdrawPreauthTests, BuilderFromSleRoundTrip)
|
||||
{
|
||||
uint256 const index{2u};
|
||||
|
||||
auto const accountValue = canonical_ACCOUNT();
|
||||
auto const authorizeValue = canonical_ACCOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const ownerNodeValue = canonical_UINT64();
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
|
||||
auto sle = std::make_shared<SLE>(WithdrawPreauth::entryType, index);
|
||||
|
||||
sle->at(sfAccount) = accountValue;
|
||||
sle->at(sfAuthorize) = authorizeValue;
|
||||
sle->at(sfDestinationTag) = destinationTagValue;
|
||||
sle->at(sfOwnerNode) = ownerNodeValue;
|
||||
sle->at(sfPreviousTxnID) = previousTxnIDValue;
|
||||
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
|
||||
|
||||
WithdrawPreauthBuilder builderFromSle{sle};
|
||||
EXPECT_TRUE(builderFromSle.validate());
|
||||
|
||||
auto const entryFromBuilder = builderFromSle.build(index);
|
||||
|
||||
WithdrawPreauth entryFromSle{sle};
|
||||
EXPECT_TRUE(entryFromBuilder.validate());
|
||||
EXPECT_TRUE(entryFromSle.validate());
|
||||
|
||||
{
|
||||
auto const& expected = accountValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getAccount();
|
||||
auto const fromBuilder = entryFromBuilder.getAccount();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfAccount");
|
||||
expectEqualField(expected, fromBuilder, "sfAccount");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = authorizeValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getAuthorize();
|
||||
auto const fromBuilder = entryFromBuilder.getAuthorize();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfAuthorize");
|
||||
expectEqualField(expected, fromBuilder, "sfAuthorize");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = ownerNodeValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getOwnerNode();
|
||||
auto const fromBuilder = entryFromBuilder.getOwnerNode();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfOwnerNode");
|
||||
expectEqualField(expected, fromBuilder, "sfOwnerNode");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnIDValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getPreviousTxnID();
|
||||
auto const fromBuilder = entryFromBuilder.getPreviousTxnID();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfPreviousTxnID");
|
||||
expectEqualField(expected, fromBuilder, "sfPreviousTxnID");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnLgrSeqValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getPreviousTxnLgrSeq();
|
||||
auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq");
|
||||
expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = destinationTagValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getDestinationTag();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getDestinationTag();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfDestinationTag");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfDestinationTag");
|
||||
}
|
||||
|
||||
EXPECT_EQ(entryFromSle.getKey(), index);
|
||||
EXPECT_EQ(entryFromBuilder.getKey(), index);
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
|
||||
TEST(WithdrawPreauthTests, 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
|
||||
TicketBuilder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
auto wrongEntry = wrongBuilder.build(index);
|
||||
|
||||
EXPECT_THROW(WithdrawPreauth{wrongEntry.getSle()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong ledger entry type.
|
||||
TEST(WithdrawPreauthTests, BuilderThrowsOnWrongEntryType)
|
||||
{
|
||||
uint256 const index{4u};
|
||||
|
||||
// Build a valid ledger entry of a different type
|
||||
TicketBuilder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
auto wrongEntry = wrongBuilder.build(index);
|
||||
|
||||
EXPECT_THROW(WithdrawPreauthBuilder{wrongEntry.getSle()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 5) Build with only required fields and verify optional fields return nullopt.
|
||||
TEST(WithdrawPreauthTests, OptionalFieldsReturnNullopt)
|
||||
{
|
||||
uint256 const index{3u};
|
||||
|
||||
auto const accountValue = canonical_ACCOUNT();
|
||||
auto const authorizeValue = canonical_ACCOUNT();
|
||||
auto const ownerNodeValue = canonical_UINT64();
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
|
||||
WithdrawPreauthBuilder builder{
|
||||
accountValue,
|
||||
authorizeValue,
|
||||
ownerNodeValue,
|
||||
previousTxnIDValue,
|
||||
previousTxnLgrSeqValue
|
||||
};
|
||||
|
||||
auto const entry = builder.build(index);
|
||||
|
||||
// Verify optional fields are not present
|
||||
EXPECT_FALSE(entry.hasDestinationTag());
|
||||
EXPECT_FALSE(entry.getDestinationTag().has_value());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// Auto-generated unit tests for transaction FirewallDelete
|
||||
|
||||
|
||||
#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/FirewallDelete.h>
|
||||
#include <xrpl/protocol_autogen/transactions/AccountSet.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(TransactionsFirewallDeleteTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testFirewallDelete"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 1;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const counterpartySignatureValue = canonical_OBJECT();
|
||||
auto const firewallIDValue = canonical_UINT256();
|
||||
|
||||
FirewallDeleteBuilder builder{
|
||||
accountValue,
|
||||
counterpartySignatureValue,
|
||||
firewallIDValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
|
||||
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
|
||||
{
|
||||
auto const& expected = counterpartySignatureValue;
|
||||
auto const actual = tx.getCounterpartySignature();
|
||||
expectEqualField(expected, actual, "sfCounterpartySignature");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = firewallIDValue;
|
||||
auto const actual = tx.getFirewallID();
|
||||
expectEqualField(expected, actual, "sfFirewallID");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
// and verify all fields match.
|
||||
TEST(TransactionsFirewallDeleteTests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testFirewallDeleteFromTx"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 2;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const counterpartySignatureValue = canonical_OBJECT();
|
||||
auto const firewallIDValue = canonical_UINT256();
|
||||
|
||||
// Build an initial transaction
|
||||
FirewallDeleteBuilder initialBuilder{
|
||||
accountValue,
|
||||
counterpartySignatureValue,
|
||||
firewallIDValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
FirewallDeleteBuilder 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
|
||||
{
|
||||
auto const& expected = counterpartySignatureValue;
|
||||
auto const actual = rebuiltTx.getCounterpartySignature();
|
||||
expectEqualField(expected, actual, "sfCounterpartySignature");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = firewallIDValue;
|
||||
auto const actual = rebuiltTx.getFirewallID();
|
||||
expectEqualField(expected, actual, "sfFirewallID");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsFirewallDeleteTests, WrapperThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(FirewallDelete{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsFirewallDeleteTests, BuilderThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(FirewallDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
// Auto-generated unit tests for transaction FirewallSet
|
||||
|
||||
|
||||
#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/FirewallSet.h>
|
||||
#include <xrpl/protocol_autogen/transactions/AccountSet.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(TransactionsFirewallSetTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testFirewallSet"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 1;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const counterpartyValue = canonical_ACCOUNT();
|
||||
auto const backupValue = canonical_ACCOUNT();
|
||||
auto const maxFeeValue = canonical_AMOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const counterpartySignatureValue = canonical_OBJECT();
|
||||
auto const firewallIDValue = canonical_UINT256();
|
||||
|
||||
FirewallSetBuilder builder{
|
||||
accountValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
builder.setCounterparty(counterpartyValue);
|
||||
builder.setBackup(backupValue);
|
||||
builder.setMaxFee(maxFeeValue);
|
||||
builder.setDestinationTag(destinationTagValue);
|
||||
builder.setCounterpartySignature(counterpartySignatureValue);
|
||||
builder.setFirewallID(firewallIDValue);
|
||||
|
||||
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
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = counterpartyValue;
|
||||
auto const actualOpt = tx.getCounterparty();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterparty should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfCounterparty");
|
||||
EXPECT_TRUE(tx.hasCounterparty());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = backupValue;
|
||||
auto const actualOpt = tx.getBackup();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBackup should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfBackup");
|
||||
EXPECT_TRUE(tx.hasBackup());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = maxFeeValue;
|
||||
auto const actualOpt = tx.getMaxFee();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMaxFee should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfMaxFee");
|
||||
EXPECT_TRUE(tx.hasMaxFee());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = destinationTagValue;
|
||||
auto const actualOpt = tx.getDestinationTag();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfDestinationTag");
|
||||
EXPECT_TRUE(tx.hasDestinationTag());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = counterpartySignatureValue;
|
||||
auto const actualOpt = tx.getCounterpartySignature();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterpartySignature should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfCounterpartySignature");
|
||||
EXPECT_TRUE(tx.hasCounterpartySignature());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = firewallIDValue;
|
||||
auto const actualOpt = tx.getFirewallID();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFirewallID should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfFirewallID");
|
||||
EXPECT_TRUE(tx.hasFirewallID());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
// and verify all fields match.
|
||||
TEST(TransactionsFirewallSetTests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testFirewallSetFromTx"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 2;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const counterpartyValue = canonical_ACCOUNT();
|
||||
auto const backupValue = canonical_ACCOUNT();
|
||||
auto const maxFeeValue = canonical_AMOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const counterpartySignatureValue = canonical_OBJECT();
|
||||
auto const firewallIDValue = canonical_UINT256();
|
||||
|
||||
// Build an initial transaction
|
||||
FirewallSetBuilder initialBuilder{
|
||||
accountValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
initialBuilder.setCounterparty(counterpartyValue);
|
||||
initialBuilder.setBackup(backupValue);
|
||||
initialBuilder.setMaxFee(maxFeeValue);
|
||||
initialBuilder.setDestinationTag(destinationTagValue);
|
||||
initialBuilder.setCounterpartySignature(counterpartySignatureValue);
|
||||
initialBuilder.setFirewallID(firewallIDValue);
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
FirewallSetBuilder 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
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = counterpartyValue;
|
||||
auto const actualOpt = rebuiltTx.getCounterparty();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterparty should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfCounterparty");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = backupValue;
|
||||
auto const actualOpt = rebuiltTx.getBackup();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBackup should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfBackup");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = maxFeeValue;
|
||||
auto const actualOpt = rebuiltTx.getMaxFee();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMaxFee should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfMaxFee");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = destinationTagValue;
|
||||
auto const actualOpt = rebuiltTx.getDestinationTag();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfDestinationTag");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = counterpartySignatureValue;
|
||||
auto const actualOpt = rebuiltTx.getCounterpartySignature();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterpartySignature should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfCounterpartySignature");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = firewallIDValue;
|
||||
auto const actualOpt = rebuiltTx.getFirewallID();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFirewallID should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfFirewallID");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsFirewallSetTests, WrapperThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(FirewallSet{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsFirewallSetTests, BuilderThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(FirewallSetBuilder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 5) Build with only required fields and verify optional fields return nullopt.
|
||||
TEST(TransactionsFirewallSetTests, OptionalFieldsReturnNullopt)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testFirewallSetNullopt"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 3;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific required field values
|
||||
|
||||
FirewallSetBuilder builder{
|
||||
accountValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Do NOT set optional fields
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
// Verify optional fields are not present
|
||||
EXPECT_FALSE(tx.hasCounterparty());
|
||||
EXPECT_FALSE(tx.getCounterparty().has_value());
|
||||
EXPECT_FALSE(tx.hasBackup());
|
||||
EXPECT_FALSE(tx.getBackup().has_value());
|
||||
EXPECT_FALSE(tx.hasMaxFee());
|
||||
EXPECT_FALSE(tx.getMaxFee().has_value());
|
||||
EXPECT_FALSE(tx.hasDestinationTag());
|
||||
EXPECT_FALSE(tx.getDestinationTag().has_value());
|
||||
EXPECT_FALSE(tx.hasCounterpartySignature());
|
||||
EXPECT_FALSE(tx.getCounterpartySignature().has_value());
|
||||
EXPECT_FALSE(tx.hasFirewallID());
|
||||
EXPECT_FALSE(tx.getFirewallID().has_value());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
// Auto-generated unit tests for transaction WithdrawPreauth
|
||||
|
||||
|
||||
#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/WithdrawPreauth.h>
|
||||
#include <xrpl/protocol_autogen/transactions/AccountSet.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(TransactionsWithdrawPreauthTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWithdrawPreauth"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 1;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const authorizeValue = canonical_ACCOUNT();
|
||||
auto const unauthorizeValue = canonical_ACCOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const counterpartySignatureValue = canonical_OBJECT();
|
||||
auto const firewallIDValue = canonical_UINT256();
|
||||
|
||||
WithdrawPreauthBuilder builder{
|
||||
accountValue,
|
||||
counterpartySignatureValue,
|
||||
firewallIDValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
builder.setAuthorize(authorizeValue);
|
||||
builder.setUnauthorize(unauthorizeValue);
|
||||
builder.setDestinationTag(destinationTagValue);
|
||||
|
||||
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
|
||||
{
|
||||
auto const& expected = counterpartySignatureValue;
|
||||
auto const actual = tx.getCounterpartySignature();
|
||||
expectEqualField(expected, actual, "sfCounterpartySignature");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = firewallIDValue;
|
||||
auto const actual = tx.getFirewallID();
|
||||
expectEqualField(expected, actual, "sfFirewallID");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = authorizeValue;
|
||||
auto const actualOpt = tx.getAuthorize();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuthorize should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfAuthorize");
|
||||
EXPECT_TRUE(tx.hasAuthorize());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = unauthorizeValue;
|
||||
auto const actualOpt = tx.getUnauthorize();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfUnauthorize should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfUnauthorize");
|
||||
EXPECT_TRUE(tx.hasUnauthorize());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = destinationTagValue;
|
||||
auto const actualOpt = tx.getDestinationTag();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfDestinationTag");
|
||||
EXPECT_TRUE(tx.hasDestinationTag());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
// and verify all fields match.
|
||||
TEST(TransactionsWithdrawPreauthTests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWithdrawPreauthFromTx"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 2;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const authorizeValue = canonical_ACCOUNT();
|
||||
auto const unauthorizeValue = canonical_ACCOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const counterpartySignatureValue = canonical_OBJECT();
|
||||
auto const firewallIDValue = canonical_UINT256();
|
||||
|
||||
// Build an initial transaction
|
||||
WithdrawPreauthBuilder initialBuilder{
|
||||
accountValue,
|
||||
counterpartySignatureValue,
|
||||
firewallIDValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
initialBuilder.setAuthorize(authorizeValue);
|
||||
initialBuilder.setUnauthorize(unauthorizeValue);
|
||||
initialBuilder.setDestinationTag(destinationTagValue);
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
WithdrawPreauthBuilder 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
|
||||
{
|
||||
auto const& expected = counterpartySignatureValue;
|
||||
auto const actual = rebuiltTx.getCounterpartySignature();
|
||||
expectEqualField(expected, actual, "sfCounterpartySignature");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = firewallIDValue;
|
||||
auto const actual = rebuiltTx.getFirewallID();
|
||||
expectEqualField(expected, actual, "sfFirewallID");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = authorizeValue;
|
||||
auto const actualOpt = rebuiltTx.getAuthorize();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuthorize should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfAuthorize");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = unauthorizeValue;
|
||||
auto const actualOpt = rebuiltTx.getUnauthorize();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfUnauthorize should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfUnauthorize");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = destinationTagValue;
|
||||
auto const actualOpt = rebuiltTx.getDestinationTag();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfDestinationTag");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsWithdrawPreauthTests, WrapperThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(WithdrawPreauth{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsWithdrawPreauthTests, BuilderThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(WithdrawPreauthBuilder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 5) Build with only required fields and verify optional fields return nullopt.
|
||||
TEST(TransactionsWithdrawPreauthTests, OptionalFieldsReturnNullopt)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWithdrawPreauthNullopt"));
|
||||
|
||||
// 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 counterpartySignatureValue = canonical_OBJECT();
|
||||
auto const firewallIDValue = canonical_UINT256();
|
||||
|
||||
WithdrawPreauthBuilder builder{
|
||||
accountValue,
|
||||
counterpartySignatureValue,
|
||||
firewallIDValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Do NOT set optional fields
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
// Verify optional fields are not present
|
||||
EXPECT_FALSE(tx.hasAuthorize());
|
||||
EXPECT_FALSE(tx.getAuthorize().has_value());
|
||||
EXPECT_FALSE(tx.hasUnauthorize());
|
||||
EXPECT_FALSE(tx.getUnauthorize().has_value());
|
||||
EXPECT_FALSE(tx.hasDestinationTag());
|
||||
EXPECT_FALSE(tx.getDestinationTag().has_value());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -212,6 +212,49 @@ parseCredential(
|
||||
return keylet::credential(*subject, *issuer, Slice(credType->data(), credType->size())).key;
|
||||
}
|
||||
|
||||
static std::expected<uint256, json::Value>
|
||||
parseFirewall(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
// A firewall is keyed by the account it protects, so an account string is
|
||||
// enough to name one.
|
||||
if (auto const account = ledger_entry_helpers::parse<AccountID>(params))
|
||||
return keylet::firewall(*account).key;
|
||||
|
||||
return ledger_entry_helpers::invalidFieldError("malformedAddress", fieldName, "AccountID");
|
||||
}
|
||||
|
||||
static std::expected<uint256, json::Value>
|
||||
parseWithdrawPreauth(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
return parseObjectID(params, fieldName);
|
||||
}
|
||||
|
||||
auto const owner =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
|
||||
if (!owner)
|
||||
return std::unexpected(owner.error());
|
||||
|
||||
auto const authorized =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::authorized, "malformedAuthorized");
|
||||
if (!authorized)
|
||||
return std::unexpected(authorized.error());
|
||||
|
||||
std::uint32_t const dtag =
|
||||
params.isMember(jss::destination_tag) && params[jss::destination_tag].isIntegral()
|
||||
? params[jss::destination_tag].asUInt()
|
||||
: 0;
|
||||
|
||||
return keylet::withdrawPreauth(*owner, *authorized, dtag).key;
|
||||
}
|
||||
|
||||
static std::expected<uint256, json::Value>
|
||||
parseDelegate(
|
||||
json::Value const& params,
|
||||
|
||||
Reference in New Issue
Block a user