diff --git a/cmake/scripts/codegen/generate_tx_classes.py b/cmake/scripts/codegen/generate_tx_classes.py index 09fb8988407..ca0ee21c233 100644 --- a/cmake/scripts/codegen/generate_tx_classes.py +++ b/cmake/scripts/codegen/generate_tx_classes.py @@ -61,6 +61,7 @@ def create_transaction_parser(): "delegable": "Delegation::NotDelegable", "amendment": "uint256{}", "privileges": "Privilege::NoPriv", + "firewall": "FirewallAction::Allow", } diff --git a/include/xrpl/protocol/Firewall.h b/include/xrpl/protocol/Firewall.h new file mode 100644 index 00000000000..0fe5a5f7e2d --- /dev/null +++ b/include/xrpl/protocol/Firewall.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +namespace xrpl { + +/** + * How an account's firewall treats a transaction of the given type. + * + * The classification is declared per transaction in transactions.macro. A type + * the switch does not name, which means a deprecated one, is allowed. + */ +[[nodiscard]] FirewallAction +firewallAction(TxType txType) noexcept; + +} // namespace xrpl diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 0836cffaf73..61d13e01d0f 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -366,6 +366,33 @@ vault(uint256 const& vaultKey) Keylet loanBroker(AccountID const& owner, SeqProxy const& seq) noexcept; +/** + * A firewall, keyed by the account it protects. + */ +Keylet +firewall(AccountID const& account) noexcept; + +inline Keylet +firewall(uint256 const& firewallID) +{ + return {ltFIREWALL, firewallID}; +} + +/** + * A withdraw preauthorization, keyed by owner, authorized account and tag. + */ +Keylet +withdrawPreauth( + AccountID const& owner, + AccountID const& preauthorized, + std::uint32_t dtag) noexcept; + +inline Keylet +withdrawPreauth(uint256 const& key) +{ + return {ltWITHDRAW_PREAUTH, key}; +} + inline Keylet loanBroker(uint256 const& key) { diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 5702b01d1d3..93168c628e3 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -184,6 +184,7 @@ enum TEFcodes : TERUnderlyingType { tefBAD_PATH_COUNT, tefNO_BYTECODE, tefBYTECODE_NOT_INCLUDED, + tefFIREWALL_BLOCK, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxSettings.h b/include/xrpl/protocol/TxSettings.h index 8ea249856a7..63a971eab5a 100644 --- a/include/xrpl/protocol/TxSettings.h +++ b/include/xrpl/protocol/TxSettings.h @@ -10,6 +10,27 @@ namespace xrpl { enum class Delegation { Delegable, NotDelegable }; +/** + * How an account's firewall treats a transaction the account submits. + * + * The classification is per transaction type and is read through + * firewallAction() in . + */ +enum class FirewallAction { + /** + * The firewall inspects the transaction's destination before applying it. + */ + Check, + /** + * The firewall lets the transaction through without inspecting it. + */ + Allow, + /** + * The firewall rejects the transaction while a firewall is set. + */ + Block +}; + /** * Operations a transaction is permitted to perform, as a bitfield. * @@ -91,6 +112,11 @@ struct TxSettings * Operations this transaction is permitted to perform. */ Privilege privileges{Privilege::NoPriv}; + + /** + * How an account's firewall treats this transaction. + */ + FirewallAction firewall{FirewallAction::Allow}; }; } // namespace xrpl diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index e63a7f515dc..8af13dab701 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FEATURE(Firewall, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 18c71b572cd..4d21327dd5e 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -522,6 +522,31 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ // no PermissionedDomainID ever (use MPTIssuance.sfDomainID) })) +/** A ledger object which blocks an account's outgoing value until a + counterparty co-signs, or the destination is preauthorized. + \sa keylet::firewall + */ +LEDGER_ENTRY(ltFIREWALL, 0x0085, Firewall, firewall, ({ + {sfOwner, SoeRequired}, + {sfCounterparty, SoeRequired}, + {sfMaxFee, SoeOptional}, + {sfOwnerNode, SoeRequired}, + {sfPreviousTxnID, SoeRequired}, + {sfPreviousTxnLgrSeq, SoeRequired}, +})) + +/** A ledger object which preauthorizes one destination of a firewalled account. + \sa keylet::withdrawPreauth + */ +LEDGER_ENTRY_DUPLICATE(ltWITHDRAW_PREAUTH, 0x0086, WithdrawPreauth, withdraw_preauth, ({ + {sfAccount, SoeRequired}, + {sfAuthorize, SoeRequired}, + {sfDestinationTag, SoeOptional}, + {sfOwnerNode, SoeRequired}, + {sfPreviousTxnID, SoeRequired}, + {sfPreviousTxnLgrSeq, SoeRequired}, +})) + /** Reserve 0x0084-0x0087 for future Vault-related objects. */ /** A ledger object representing a loan broker diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 2cf35743aea..0b78c3a15cb 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -220,6 +220,7 @@ TYPED_SFIELD(sfLoanID, UINT256, 38) TYPED_SFIELD(sfReferenceHolding, UINT256, 39) TYPED_SFIELD(sfBlindingFactor, UINT256, 40) TYPED_SFIELD(sfObjectID, UINT256, 41) +TYPED_SFIELD(sfFirewallID, UINT256, 42) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -360,6 +361,7 @@ TYPED_SFIELD(sfHighSponsor, ACCOUNT, 28) TYPED_SFIELD(sfLowSponsor, ACCOUNT, 29) TYPED_SFIELD(sfCounterpartySponsor, ACCOUNT, 30) TYPED_SFIELD(sfSponsee, ACCOUNT, 31) +TYPED_SFIELD(sfBackup, ACCOUNT, 32) // vector of 256-bit TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::kSmdNever) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 454aa85ffd0..14584819199 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -47,7 +47,7 @@ # include #endif TRANSACTION(ttPAYMENT, 0, Payment, - ({.delegable = Delegation::Delegable, .privileges = Privilege::CreateAcct | Privilege::MayCreateMpt}), + ({.delegable = Delegation::Delegable, .privileges = Privilege::CreateAcct | Privilege::MayCreateMpt, .firewall = FirewallAction::Check}), ({ {sfDestination, SoeRequired}, {sfAmount, SoeRequired, SoeMptSupported}, @@ -64,7 +64,7 @@ TRANSACTION(ttPAYMENT, 0, Payment, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({.delegable = Delegation::Delegable}), ({ +TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({.delegable = Delegation::Delegable, .firewall = FirewallAction::Check}), ({ {sfDestination, SoeRequired}, {sfDestinationTag, SoeOptional}, {sfAmount, SoeRequired, SoeMptSupported}, @@ -79,7 +79,7 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({.delegable = Delegation::Delegab #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, ({.delegable = Delegation::Delegable}), ({ +TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, ({.delegable = Delegation::Delegable, .firewall = FirewallAction::Check}), ({ {sfOwner, SoeRequired}, {sfOfferSequence, SoeRequired}, {sfFulfillment, SoeOptional}, @@ -112,7 +112,7 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, ({.delegable = Delegation::Delegable}), ({ +TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, ({.delegable = Delegation::Delegable, .firewall = FirewallAction::Check}), ({ {sfOwner, SoeRequired}, {sfOfferSequence, SoeRequired}, })) @@ -134,7 +134,7 @@ TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, # include #endif TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, - ({.delegable = Delegation::Delegable, .privileges = Privilege::MayCreateMpt}), + ({.delegable = Delegation::Delegable, .privileges = Privilege::MayCreateMpt, .firewall = FirewallAction::Block}), ({ {sfTakerPays, SoeRequired, SoeMptSupported}, {sfTakerGets, SoeRequired, SoeMptSupported}, @@ -180,7 +180,7 @@ TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({.delegable = Delegation::Delegable}), ({ +TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({.delegable = Delegation::Delegable, .firewall = FirewallAction::Check}), ({ {sfDestination, SoeRequired}, {sfAmount, SoeRequired}, {sfSettleDelay, SoeRequired}, @@ -193,7 +193,7 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({.delegable = Delegatio #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, ({.delegable = Delegation::Delegable}), ({ +TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, ({.delegable = Delegation::Delegable, .firewall = FirewallAction::Check}), ({ {sfChannel, SoeRequired}, {sfAmount, SoeRequired}, {sfExpiration, SoeOptional}, @@ -216,7 +216,7 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, ({.delegable = Delegation: #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({.delegable = Delegation::Delegable}), ({ +TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({.delegable = Delegation::Delegable, .firewall = FirewallAction::Check}), ({ {sfDestination, SoeRequired}, {sfSendMax, SoeRequired, SoeMptSupported}, {sfExpiration, SoeOptional}, @@ -286,7 +286,7 @@ TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, # include #endif TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, - ({.delegable = Delegation::Delegable, .privileges = Privilege::ChangeNftCounts}), + ({.delegable = Delegation::Delegable, .privileges = Privilege::ChangeNftCounts, .firewall = FirewallAction::Check}), ({ {sfNFTokenTaxon, SoeRequired}, {sfTransferFee, SoeOptional}, @@ -312,7 +312,7 @@ TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, ({.delegable = Delegation::Delegable}), ({ +TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, ({.delegable = Delegation::Delegable, .firewall = FirewallAction::Check}), ({ {sfNFTokenID, SoeRequired}, {sfAmount, SoeRequired}, {sfDestination, SoeOptional}, @@ -370,11 +370,9 @@ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, # include #endif TRANSACTION(ttAMM_CREATE, 35, AMMCreate, - ({ - .delegable = Delegation::Delegable, + ({.delegable = Delegation::Delegable, .amendment = featureAMM, - .privileges = Privilege::CreatePseudoAcct | Privilege::MayCreateMpt, - }), + .privileges = Privilege::CreatePseudoAcct | Privilege::MayCreateMpt, .firewall = FirewallAction::Block}), ({ {sfAmount, SoeRequired, SoeMptSupported}, {sfAmount2, SoeRequired, SoeMptSupported}, @@ -386,7 +384,7 @@ TRANSACTION(ttAMM_CREATE, 35, AMMCreate, # include #endif TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, - ({.delegable = Delegation::Delegable, .amendment = featureAMM}), + ({.delegable = Delegation::Delegable, .amendment = featureAMM, .firewall = FirewallAction::Block}), ({ {sfAsset, SoeRequired, SoeMptSupported}, {sfAsset2, SoeRequired, SoeMptSupported}, @@ -402,11 +400,9 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, # include #endif TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, - ({ - .delegable = Delegation::Delegable, + ({.delegable = Delegation::Delegable, .amendment = featureAMM, - .privileges = Privilege::MayDeleteAcct | Privilege::MayAuthorizeMpt, - }), + .privileges = Privilege::MayDeleteAcct | Privilege::MayAuthorizeMpt, .firewall = FirewallAction::Block}), ({ {sfAsset, SoeRequired, SoeMptSupported}, {sfAsset2, SoeRequired, SoeMptSupported}, @@ -421,7 +417,7 @@ TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, # include #endif TRANSACTION(ttAMM_VOTE, 38, AMMVote, - ({.delegable = Delegation::Delegable, .amendment = featureAMM}), + ({.delegable = Delegation::Delegable, .amendment = featureAMM, .firewall = FirewallAction::Block}), ({ {sfAsset, SoeRequired, SoeMptSupported}, {sfAsset2, SoeRequired, SoeMptSupported}, @@ -433,7 +429,7 @@ TRANSACTION(ttAMM_VOTE, 38, AMMVote, # include #endif TRANSACTION(ttAMM_BID, 39, AMMBid, - ({.delegable = Delegation::Delegable, .amendment = featureAMM}), + ({.delegable = Delegation::Delegable, .amendment = featureAMM, .firewall = FirewallAction::Block}), ({ {sfAsset, SoeRequired, SoeMptSupported}, {sfAsset2, SoeRequired, SoeMptSupported}, @@ -447,11 +443,9 @@ TRANSACTION(ttAMM_BID, 39, AMMBid, # include #endif TRANSACTION(ttAMM_DELETE, 40, AMMDelete, - ({ - .delegable = Delegation::Delegable, + ({.delegable = Delegation::Delegable, .amendment = featureAMM, - .privileges = Privilege::MustDeleteAcct | Privilege::MayDeleteMpt, - }), + .privileges = Privilege::MustDeleteAcct | Privilege::MayDeleteMpt, .firewall = FirewallAction::Block}), ({ {sfAsset, SoeRequired, SoeMptSupported}, {sfAsset2, SoeRequired, SoeMptSupported}, @@ -462,7 +456,7 @@ TRANSACTION(ttAMM_DELETE, 40, AMMDelete, # include #endif TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, - ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge}), + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge, .firewall = FirewallAction::Block}), ({ {sfXChainBridge, SoeRequired}, {sfSignatureReward, SoeRequired}, @@ -471,7 +465,7 @@ TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, /** This transactions initiates a crosschain transaction */ TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, - ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge}), + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge, .firewall = FirewallAction::Block}), ({ {sfXChainBridge, SoeRequired}, {sfXChainClaimID, SoeRequired}, @@ -481,7 +475,7 @@ TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, /** This transaction completes a crosschain transaction */ TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, - ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge}), + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge, .firewall = FirewallAction::Block}), ({ {sfXChainBridge, SoeRequired}, {sfXChainClaimID, SoeRequired}, @@ -492,7 +486,7 @@ TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, /** This transaction initiates a crosschain account create transaction */ TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, - ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge}), + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge, .firewall = FirewallAction::Block}), ({ {sfXChainBridge, SoeRequired}, {sfDestination, SoeRequired}, @@ -502,11 +496,9 @@ TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, /** This transaction adds an attestation to a claim */ TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, - ({ - .delegable = Delegation::Delegable, + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge, - .privileges = Privilege::CreateAcct, - }), + .privileges = Privilege::CreateAcct, .firewall = FirewallAction::Block}), ({ {sfXChainBridge, SoeRequired}, @@ -524,11 +516,9 @@ TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, /** This transaction adds an attestation to an account */ TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateAttestation, - ({ - .delegable = Delegation::Delegable, + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge, - .privileges = Privilege::CreateAcct, - }), + .privileges = Privilege::CreateAcct, .firewall = FirewallAction::Block}), ({ {sfXChainBridge, SoeRequired}, @@ -547,7 +537,7 @@ TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateA /** This transaction modifies a sidechain */ TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, - ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge}), + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge, .firewall = FirewallAction::Block}), ({ {sfXChainBridge, SoeRequired}, {sfSignatureReward, SoeOptional}, @@ -556,7 +546,7 @@ TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, /** This transactions creates a sidechain */ TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, - ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge}), + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge, .firewall = FirewallAction::Block}), ({ {sfXChainBridge, SoeRequired}, {sfSignatureReward, SoeRequired}, @@ -772,11 +762,9 @@ TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, # include #endif TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, - ({ - .amendment = featureSingleAssetVault, + ({.amendment = featureSingleAssetVault, .privileges = Privilege::CreatePseudoAcct | Privilege::CreateMptIssuance | - Privilege::MustModifyVault, - }), + Privilege::MustModifyVault, .firewall = FirewallAction::Block}), ({ {sfAsset, SoeRequired, SoeMptSupported}, {sfAssetsMaximum, SoeOptional}, @@ -795,10 +783,8 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, # include #endif TRANSACTION(ttVAULT_SET, 66, VaultSet, - ({ - .amendment = featureSingleAssetVault, - .privileges = Privilege::MustModifyVault, - }), + ({.amendment = featureSingleAssetVault, + .privileges = Privilege::MustModifyVault, .firewall = FirewallAction::Block}), ({ {sfVaultID, SoeRequired}, {sfAssetsMaximum, SoeOptional}, @@ -811,11 +797,9 @@ TRANSACTION(ttVAULT_SET, 66, VaultSet, # include #endif TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, - ({ - .amendment = featureSingleAssetVault, + ({.amendment = featureSingleAssetVault, .privileges = Privilege::MustDeleteAcct | Privilege::DestroyMptIssuance | - Privilege::MustModifyVault, - }), + Privilege::MustModifyVault, .firewall = FirewallAction::Block}), ({ {sfVaultID, SoeRequired}, {sfMemoData, SoeOptional}, @@ -826,10 +810,8 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, # include #endif TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, - ({ - .amendment = featureSingleAssetVault, - .privileges = Privilege::MayAuthorizeMpt | Privilege::MustModifyVault, - }), + ({.amendment = featureSingleAssetVault, + .privileges = Privilege::MayAuthorizeMpt | Privilege::MustModifyVault, .firewall = FirewallAction::Block}), ({ {sfVaultID, SoeRequired}, {sfAmount, SoeRequired, SoeMptSupported}, @@ -840,11 +822,9 @@ TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, # include #endif TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, - ({ - .amendment = featureSingleAssetVault, + ({.amendment = featureSingleAssetVault, .privileges = Privilege::MayDeleteMpt | Privilege::MayAuthorizeMpt | - Privilege::MustModifyVault, - }), + Privilege::MustModifyVault, .firewall = FirewallAction::Block}), ({ {sfVaultID, SoeRequired}, {sfAmount, SoeRequired, SoeMptSupported}, @@ -858,10 +838,8 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, # include #endif TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, - ({ - .amendment = featureSingleAssetVault, - .privileges = Privilege::MayDeleteMpt | Privilege::MustModifyVault, - }), + ({.amendment = featureSingleAssetVault, + .privileges = Privilege::MayDeleteMpt | Privilege::MustModifyVault, .firewall = FirewallAction::Block}), ({ {sfVaultID, SoeRequired}, {sfHolder, SoeRequired}, @@ -888,10 +866,8 @@ TRANSACTION(ttBATCH, 71, Batch, # include #endif TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet, - ({ - .amendment = featureLendingProtocol, - .privileges = Privilege::CreatePseudoAcct | Privilege::MayAuthorizeMpt, - }), + ({.amendment = featureLendingProtocol, + .privileges = Privilege::CreatePseudoAcct | Privilege::MayAuthorizeMpt, .firewall = FirewallAction::Block}), ({ {sfVaultID, SoeRequired}, {sfLoanBrokerID, SoeOptional}, @@ -907,10 +883,8 @@ TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet, # include #endif TRANSACTION(ttLOAN_BROKER_DELETE, 75, LoanBrokerDelete, - ({ - .amendment = featureLendingProtocol, - .privileges = Privilege::MustDeleteAcct | Privilege::MayAuthorizeMpt, - }), + ({.amendment = featureLendingProtocol, + .privileges = Privilege::MustDeleteAcct | Privilege::MayAuthorizeMpt, .firewall = FirewallAction::Block}), ({ {sfLoanBrokerID, SoeRequired}, })) @@ -920,9 +894,7 @@ TRANSACTION(ttLOAN_BROKER_DELETE, 75, LoanBrokerDelete, # include #endif TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit, - ({ - .amendment = featureLendingProtocol, - }), + ({.amendment = featureLendingProtocol, .firewall = FirewallAction::Block}), ({ {sfLoanBrokerID, SoeRequired}, {sfAmount, SoeRequired, SoeMptSupported}, @@ -933,10 +905,8 @@ TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit, # include #endif TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, - ({ - .amendment = featureLendingProtocol, - .privileges = Privilege::MayAuthorizeMpt, - }), + ({.amendment = featureLendingProtocol, + .privileges = Privilege::MayAuthorizeMpt, .firewall = FirewallAction::Block}), ({ {sfLoanBrokerID, SoeRequired}, {sfAmount, SoeRequired, SoeMptSupported}, @@ -951,9 +921,7 @@ TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, # include #endif TRANSACTION(ttLOAN_BROKER_COVER_CLAWBACK, 78, LoanBrokerCoverClawback, - ({ - .amendment = featureLendingProtocol, - }), + ({.amendment = featureLendingProtocol, .firewall = FirewallAction::Block}), ({ {sfLoanBrokerID, SoeOptional}, {sfAmount, SoeOptional, SoeMptSupported}, @@ -964,10 +932,8 @@ TRANSACTION(ttLOAN_BROKER_COVER_CLAWBACK, 78, LoanBrokerCoverClawback, # include #endif TRANSACTION(ttLOAN_SET, 80, LoanSet, - ({ - .amendment = featureLendingProtocol, - .privileges = Privilege::MayAuthorizeMpt | Privilege::MustModifyVault, - }), + ({.amendment = featureLendingProtocol, + .privileges = Privilege::MayAuthorizeMpt | Privilege::MustModifyVault, .firewall = FirewallAction::Block}), ({ {sfLoanBrokerID, SoeRequired}, {sfData, SoeOptional}, @@ -993,9 +959,7 @@ TRANSACTION(ttLOAN_SET, 80, LoanSet, # include #endif TRANSACTION(ttLOAN_DELETE, 81, LoanDelete, - ({ - .amendment = featureLendingProtocol, - }), + ({.amendment = featureLendingProtocol, .firewall = FirewallAction::Block}), ({ {sfLoanID, SoeRequired}, })) @@ -1005,13 +969,11 @@ TRANSACTION(ttLOAN_DELETE, 81, LoanDelete, # include #endif TRANSACTION(ttLOAN_MANAGE, 82, LoanManage, - ({ - .amendment = featureLendingProtocol, + ({.amendment = featureLendingProtocol, // All of the LoanManage options will modify the vault, but the // transaction can succeed without options, essentially making it // a noop. - .privileges = Privilege::MayModifyVault, - }), + .privileges = Privilege::MayModifyVault, .firewall = FirewallAction::Block}), ({ {sfLoanID, SoeRequired}, })) @@ -1021,10 +983,8 @@ TRANSACTION(ttLOAN_MANAGE, 82, LoanManage, # include #endif TRANSACTION(ttLOAN_PAY, 84, LoanPay, - ({ - .amendment = featureLendingProtocol, - .privileges = Privilege::MayAuthorizeMpt | Privilege::MustModifyVault, - }), + ({.amendment = featureLendingProtocol, + .privileges = Privilege::MayAuthorizeMpt | Privilege::MustModifyVault, .firewall = FirewallAction::Block}), ({ {sfLoanID, SoeRequired}, {sfAmount, SoeRequired, SoeMptSupported}, @@ -1035,9 +995,7 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay, # include #endif TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert, - ({ - .amendment = featureConfidentialTransfer, - }), + ({.amendment = featureConfidentialTransfer, .firewall = FirewallAction::Block}), ({ {sfMPTokenIssuanceID, SoeRequired}, {sfMPTAmount, SoeRequired}, @@ -1054,7 +1012,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert, # include #endif TRANSACTION(ttCONFIDENTIAL_MPT_MERGE_INBOX, 86, ConfidentialMPTMergeInbox, - ({.delegable = Delegation::Delegable, .amendment = featureConfidentialTransfer}), + ({.delegable = Delegation::Delegable, .amendment = featureConfidentialTransfer, .firewall = FirewallAction::Check}), ({ {sfMPTokenIssuanceID, SoeRequired}, })) @@ -1064,7 +1022,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_MERGE_INBOX, 86, ConfidentialMPTMergeInbox, # include #endif TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT_BACK, 87, ConfidentialMPTConvertBack, - ({.delegable = Delegation::Delegable, .amendment = featureConfidentialTransfer}), + ({.delegable = Delegation::Delegable, .amendment = featureConfidentialTransfer, .firewall = FirewallAction::Block}), ({ {sfMPTokenIssuanceID, SoeRequired}, {sfMPTAmount, SoeRequired}, @@ -1080,7 +1038,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT_BACK, 87, ConfidentialMPTConvertBack, # include #endif TRANSACTION(ttCONFIDENTIAL_MPT_SEND, 88, ConfidentialMPTSend, - ({.delegable = Delegation::Delegable, .amendment = featureConfidentialTransfer}), + ({.delegable = Delegation::Delegable, .amendment = featureConfidentialTransfer, .firewall = FirewallAction::Block}), ({ {sfMPTokenIssuanceID, SoeRequired}, {sfDestination, SoeRequired}, @@ -1112,9 +1070,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback, # include #endif TRANSACTION(ttSPONSORSHIP_TRANSFER, 90, SponsorshipTransfer, - ({ - .amendment = featureSponsor, - }), + ({.amendment = featureSponsor, .firewall = FirewallAction::Check}), ({ {sfObjectID, SoeOptional}, {sfSponsee, SoeOptional}, @@ -1181,3 +1137,43 @@ TRANSACTION(ttUNL_MODIFY, 102, UNLModify, {sfLedgerSequence, SoeRequired}, {sfUNLModifyValidator, SoeRequired}, })) + +/** This transaction sets or updates an account's firewall. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttFIREWALL_SET, 104, FirewallSet, + ({.amendment = featureFirewall}), + ({ + {sfCounterparty, SoeOptional}, + {sfBackup, SoeOptional}, + {sfMaxFee, SoeOptional}, + {sfDestinationTag, SoeOptional}, + {sfCounterpartySignature, SoeOptional}, + {sfFirewallID, SoeOptional}, +})) + +/** This transaction deletes an account's firewall. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttFIREWALL_DELETE, 105, FirewallDelete, + ({.amendment = featureFirewall}), + ({ + {sfCounterpartySignature, SoeRequired}, + {sfFirewallID, SoeRequired}, +})) + +/** This transaction preauthorizes a destination for a firewalled account. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttWITHDRAW_PREAUTH, 103, WithdrawPreauth, + ({.amendment = featureFirewall}), + ({ + {sfAuthorize, SoeOptional}, + {sfUnauthorize, SoeOptional}, + {sfDestinationTag, SoeOptional}, + {sfCounterpartySignature, SoeRequired}, + {sfFirewallID, SoeRequired}, +})) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Firewall.h b/include/xrpl/protocol_autogen/ledger_entries/Firewall.h new file mode 100644 index 00000000000..d74061a4cce --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Firewall.h @@ -0,0 +1,252 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::ledger_entries { + +class FirewallBuilder; + +/** + * @brief Ledger Entry: Firewall + * + * Type: ltFIREWALL (0x0085) + * RPC Name: firewall + * + * Immutable wrapper around SLE providing type-safe field access. + * Use FirewallBuilder to construct new ledger entries. + */ +class Firewall : public LedgerEntryBase +{ +public: + static constexpr LedgerEntryType entryType = ltFIREWALL; + + /** + * @brief Construct a Firewall ledger entry wrapper from an existing SLE object. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + explicit Firewall(SLE::const_pointer sle) + : LedgerEntryBase(std::move(sle)) + { + // Verify ledger entry type + if (sle_->getType() != entryType) + { + throw std::runtime_error("Invalid ledger entry type for Firewall"); + } + } + + // Ledger entry-specific field getters + + /** + * @brief Get sfOwner (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getOwner() const + { + return this->sle_->at(sfOwner); + } + + /** + * @brief Get sfCounterparty (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getCounterparty() const + { + return this->sle_->at(sfCounterparty); + } + + /** + * @brief Get sfMaxFee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getMaxFee() const + { + if (hasMaxFee()) + return this->sle_->at(sfMaxFee); + return std::nullopt; + } + + /** + * @brief Check if sfMaxFee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasMaxFee() const + { + return this->sle_->isFieldPresent(sfMaxFee); + } + + /** + * @brief Get sfOwnerNode (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getOwnerNode() const + { + return this->sle_->at(sfOwnerNode); + } + + /** + * @brief Get sfPreviousTxnID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getPreviousTxnID() const + { + return this->sle_->at(sfPreviousTxnID); + } + + /** + * @brief Get sfPreviousTxnLgrSeq (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getPreviousTxnLgrSeq() const + { + return this->sle_->at(sfPreviousTxnLgrSeq); + } +}; + +/** + * @brief Builder for Firewall ledger entries. + * + * Provides a fluent interface for constructing ledger entries with method chaining. + * Uses STObject internally for flexible ledger entry construction. + * Inherits common field setters from LedgerEntryBuilderBase. + */ +class FirewallBuilder : public LedgerEntryBuilderBase +{ +public: + /** + * @brief Construct a new FirewallBuilder with required fields. + * @param owner The sfOwner field value. + * @param counterparty The sfCounterparty field value. + * @param ownerNode The sfOwnerNode field value. + * @param previousTxnID The sfPreviousTxnID field value. + * @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value. + */ + FirewallBuilder(std::decay_t const& owner,std::decay_t const& counterparty,std::decay_t const& ownerNode,std::decay_t const& previousTxnID,std::decay_t const& previousTxnLgrSeq) + : LedgerEntryBuilderBase(ltFIREWALL) + { + setOwner(owner); + setCounterparty(counterparty); + setOwnerNode(ownerNode); + setPreviousTxnID(previousTxnID); + setPreviousTxnLgrSeq(previousTxnLgrSeq); + } + + /** + * @brief Construct a FirewallBuilder from an existing SLE object. + * @param sle The existing ledger entry to copy from. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + FirewallBuilder(SLE::const_pointer sle) + { + if (sle->at(sfLedgerEntryType) != ltFIREWALL) + { + throw std::runtime_error("Invalid ledger entry type for Firewall"); + } + object_ = *sle; + } + + /** + * @brief Ledger entry-specific field setters + */ + + /** + * @brief Set sfOwner (SoeRequired) + * @return Reference to this builder for method chaining. + */ + FirewallBuilder& + setOwner(std::decay_t const& value) + { + object_[sfOwner] = value; + return *this; + } + + /** + * @brief Set sfCounterparty (SoeRequired) + * @return Reference to this builder for method chaining. + */ + FirewallBuilder& + setCounterparty(std::decay_t const& value) + { + object_[sfCounterparty] = value; + return *this; + } + + /** + * @brief Set sfMaxFee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + FirewallBuilder& + setMaxFee(std::decay_t const& value) + { + object_[sfMaxFee] = value; + return *this; + } + + /** + * @brief Set sfOwnerNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + FirewallBuilder& + setOwnerNode(std::decay_t const& value) + { + object_[sfOwnerNode] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + FirewallBuilder& + setPreviousTxnID(std::decay_t const& value) + { + object_[sfPreviousTxnID] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnLgrSeq (SoeRequired) + * @return Reference to this builder for method chaining. + */ + FirewallBuilder& + setPreviousTxnLgrSeq(std::decay_t const& value) + { + object_[sfPreviousTxnLgrSeq] = value; + return *this; + } + + /** + * @brief Build and return the completed Firewall wrapper. + * @param index The ledger entry index. + * @return The constructed ledger entry wrapper. + */ + Firewall + build(uint256 const& index) + { + return Firewall{std::make_shared(std::move(object_), index)}; + } +}; + +} // namespace xrpl::ledger_entries diff --git a/include/xrpl/protocol_autogen/ledger_entries/WithdrawPreauth.h b/include/xrpl/protocol_autogen/ledger_entries/WithdrawPreauth.h new file mode 100644 index 00000000000..1a7f2539637 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/WithdrawPreauth.h @@ -0,0 +1,252 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::ledger_entries { + +class WithdrawPreauthBuilder; + +/** + * @brief Ledger Entry: WithdrawPreauth + * + * Type: ltWITHDRAW_PREAUTH (0x0086) + * RPC Name: withdraw_preauth + * + * Immutable wrapper around SLE providing type-safe field access. + * Use WithdrawPreauthBuilder to construct new ledger entries. + */ +class WithdrawPreauth : public LedgerEntryBase +{ +public: + static constexpr LedgerEntryType entryType = ltWITHDRAW_PREAUTH; + + /** + * @brief Construct a WithdrawPreauth ledger entry wrapper from an existing SLE object. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + explicit WithdrawPreauth(SLE::const_pointer sle) + : LedgerEntryBase(std::move(sle)) + { + // Verify ledger entry type + if (sle_->getType() != entryType) + { + throw std::runtime_error("Invalid ledger entry type for WithdrawPreauth"); + } + } + + // Ledger entry-specific field getters + + /** + * @brief Get sfAccount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getAccount() const + { + return this->sle_->at(sfAccount); + } + + /** + * @brief Get sfAuthorize (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getAuthorize() const + { + return this->sle_->at(sfAuthorize); + } + + /** + * @brief Get sfDestinationTag (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getDestinationTag() const + { + if (hasDestinationTag()) + return this->sle_->at(sfDestinationTag); + return std::nullopt; + } + + /** + * @brief Check if sfDestinationTag is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasDestinationTag() const + { + return this->sle_->isFieldPresent(sfDestinationTag); + } + + /** + * @brief Get sfOwnerNode (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getOwnerNode() const + { + return this->sle_->at(sfOwnerNode); + } + + /** + * @brief Get sfPreviousTxnID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getPreviousTxnID() const + { + return this->sle_->at(sfPreviousTxnID); + } + + /** + * @brief Get sfPreviousTxnLgrSeq (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getPreviousTxnLgrSeq() const + { + return this->sle_->at(sfPreviousTxnLgrSeq); + } +}; + +/** + * @brief Builder for WithdrawPreauth ledger entries. + * + * Provides a fluent interface for constructing ledger entries with method chaining. + * Uses STObject internally for flexible ledger entry construction. + * Inherits common field setters from LedgerEntryBuilderBase. + */ +class WithdrawPreauthBuilder : public LedgerEntryBuilderBase +{ +public: + /** + * @brief Construct a new WithdrawPreauthBuilder with required fields. + * @param account The sfAccount field value. + * @param authorize The sfAuthorize field value. + * @param ownerNode The sfOwnerNode field value. + * @param previousTxnID The sfPreviousTxnID field value. + * @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value. + */ + WithdrawPreauthBuilder(std::decay_t const& account,std::decay_t const& authorize,std::decay_t const& ownerNode,std::decay_t const& previousTxnID,std::decay_t const& previousTxnLgrSeq) + : LedgerEntryBuilderBase(ltWITHDRAW_PREAUTH) + { + setAccount(account); + setAuthorize(authorize); + setOwnerNode(ownerNode); + setPreviousTxnID(previousTxnID); + setPreviousTxnLgrSeq(previousTxnLgrSeq); + } + + /** + * @brief Construct a WithdrawPreauthBuilder from an existing SLE object. + * @param sle The existing ledger entry to copy from. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + WithdrawPreauthBuilder(SLE::const_pointer sle) + { + if (sle->at(sfLedgerEntryType) != ltWITHDRAW_PREAUTH) + { + throw std::runtime_error("Invalid ledger entry type for WithdrawPreauth"); + } + object_ = *sle; + } + + /** + * @brief Ledger entry-specific field setters + */ + + /** + * @brief Set sfAccount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + WithdrawPreauthBuilder& + setAccount(std::decay_t const& value) + { + object_[sfAccount] = value; + return *this; + } + + /** + * @brief Set sfAuthorize (SoeRequired) + * @return Reference to this builder for method chaining. + */ + WithdrawPreauthBuilder& + setAuthorize(std::decay_t const& value) + { + object_[sfAuthorize] = value; + return *this; + } + + /** + * @brief Set sfDestinationTag (SoeOptional) + * @return Reference to this builder for method chaining. + */ + WithdrawPreauthBuilder& + setDestinationTag(std::decay_t const& value) + { + object_[sfDestinationTag] = value; + return *this; + } + + /** + * @brief Set sfOwnerNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + WithdrawPreauthBuilder& + setOwnerNode(std::decay_t const& value) + { + object_[sfOwnerNode] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + WithdrawPreauthBuilder& + setPreviousTxnID(std::decay_t const& value) + { + object_[sfPreviousTxnID] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnLgrSeq (SoeRequired) + * @return Reference to this builder for method chaining. + */ + WithdrawPreauthBuilder& + setPreviousTxnLgrSeq(std::decay_t const& value) + { + object_[sfPreviousTxnLgrSeq] = value; + return *this; + } + + /** + * @brief Build and return the completed WithdrawPreauth wrapper. + * @param index The ledger entry index. + * @return The constructed ledger entry wrapper. + */ + WithdrawPreauth + build(uint256 const& index) + { + return WithdrawPreauth{std::make_shared(std::move(object_), index)}; + } +}; + +} // namespace xrpl::ledger_entries diff --git a/include/xrpl/protocol_autogen/transactions/FirewallDelete.h b/include/xrpl/protocol_autogen/transactions/FirewallDelete.h new file mode 100644 index 00000000000..545ef3f1da0 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/FirewallDelete.h @@ -0,0 +1,155 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class FirewallDeleteBuilder; + +/** + * @brief Transaction: FirewallDelete + * + * Type: ttFIREWALL_DELETE (105) + * Delegable: Delegation::NotDelegable + * Amendment: featureFirewall + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use FirewallDeleteBuilder to construct new transactions. + */ +class FirewallDelete : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttFIREWALL_DELETE; + + /** + * @brief Construct a FirewallDelete transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit FirewallDelete(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for FirewallDelete"); + } + } + + // Transaction-specific field getters + /** + * @brief Get sfCounterpartySignature (SoeRequired) + * @note This is an untyped field. + * @return The field value. + */ + [[nodiscard]] + STObject + getCounterpartySignature() const + { + return this->tx_->getFieldObject(sfCounterpartySignature); + } + + /** + * @brief Get sfFirewallID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getFirewallID() const + { + return this->tx_->at(sfFirewallID); + } +}; + +/** + * @brief Builder for FirewallDelete transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class FirewallDeleteBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new FirewallDeleteBuilder with required fields. + * @param account The account initiating the transaction. + * @param counterpartySignature The sfCounterpartySignature field value. + * @param firewallID The sfFirewallID field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + FirewallDeleteBuilder(SF_ACCOUNT::type::value_type account, + STObject const& counterpartySignature, std::decay_t const& firewallID, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttFIREWALL_DELETE, account, sequence, fee) + { + setCounterpartySignature(counterpartySignature); + setFirewallID(firewallID); + } + + /** + * @brief Construct a FirewallDeleteBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + FirewallDeleteBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttFIREWALL_DELETE) + { + throw std::runtime_error("Invalid transaction type for FirewallDeleteBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfCounterpartySignature (SoeRequired) + * @return Reference to this builder for method chaining. + */ + FirewallDeleteBuilder& + setCounterpartySignature(STObject const& value) + { + object_.setFieldObject(sfCounterpartySignature, value); + return *this; + } + + /** + * @brief Set sfFirewallID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + FirewallDeleteBuilder& + setFirewallID(std::decay_t const& value) + { + object_[sfFirewallID] = value; + return *this; + } + + /** + * @brief Build and return the FirewallDelete wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + FirewallDelete + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return FirewallDelete{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/FirewallSet.h b/include/xrpl/protocol_autogen/transactions/FirewallSet.h new file mode 100644 index 00000000000..6557cc0763e --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/FirewallSet.h @@ -0,0 +1,327 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class FirewallSetBuilder; + +/** + * @brief Transaction: FirewallSet + * + * Type: ttFIREWALL_SET (104) + * Delegable: Delegation::NotDelegable + * Amendment: featureFirewall + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use FirewallSetBuilder to construct new transactions. + */ +class FirewallSet : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttFIREWALL_SET; + + /** + * @brief Construct a FirewallSet transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit FirewallSet(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for FirewallSet"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfCounterparty (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCounterparty() const + { + if (hasCounterparty()) + { + return this->tx_->at(sfCounterparty); + } + return std::nullopt; + } + + /** + * @brief Check if sfCounterparty is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCounterparty() const + { + return this->tx_->isFieldPresent(sfCounterparty); + } + + /** + * @brief Get sfBackup (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getBackup() const + { + if (hasBackup()) + { + return this->tx_->at(sfBackup); + } + return std::nullopt; + } + + /** + * @brief Check if sfBackup is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasBackup() const + { + return this->tx_->isFieldPresent(sfBackup); + } + + /** + * @brief Get sfMaxFee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getMaxFee() const + { + if (hasMaxFee()) + { + return this->tx_->at(sfMaxFee); + } + return std::nullopt; + } + + /** + * @brief Check if sfMaxFee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasMaxFee() const + { + return this->tx_->isFieldPresent(sfMaxFee); + } + + /** + * @brief Get sfDestinationTag (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getDestinationTag() const + { + if (hasDestinationTag()) + { + return this->tx_->at(sfDestinationTag); + } + return std::nullopt; + } + + /** + * @brief Check if sfDestinationTag is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasDestinationTag() const + { + return this->tx_->isFieldPresent(sfDestinationTag); + } + /** + * @brief Get sfCounterpartySignature (SoeOptional) + * @note This is an untyped field. + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional + getCounterpartySignature() const + { + if (this->tx_->isFieldPresent(sfCounterpartySignature)) + return this->tx_->getFieldObject(sfCounterpartySignature); + return std::nullopt; + } + + /** + * @brief Check if sfCounterpartySignature is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCounterpartySignature() const + { + return this->tx_->isFieldPresent(sfCounterpartySignature); + } + + /** + * @brief Get sfFirewallID (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getFirewallID() const + { + if (hasFirewallID()) + { + return this->tx_->at(sfFirewallID); + } + return std::nullopt; + } + + /** + * @brief Check if sfFirewallID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasFirewallID() const + { + return this->tx_->isFieldPresent(sfFirewallID); + } +}; + +/** + * @brief Builder for FirewallSet transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class FirewallSetBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new FirewallSetBuilder with required fields. + * @param account The account initiating the transaction. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + FirewallSetBuilder(SF_ACCOUNT::type::value_type account, + std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttFIREWALL_SET, account, sequence, fee) + { + } + + /** + * @brief Construct a FirewallSetBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + FirewallSetBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttFIREWALL_SET) + { + throw std::runtime_error("Invalid transaction type for FirewallSetBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfCounterparty (SoeOptional) + * @return Reference to this builder for method chaining. + */ + FirewallSetBuilder& + setCounterparty(std::decay_t const& value) + { + object_[sfCounterparty] = value; + return *this; + } + + /** + * @brief Set sfBackup (SoeOptional) + * @return Reference to this builder for method chaining. + */ + FirewallSetBuilder& + setBackup(std::decay_t const& value) + { + object_[sfBackup] = value; + return *this; + } + + /** + * @brief Set sfMaxFee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + FirewallSetBuilder& + setMaxFee(std::decay_t const& value) + { + object_[sfMaxFee] = value; + return *this; + } + + /** + * @brief Set sfDestinationTag (SoeOptional) + * @return Reference to this builder for method chaining. + */ + FirewallSetBuilder& + setDestinationTag(std::decay_t const& value) + { + object_[sfDestinationTag] = value; + return *this; + } + + /** + * @brief Set sfCounterpartySignature (SoeOptional) + * @return Reference to this builder for method chaining. + */ + FirewallSetBuilder& + setCounterpartySignature(STObject const& value) + { + object_.setFieldObject(sfCounterpartySignature, value); + return *this; + } + + /** + * @brief Set sfFirewallID (SoeOptional) + * @return Reference to this builder for method chaining. + */ + FirewallSetBuilder& + setFirewallID(std::decay_t const& value) + { + object_[sfFirewallID] = value; + return *this; + } + + /** + * @brief Build and return the FirewallSet wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + FirewallSet + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return FirewallSet{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/WithdrawPreauth.h b/include/xrpl/protocol_autogen/transactions/WithdrawPreauth.h new file mode 100644 index 00000000000..d951ccb546d --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/WithdrawPreauth.h @@ -0,0 +1,266 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class WithdrawPreauthBuilder; + +/** + * @brief Transaction: WithdrawPreauth + * + * Type: ttWITHDRAW_PREAUTH (103) + * Delegable: Delegation::NotDelegable + * Amendment: featureFirewall + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use WithdrawPreauthBuilder to construct new transactions. + */ +class WithdrawPreauth : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttWITHDRAW_PREAUTH; + + /** + * @brief Construct a WithdrawPreauth transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit WithdrawPreauth(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for WithdrawPreauth"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfAuthorize (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAuthorize() const + { + if (hasAuthorize()) + { + return this->tx_->at(sfAuthorize); + } + return std::nullopt; + } + + /** + * @brief Check if sfAuthorize is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuthorize() const + { + return this->tx_->isFieldPresent(sfAuthorize); + } + + /** + * @brief Get sfUnauthorize (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getUnauthorize() const + { + if (hasUnauthorize()) + { + return this->tx_->at(sfUnauthorize); + } + return std::nullopt; + } + + /** + * @brief Check if sfUnauthorize is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasUnauthorize() const + { + return this->tx_->isFieldPresent(sfUnauthorize); + } + + /** + * @brief Get sfDestinationTag (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getDestinationTag() const + { + if (hasDestinationTag()) + { + return this->tx_->at(sfDestinationTag); + } + return std::nullopt; + } + + /** + * @brief Check if sfDestinationTag is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasDestinationTag() const + { + return this->tx_->isFieldPresent(sfDestinationTag); + } + /** + * @brief Get sfCounterpartySignature (SoeRequired) + * @note This is an untyped field. + * @return The field value. + */ + [[nodiscard]] + STObject + getCounterpartySignature() const + { + return this->tx_->getFieldObject(sfCounterpartySignature); + } + + /** + * @brief Get sfFirewallID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getFirewallID() const + { + return this->tx_->at(sfFirewallID); + } +}; + +/** + * @brief Builder for WithdrawPreauth transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class WithdrawPreauthBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new WithdrawPreauthBuilder with required fields. + * @param account The account initiating the transaction. + * @param counterpartySignature The sfCounterpartySignature field value. + * @param firewallID The sfFirewallID field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + WithdrawPreauthBuilder(SF_ACCOUNT::type::value_type account, + STObject const& counterpartySignature, std::decay_t const& firewallID, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttWITHDRAW_PREAUTH, account, sequence, fee) + { + setCounterpartySignature(counterpartySignature); + setFirewallID(firewallID); + } + + /** + * @brief Construct a WithdrawPreauthBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + WithdrawPreauthBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttWITHDRAW_PREAUTH) + { + throw std::runtime_error("Invalid transaction type for WithdrawPreauthBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfAuthorize (SoeOptional) + * @return Reference to this builder for method chaining. + */ + WithdrawPreauthBuilder& + setAuthorize(std::decay_t const& value) + { + object_[sfAuthorize] = value; + return *this; + } + + /** + * @brief Set sfUnauthorize (SoeOptional) + * @return Reference to this builder for method chaining. + */ + WithdrawPreauthBuilder& + setUnauthorize(std::decay_t const& value) + { + object_[sfUnauthorize] = value; + return *this; + } + + /** + * @brief Set sfDestinationTag (SoeOptional) + * @return Reference to this builder for method chaining. + */ + WithdrawPreauthBuilder& + setDestinationTag(std::decay_t const& value) + { + object_[sfDestinationTag] = value; + return *this; + } + + /** + * @brief Set sfCounterpartySignature (SoeRequired) + * @return Reference to this builder for method chaining. + */ + WithdrawPreauthBuilder& + setCounterpartySignature(STObject const& value) + { + object_.setFieldObject(sfCounterpartySignature, value); + return *this; + } + + /** + * @brief Set sfFirewallID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + WithdrawPreauthBuilder& + setFirewallID(std::decay_t const& value) + { + object_[sfFirewallID] = value; + return *this; + } + + /** + * @brief Build and return the WithdrawPreauth wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + WithdrawPreauth + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return WithdrawPreauth{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index aabde69ff95..31cffaa0c63 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -364,6 +364,15 @@ class Transactor : public TxInvariantCheck static NotTEC checkSponsor(ReadView const& view, STTx const& tx); + /** + * Applies the account's firewall, if it has one, to this transaction. + * + * Returns tefFIREWALL_BLOCK when the firewall rejects it, so a blocked + * transaction is not applied and claims no fee. + */ + static NotTEC + checkFirewall(PreclaimContext const& ctx); + ///////////////////////////////////////////////////// // Interface used by AccountDelete diff --git a/include/xrpl/tx/invariants/FirewallInvariant.h b/include/xrpl/tx/invariants/FirewallInvariant.h new file mode 100644 index 00000000000..6e99311e331 --- /dev/null +++ b/include/xrpl/tx/invariants/FirewallInvariant.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/** + * A firewall keeps its owner and its counterparty. + * + * Every Firewall entry names a counterparty, because without one no update or + * deletion could ever be authorized and the account's value would be stranded. + * The owner is fixed at creation, so a firewall cannot be moved to another + * account, and the counterparty is never the owner, which would let the owner + * authorize its own updates. + */ +class ValidFirewall +{ + // . before is unseated when the entry is being created. + std::vector> firewalls_; + +public: + void + visitEntry(bool, SLE::const_ref, SLE::const_ref); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h index e8dafbd3017..4344e8b0145 100644 --- a/include/xrpl/tx/invariants/InvariantCheck.h +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -448,6 +449,7 @@ using InvariantChecks = std::tuple< NFTokenCountTracking, ValidClawback, ValidMPTIssuance, + ValidFirewall, ValidPermissionedDomain, ValidPermissionedDEX, ValidBookDirectory, diff --git a/include/xrpl/tx/transactors/firewall/FirewallDelete.h b/include/xrpl/tx/transactors/firewall/FirewallDelete.h new file mode 100644 index 00000000000..084fe2508aa --- /dev/null +++ b/include/xrpl/tx/transactors/firewall/FirewallDelete.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * Deletes an account's firewall and every withdraw preauthorization it owns. + * + * The counterparty recorded on the firewall must countersign. + */ +class FirewallDelete : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit FirewallDelete(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/firewall/FirewallSet.h b/include/xrpl/tx/transactors/firewall/FirewallSet.h new file mode 100644 index 00000000000..9f6080767b5 --- /dev/null +++ b/include/xrpl/tx/transactors/firewall/FirewallSet.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/** + * Creates an account's firewall, or updates one that already exists. + * + * A create names the counterparty and the backup destination and needs no + * counterparty signature. An update carries sfFirewallID and must be + * countersigned by the counterparty recorded on the firewall. + */ +class FirewallSet : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit FirewallSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; + +private: + TER + createFirewall(std::shared_ptr const& sleOwner); + + TER + updateFirewall(); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/firewall/WithdrawPreauth.h b/include/xrpl/tx/transactors/firewall/WithdrawPreauth.h new file mode 100644 index 00000000000..04e48672cf2 --- /dev/null +++ b/include/xrpl/tx/transactors/firewall/WithdrawPreauth.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * Authorizes or unauthorizes one destination of a firewalled account. + * + * The counterparty recorded on the account's firewall must countersign, so an + * account whose keys are stolen cannot add a destination on its own. + */ +class WithdrawPreauth : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit WithdrawPreauth(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + /** + * Removes one withdraw preauthorization and returns its reserve. + * + * Also called when the firewall itself is deleted. + */ + static TER + removeFromLedger(ApplyView& view, uint256 const& preauthIndex, beast::Journal j); + + void + visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/src/libxrpl/protocol/Firewall.cpp b/src/libxrpl/protocol/Firewall.cpp new file mode 100644 index 00000000000..f56242a08cd --- /dev/null +++ b/src/libxrpl/protocol/Firewall.cpp @@ -0,0 +1,37 @@ +#include + +#include +#include +#include + +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 + + // Deprecated types + default: + return FirewallAction::Allow; + } +} + +#undef TRANSACTION +#pragma pop_macro("TRANSACTION") +#undef UNWRAP +#pragma pop_macro("UNWRAP") + +} // namespace xrpl diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 91ed5c893fd..cab861f2c97 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -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 diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index c6ebe986424..66cdc5528ff 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -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."), diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 6bf99e567de..c1c1a5b7fae 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -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, diff --git a/src/libxrpl/tx/applySteps.cpp b/src/libxrpl/tx/applySteps.cpp index 2c05c874d37..73f248d1078 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -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; diff --git a/src/libxrpl/tx/invariants/FirewallInvariant.cpp b/src/libxrpl/tx/invariants/FirewallInvariant.cpp new file mode 100644 index 00000000000..c3910b16e35 --- /dev/null +++ b/src/libxrpl/tx/invariants/FirewallInvariant.cpp @@ -0,0 +1,53 @@ +#include + +#include +#include +#include +#include // IWYU pragma: keep +#include + +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 diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp b/src/libxrpl/tx/transactors/account/AccountDelete.cpp index 0936fe26dc3..8163ce5b760 100644 --- a/src/libxrpl/tx/transactors/account/AccountDelete.cpp +++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -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; } diff --git a/src/libxrpl/tx/transactors/firewall/FirewallDelete.cpp b/src/libxrpl/tx/transactors/firewall/FirewallDelete.cpp new file mode 100644 index 00000000000..0244b0be433 --- /dev/null +++ b/src/libxrpl/tx/transactors/firewall/FirewallDelete.cpp @@ -0,0 +1,145 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +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& sleItem) -> std::pair { + 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 diff --git a/src/libxrpl/tx/transactors/firewall/FirewallSet.cpp b/src/libxrpl/tx/transactors/firewall/FirewallSet.cpp new file mode 100644 index 00000000000..282da6ab37b --- /dev/null +++ b/src/libxrpl/tx/transactors/firewall/FirewallSet.cpp @@ -0,0 +1,303 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +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 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(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(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 diff --git a/src/libxrpl/tx/transactors/firewall/WithdrawPreauth.cpp b/src/libxrpl/tx/transactors/firewall/WithdrawPreauth.cpp new file mode 100644 index 00000000000..d9b60c32e81 --- /dev/null +++ b/src/libxrpl/tx/transactors/firewall/WithdrawPreauth.cpp @@ -0,0 +1,223 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +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(optAuth) == static_cast(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(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 diff --git a/src/test/app/Firewall_test.cpp b/src/test/app/Firewall_test.cpp new file mode 100644 index 00000000000..f8700febcb3 --- /dev/null +++ b/src/test/app/Firewall_test.cpp @@ -0,0 +1,428 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +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 diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 801c3627b80..548bdf5ab0a 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -913,6 +913,48 @@ auto const kDestination = JTxFieldWrapper(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(sfCounterparty); + +auto const kBackup = JTxFieldWrapper(sfBackup); + +// For `CounterpartySignature`, use `Sig(sfCounterpartySignature, ...)` + +} // namespace firewall + /* Loan */ /******************************************************************************/ namespace loan { diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 2fa2aebcda5..74de1e9700b 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -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 diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/FirewallTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/FirewallTests.cpp new file mode 100644 index 00000000000..33cc5c0d3e3 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/FirewallTests.cpp @@ -0,0 +1,253 @@ +// Auto-generated unit tests for ledger entry Firewall + + +#include + +#include + +#include +#include +#include + +#include + +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(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()); +} +} diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/WithdrawPreauthTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/WithdrawPreauthTests.cpp new file mode 100644 index 00000000000..c369e5e7da5 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/WithdrawPreauthTests.cpp @@ -0,0 +1,253 @@ +// Auto-generated unit tests for ledger entry WithdrawPreauth + + +#include + +#include + +#include +#include +#include + +#include + +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(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()); +} +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/FirewallDeleteTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/FirewallDeleteTests.cpp new file mode 100644 index 00000000000..df70f656602 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/FirewallDeleteTests.cpp @@ -0,0 +1,162 @@ +// Auto-generated unit tests for transaction FirewallDelete + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +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); +} + + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/FirewallSetTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/FirewallSetTests.cpp new file mode 100644 index 00000000000..84213e40d32 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/FirewallSetTests.cpp @@ -0,0 +1,282 @@ +// Auto-generated unit tests for transaction FirewallSet + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +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()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/WithdrawPreauthTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/WithdrawPreauthTests.cpp new file mode 100644 index 00000000000..66b47560e0c --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/WithdrawPreauthTests.cpp @@ -0,0 +1,255 @@ +// Auto-generated unit tests for transaction WithdrawPreauth + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +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()); +} + +} diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp index 5271720b34f..874923c0800 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp @@ -212,6 +212,49 @@ parseCredential( return keylet::credential(*subject, *issuer, Slice(credType->data(), credType->size())).key; } +static std::expected +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(params)) + return keylet::firewall(*account).key; + + return ledger_entry_helpers::invalidFieldError("malformedAddress", fieldName, "AccountID"); +} + +static std::expected +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 parseDelegate( json::Value const& params,