From 6badf9b5aa3330bf6c8b8c33b47d161df72f7540 Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Sat, 12 Sep 2026 06:35:47 -0400 Subject: [PATCH] featureRepo --- include/xrpl/ledger/helpers/RepoHelpers.h | 85 +++ include/xrpl/protocol/Indexes.h | 12 + include/xrpl/protocol/TER.h | 2 + include/xrpl/protocol/detail/features.macro | 1 + .../xrpl/protocol/detail/ledger_entries.macro | 24 + include/xrpl/protocol/detail/sfields.macro | 4 + .../xrpl/protocol/detail/transactions.macro | 57 ++ .../protocol_autogen/ledger_entries/Repo.h | 525 ++++++++++++++ .../transactions/RepoAccept.h | 131 ++++ .../transactions/RepoCancel.h | 131 ++++ .../protocol_autogen/transactions/RepoClose.h | 131 ++++ .../transactions/RepoCreate.h | 316 +++++++++ .../transactions/RepoDefault.h | 131 ++++ include/xrpl/tx/invariants/InvariantCheck.h | 2 + include/xrpl/tx/invariants/RepoInvariant.h | 44 ++ include/xrpl/tx/transactors/repo/RepoAccept.h | 55 ++ include/xrpl/tx/transactors/repo/RepoCancel.h | 55 ++ include/xrpl/tx/transactors/repo/RepoClose.h | 55 ++ include/xrpl/tx/transactors/repo/RepoCreate.h | 55 ++ .../xrpl/tx/transactors/repo/RepoDefault.h | 55 ++ src/libxrpl/ledger/helpers/RepoHelpers.cpp | 263 +++++++ src/libxrpl/protocol/Indexes.cpp | 7 + src/libxrpl/protocol/TER.cpp | 2 + src/libxrpl/tx/invariants/InvariantCheck.cpp | 8 + src/libxrpl/tx/invariants/RepoInvariant.cpp | 126 ++++ .../tx/transactors/repo/RepoAccept.cpp | 87 +++ .../tx/transactors/repo/RepoCancel.cpp | 76 ++ src/libxrpl/tx/transactors/repo/RepoClose.cpp | 87 +++ .../tx/transactors/repo/RepoCreate.cpp | 240 +++++++ .../tx/transactors/repo/RepoDefault.cpp | 77 ++ src/test/app/Repo_test.cpp | 655 ++++++++++++++++++ .../app/invariants/InvariantsRepo_test.cpp | 239 +++++++ .../ledger_entries/RepoTests.cpp | 488 +++++++++++++ .../transactions/RepoAcceptTests.cpp | 146 ++++ .../transactions/RepoCancelTests.cpp | 146 ++++ .../transactions/RepoCloseTests.cpp | 146 ++++ .../transactions/RepoCreateTests.cpp | 303 ++++++++ .../transactions/RepoDefaultTests.cpp | 146 ++++ src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp | 24 + 39 files changed, 5137 insertions(+) create mode 100644 include/xrpl/ledger/helpers/RepoHelpers.h create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Repo.h create mode 100644 include/xrpl/protocol_autogen/transactions/RepoAccept.h create mode 100644 include/xrpl/protocol_autogen/transactions/RepoCancel.h create mode 100644 include/xrpl/protocol_autogen/transactions/RepoClose.h create mode 100644 include/xrpl/protocol_autogen/transactions/RepoCreate.h create mode 100644 include/xrpl/protocol_autogen/transactions/RepoDefault.h create mode 100644 include/xrpl/tx/invariants/RepoInvariant.h create mode 100644 include/xrpl/tx/transactors/repo/RepoAccept.h create mode 100644 include/xrpl/tx/transactors/repo/RepoCancel.h create mode 100644 include/xrpl/tx/transactors/repo/RepoClose.h create mode 100644 include/xrpl/tx/transactors/repo/RepoCreate.h create mode 100644 include/xrpl/tx/transactors/repo/RepoDefault.h create mode 100644 src/libxrpl/ledger/helpers/RepoHelpers.cpp create mode 100644 src/libxrpl/tx/invariants/RepoInvariant.cpp create mode 100644 src/libxrpl/tx/transactors/repo/RepoAccept.cpp create mode 100644 src/libxrpl/tx/transactors/repo/RepoCancel.cpp create mode 100644 src/libxrpl/tx/transactors/repo/RepoClose.cpp create mode 100644 src/libxrpl/tx/transactors/repo/RepoCreate.cpp create mode 100644 src/libxrpl/tx/transactors/repo/RepoDefault.cpp create mode 100644 src/test/app/Repo_test.cpp create mode 100644 src/test/app/invariants/InvariantsRepo_test.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/ledger_entries/RepoTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/RepoAcceptTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/RepoCancelTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/RepoCloseTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/RepoCreateTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/RepoDefaultTests.cpp diff --git a/include/xrpl/ledger/helpers/RepoHelpers.h b/include/xrpl/ledger/helpers/RepoHelpers.h new file mode 100644 index 00000000000..986ced73dc7 --- /dev/null +++ b/include/xrpl/ledger/helpers/RepoHelpers.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { +namespace repo { + +/** + * Seconds in a year, the denominator of the annualized interest rate. + */ + +/** + * A repo is active once the buyer has accepted it, which is recorded by + * sfStartDate. Before that it is a pending offer. + */ +[[nodiscard]] inline bool +isActive(SLE::const_ref sleRepo) +{ + return sleRepo->isFieldPresent(sfStartDate); +} + +/** + * The amount the seller owes to repurchase the collateral. + * + * PurchasePrice * (1 + InterestRate * elapsed / kSecondsInYear), where elapsed + * runs from StartDate to the close time, capped at MaturityDate so the seller + * never pays for time past maturity. Computed in Number and rounded up, so + * rounding never favours the seller. + */ +[[nodiscard]] STAmount +repurchaseAmount(SLE::const_ref sleRepo, std::uint32_t closeTime); + +/** + * Lock the collateral out of the seller's spendable balance. + * + * XRP is deducted from the account balance by the caller; this handles the + * issued-asset cases the same way an escrow does. + */ +[[nodiscard]] TER +lockCollateral( + ApplyView& view, + AccountID const& issuer, + AccountID const& seller, + STAmount const& amount, + beast::Journal journal); + +/** + * The XLS-85 checks that decide whether collateral may be locked at all. + * + * Both parties are checked, not just the seller: the buyer receives the + * collateral if the repo defaults, so an unauthorized or frozen buyer would + * leave the collateral unable to move at exactly the moment it must. + */ +[[nodiscard]] TER +checkCollateral( + ReadView const& view, + AccountID const& seller, + AccountID const& buyer, + STAmount const& collateral, + beast::Journal journal); + +/** + * Return the locked collateral to an account and remove the entry. + * + * Used by cancel, close and default; they differ only in who receives the + * collateral and whether any cash moved first. + */ +[[nodiscard]] TER +releaseAndDelete( + ApplyViewContext ctx, + SLE::ref sleRepo, + AccountID const& receiver, + beast::Journal journal); + +} // namespace repo +} // namespace xrpl diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 0836cffaf73..75c44a079de 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -366,6 +366,18 @@ vault(uint256 const& vaultKey) Keylet loanBroker(AccountID const& owner, SeqProxy const& seq) noexcept; +/** + * A repurchase agreement, keyed by the seller and the creating sequence. + */ +Keylet +repo(AccountID const& seller, SeqProxy const& seq) noexcept; + +inline Keylet +repo(uint256 const& repoID) +{ + return {ltREPO, repoID}; +} + inline Keylet loanBroker(uint256 const& key) { diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 5702b01d1d3..ea93072c01b 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -377,6 +377,8 @@ enum TECcodes : TERUnderlyingType { tecNO_SPONSOR_PERMISSION = 200, tecOUT_OF_GAS = 201, tecBYTECODE_REJECTED = 202, + tecREPO_ACTIVE = 211, + tecREPO_PENDING = 212, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index e63a7f515dc..89cdf9b9417 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(Repo, 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..271a52339e1 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -522,6 +522,30 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ // no PermissionedDomainID ever (use MPTIssuance.sfDomainID) })) +/** + * A bilateral repurchase agreement. The seller locks collateral and receives + * cash; the entry is pending until the buyer accepts, active thereafter. + * \sa keylet::repo + */ +LEDGER_ENTRY(ltREPO, 0x0095, Repo, repo, ({ + {sfAccount, SoeRequired}, + {sfCounterparty, SoeRequired}, + {sfCollateralAmount, SoeRequired, SoeMptSupported}, + {sfPurchasePrice, SoeRequired, SoeMptSupported}, + {sfInterestRate, SoeRequired}, + {sfExpiration, SoeRequired}, + {sfStartDate, SoeOptional}, + {sfMaturityDate, SoeRequired}, + {sfGracePeriod, SoeRequired}, + {sfTransferRate, SoeOptional}, + {sfData, SoeOptional}, + {sfOwnerNode, SoeRequired}, + {sfDestinationNode, SoeRequired}, + {sfIssuerNode, SoeOptional}, + {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..3a9b9ac666e 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -115,6 +115,7 @@ TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 69) TYPED_SFIELD(sfSponsoredOwnerCount, UINT32, 70) TYPED_SFIELD(sfSponsoringOwnerCount, UINT32, 71) TYPED_SFIELD(sfSponsoringAccountCount, UINT32, 72) +TYPED_SFIELD(sfMaturityDate, UINT32, 95) TYPED_SFIELD(sfRemainingOwnerCount, UINT32, 73) TYPED_SFIELD(sfSponsorFlags, UINT32, 74) TYPED_SFIELD(sfSubscriptionDate, UINT32, 75) @@ -220,6 +221,7 @@ TYPED_SFIELD(sfLoanID, UINT256, 38) TYPED_SFIELD(sfReferenceHolding, UINT256, 39) TYPED_SFIELD(sfBlindingFactor, UINT256, 40) TYPED_SFIELD(sfObjectID, UINT256, 41) +TYPED_SFIELD(sfRepoID, UINT256, 47) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -282,6 +284,8 @@ TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31) TYPED_SFIELD(sfFeeAmount, AMOUNT, 32) TYPED_SFIELD(sfMaxFee, AMOUNT, 33) TYPED_SFIELD(sfFeeAmountDelta, AMOUNT, 34) +TYPED_SFIELD(sfCollateralAmount, AMOUNT, 39) +TYPED_SFIELD(sfPurchasePrice, AMOUNT, 40) // variable length (common) TYPED_SFIELD(sfPublicKey, VL, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 454aa85ffd0..19340329c36 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1181,3 +1181,60 @@ TRANSACTION(ttUNL_MODIFY, 102, UNLModify, {sfLedgerSequence, SoeRequired}, {sfUNLModifyValidator, SoeRequired}, })) + +/** This transaction type offers a repurchase agreement and locks its collateral. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttREPO_CREATE, 114, RepoCreate, + ({.delegable = Delegation::Delegable, .amendment = featureRepo}), + ({ + {sfCounterparty, SoeRequired}, + {sfCollateralAmount, SoeRequired, SoeMptSupported}, + {sfPurchasePrice, SoeRequired, SoeMptSupported}, + {sfInterestRate, SoeRequired}, + {sfExpiration, SoeRequired}, + {sfMaturityDate, SoeRequired}, + {sfGracePeriod, SoeRequired}, + {sfData, SoeOptional}, +})) + +/** This transaction type accepts a pending repo, delivering the cash. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttREPO_ACCEPT, 115, RepoAccept, + ({.delegable = Delegation::Delegable, .amendment = featureRepo}), + ({ + {sfRepoID, SoeRequired}, +})) + +/** This transaction type cancels a pending repo and returns the collateral. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttREPO_CANCEL, 116, RepoCancel, + ({.delegable = Delegation::Delegable, .amendment = featureRepo}), + ({ + {sfRepoID, SoeRequired}, +})) + +/** This transaction type repurchases the collateral, paying principal plus interest. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttREPO_CLOSE, 117, RepoClose, + ({.delegable = Delegation::Delegable, .amendment = featureRepo}), + ({ + {sfRepoID, SoeRequired}, +})) + +/** This transaction type closes out a defaulted repo, delivering the collateral. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttREPO_DEFAULT, 118, RepoDefault, + ({.delegable = Delegation::Delegable, .amendment = featureRepo}), + ({ + {sfRepoID, SoeRequired}, +})) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Repo.h b/include/xrpl/protocol_autogen/ledger_entries/Repo.h new file mode 100644 index 00000000000..a015de57613 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Repo.h @@ -0,0 +1,525 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::ledger_entries { + +class RepoBuilder; + +/** + * @brief Ledger Entry: Repo + * + * Type: ltREPO (0x0095) + * RPC Name: repo + * + * Immutable wrapper around SLE providing type-safe field access. + * Use RepoBuilder to construct new ledger entries. + */ +class Repo : public LedgerEntryBase +{ +public: + static constexpr LedgerEntryType entryType = ltREPO; + + /** + * @brief Construct a Repo ledger entry wrapper from an existing SLE object. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + explicit Repo(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 Repo"); + } + } + + // 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 sfCounterparty (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getCounterparty() const + { + return this->sle_->at(sfCounterparty); + } + + /** + * @brief Get sfCollateralAmount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_AMOUNT::type::value_type + getCollateralAmount() const + { + return this->sle_->at(sfCollateralAmount); + } + + /** + * @brief Get sfPurchasePrice (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_AMOUNT::type::value_type + getPurchasePrice() const + { + return this->sle_->at(sfPurchasePrice); + } + + /** + * @brief Get sfInterestRate (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getInterestRate() const + { + return this->sle_->at(sfInterestRate); + } + + /** + * @brief Get sfExpiration (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getExpiration() const + { + return this->sle_->at(sfExpiration); + } + + /** + * @brief Get sfStartDate (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getStartDate() const + { + if (hasStartDate()) + return this->sle_->at(sfStartDate); + return std::nullopt; + } + + /** + * @brief Check if sfStartDate is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasStartDate() const + { + return this->sle_->isFieldPresent(sfStartDate); + } + + /** + * @brief Get sfMaturityDate (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getMaturityDate() const + { + return this->sle_->at(sfMaturityDate); + } + + /** + * @brief Get sfGracePeriod (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getGracePeriod() const + { + return this->sle_->at(sfGracePeriod); + } + + /** + * @brief Get sfTransferRate (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getTransferRate() const + { + if (hasTransferRate()) + return this->sle_->at(sfTransferRate); + return std::nullopt; + } + + /** + * @brief Check if sfTransferRate is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasTransferRate() const + { + return this->sle_->isFieldPresent(sfTransferRate); + } + + /** + * @brief Get sfData (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getData() const + { + if (hasData()) + return this->sle_->at(sfData); + return std::nullopt; + } + + /** + * @brief Check if sfData is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasData() const + { + return this->sle_->isFieldPresent(sfData); + } + + /** + * @brief Get sfOwnerNode (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getOwnerNode() const + { + return this->sle_->at(sfOwnerNode); + } + + /** + * @brief Get sfDestinationNode (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getDestinationNode() const + { + return this->sle_->at(sfDestinationNode); + } + + /** + * @brief Get sfIssuerNode (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getIssuerNode() const + { + if (hasIssuerNode()) + return this->sle_->at(sfIssuerNode); + return std::nullopt; + } + + /** + * @brief Check if sfIssuerNode is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasIssuerNode() const + { + return this->sle_->isFieldPresent(sfIssuerNode); + } + + /** + * @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 Repo 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 RepoBuilder : public LedgerEntryBuilderBase +{ +public: + /** + * @brief Construct a new RepoBuilder with required fields. + * @param account The sfAccount field value. + * @param counterparty The sfCounterparty field value. + * @param collateralAmount The sfCollateralAmount field value. + * @param purchasePrice The sfPurchasePrice field value. + * @param interestRate The sfInterestRate field value. + * @param expiration The sfExpiration field value. + * @param maturityDate The sfMaturityDate field value. + * @param gracePeriod The sfGracePeriod field value. + * @param ownerNode The sfOwnerNode field value. + * @param destinationNode The sfDestinationNode field value. + * @param previousTxnID The sfPreviousTxnID field value. + * @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value. + */ + RepoBuilder(std::decay_t const& account,std::decay_t const& counterparty,std::decay_t const& collateralAmount,std::decay_t const& purchasePrice,std::decay_t const& interestRate,std::decay_t const& expiration,std::decay_t const& maturityDate,std::decay_t const& gracePeriod,std::decay_t const& ownerNode,std::decay_t const& destinationNode,std::decay_t const& previousTxnID,std::decay_t const& previousTxnLgrSeq) + : LedgerEntryBuilderBase(ltREPO) + { + setAccount(account); + setCounterparty(counterparty); + setCollateralAmount(collateralAmount); + setPurchasePrice(purchasePrice); + setInterestRate(interestRate); + setExpiration(expiration); + setMaturityDate(maturityDate); + setGracePeriod(gracePeriod); + setOwnerNode(ownerNode); + setDestinationNode(destinationNode); + setPreviousTxnID(previousTxnID); + setPreviousTxnLgrSeq(previousTxnLgrSeq); + } + + /** + * @brief Construct a RepoBuilder 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. + */ + RepoBuilder(SLE::const_pointer sle) + { + if (sle->at(sfLedgerEntryType) != ltREPO) + { + throw std::runtime_error("Invalid ledger entry type for Repo"); + } + object_ = *sle; + } + + /** + * @brief Ledger entry-specific field setters + */ + + /** + * @brief Set sfAccount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setAccount(std::decay_t const& value) + { + object_[sfAccount] = value; + return *this; + } + + /** + * @brief Set sfCounterparty (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setCounterparty(std::decay_t const& value) + { + object_[sfCounterparty] = value; + return *this; + } + + /** + * @brief Set sfCollateralAmount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setCollateralAmount(std::decay_t const& value) + { + object_[sfCollateralAmount] = value; + return *this; + } + + /** + * @brief Set sfPurchasePrice (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setPurchasePrice(std::decay_t const& value) + { + object_[sfPurchasePrice] = value; + return *this; + } + + /** + * @brief Set sfInterestRate (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setInterestRate(std::decay_t const& value) + { + object_[sfInterestRate] = value; + return *this; + } + + /** + * @brief Set sfExpiration (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setExpiration(std::decay_t const& value) + { + object_[sfExpiration] = value; + return *this; + } + + /** + * @brief Set sfStartDate (SoeOptional) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setStartDate(std::decay_t const& value) + { + object_[sfStartDate] = value; + return *this; + } + + /** + * @brief Set sfMaturityDate (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setMaturityDate(std::decay_t const& value) + { + object_[sfMaturityDate] = value; + return *this; + } + + /** + * @brief Set sfGracePeriod (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setGracePeriod(std::decay_t const& value) + { + object_[sfGracePeriod] = value; + return *this; + } + + /** + * @brief Set sfTransferRate (SoeOptional) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setTransferRate(std::decay_t const& value) + { + object_[sfTransferRate] = value; + return *this; + } + + /** + * @brief Set sfData (SoeOptional) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setData(std::decay_t const& value) + { + object_[sfData] = value; + return *this; + } + + /** + * @brief Set sfOwnerNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setOwnerNode(std::decay_t const& value) + { + object_[sfOwnerNode] = value; + return *this; + } + + /** + * @brief Set sfDestinationNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setDestinationNode(std::decay_t const& value) + { + object_[sfDestinationNode] = value; + return *this; + } + + /** + * @brief Set sfIssuerNode (SoeOptional) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setIssuerNode(std::decay_t const& value) + { + object_[sfIssuerNode] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setPreviousTxnID(std::decay_t const& value) + { + object_[sfPreviousTxnID] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnLgrSeq (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoBuilder& + setPreviousTxnLgrSeq(std::decay_t const& value) + { + object_[sfPreviousTxnLgrSeq] = value; + return *this; + } + + /** + * @brief Build and return the completed Repo wrapper. + * @param index The ledger entry index. + * @return The constructed ledger entry wrapper. + */ + Repo + build(uint256 const& index) + { + return Repo{std::make_shared(std::move(object_), index)}; + } +}; + +} // namespace xrpl::ledger_entries diff --git a/include/xrpl/protocol_autogen/transactions/RepoAccept.h b/include/xrpl/protocol_autogen/transactions/RepoAccept.h new file mode 100644 index 00000000000..22b0484465c --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/RepoAccept.h @@ -0,0 +1,131 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class RepoAcceptBuilder; + +/** + * @brief Transaction: RepoAccept + * + * Type: ttREPO_ACCEPT (115) + * Delegable: Delegation::Delegable + * Amendment: featureRepo + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use RepoAcceptBuilder to construct new transactions. + */ +class RepoAccept : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttREPO_ACCEPT; + + /** + * @brief Construct a RepoAccept transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit RepoAccept(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for RepoAccept"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfRepoID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getRepoID() const + { + return this->tx_->at(sfRepoID); + } +}; + +/** + * @brief Builder for RepoAccept 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 RepoAcceptBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new RepoAcceptBuilder with required fields. + * @param account The account initiating the transaction. + * @param repoID The sfRepoID field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + RepoAcceptBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& repoID, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttREPO_ACCEPT, account, sequence, fee) + { + setRepoID(repoID); + } + + /** + * @brief Construct a RepoAcceptBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + RepoAcceptBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttREPO_ACCEPT) + { + throw std::runtime_error("Invalid transaction type for RepoAcceptBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfRepoID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoAcceptBuilder& + setRepoID(std::decay_t const& value) + { + object_[sfRepoID] = value; + return *this; + } + + /** + * @brief Build and return the RepoAccept wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + RepoAccept + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return RepoAccept{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/RepoCancel.h b/include/xrpl/protocol_autogen/transactions/RepoCancel.h new file mode 100644 index 00000000000..b0258b078c9 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/RepoCancel.h @@ -0,0 +1,131 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class RepoCancelBuilder; + +/** + * @brief Transaction: RepoCancel + * + * Type: ttREPO_CANCEL (116) + * Delegable: Delegation::Delegable + * Amendment: featureRepo + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use RepoCancelBuilder to construct new transactions. + */ +class RepoCancel : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttREPO_CANCEL; + + /** + * @brief Construct a RepoCancel transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit RepoCancel(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for RepoCancel"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfRepoID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getRepoID() const + { + return this->tx_->at(sfRepoID); + } +}; + +/** + * @brief Builder for RepoCancel 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 RepoCancelBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new RepoCancelBuilder with required fields. + * @param account The account initiating the transaction. + * @param repoID The sfRepoID field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + RepoCancelBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& repoID, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttREPO_CANCEL, account, sequence, fee) + { + setRepoID(repoID); + } + + /** + * @brief Construct a RepoCancelBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + RepoCancelBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttREPO_CANCEL) + { + throw std::runtime_error("Invalid transaction type for RepoCancelBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfRepoID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoCancelBuilder& + setRepoID(std::decay_t const& value) + { + object_[sfRepoID] = value; + return *this; + } + + /** + * @brief Build and return the RepoCancel wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + RepoCancel + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return RepoCancel{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/RepoClose.h b/include/xrpl/protocol_autogen/transactions/RepoClose.h new file mode 100644 index 00000000000..6c182991d38 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/RepoClose.h @@ -0,0 +1,131 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class RepoCloseBuilder; + +/** + * @brief Transaction: RepoClose + * + * Type: ttREPO_CLOSE (117) + * Delegable: Delegation::Delegable + * Amendment: featureRepo + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use RepoCloseBuilder to construct new transactions. + */ +class RepoClose : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttREPO_CLOSE; + + /** + * @brief Construct a RepoClose transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit RepoClose(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for RepoClose"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfRepoID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getRepoID() const + { + return this->tx_->at(sfRepoID); + } +}; + +/** + * @brief Builder for RepoClose 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 RepoCloseBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new RepoCloseBuilder with required fields. + * @param account The account initiating the transaction. + * @param repoID The sfRepoID field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + RepoCloseBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& repoID, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttREPO_CLOSE, account, sequence, fee) + { + setRepoID(repoID); + } + + /** + * @brief Construct a RepoCloseBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + RepoCloseBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttREPO_CLOSE) + { + throw std::runtime_error("Invalid transaction type for RepoCloseBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfRepoID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoCloseBuilder& + setRepoID(std::decay_t const& value) + { + object_[sfRepoID] = value; + return *this; + } + + /** + * @brief Build and return the RepoClose wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + RepoClose + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return RepoClose{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/RepoCreate.h b/include/xrpl/protocol_autogen/transactions/RepoCreate.h new file mode 100644 index 00000000000..33707e92db3 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/RepoCreate.h @@ -0,0 +1,316 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class RepoCreateBuilder; + +/** + * @brief Transaction: RepoCreate + * + * Type: ttREPO_CREATE (114) + * Delegable: Delegation::Delegable + * Amendment: featureRepo + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use RepoCreateBuilder to construct new transactions. + */ +class RepoCreate : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttREPO_CREATE; + + /** + * @brief Construct a RepoCreate transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit RepoCreate(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for RepoCreate"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfCounterparty (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getCounterparty() const + { + return this->tx_->at(sfCounterparty); + } + + /** + * @brief Get sfCollateralAmount (SoeRequired) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return The field value. + */ + [[nodiscard]] + SF_AMOUNT::type::value_type + getCollateralAmount() const + { + return this->tx_->at(sfCollateralAmount); + } + + /** + * @brief Get sfPurchasePrice (SoeRequired) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return The field value. + */ + [[nodiscard]] + SF_AMOUNT::type::value_type + getPurchasePrice() const + { + return this->tx_->at(sfPurchasePrice); + } + + /** + * @brief Get sfInterestRate (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getInterestRate() const + { + return this->tx_->at(sfInterestRate); + } + + /** + * @brief Get sfExpiration (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getExpiration() const + { + return this->tx_->at(sfExpiration); + } + + /** + * @brief Get sfMaturityDate (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getMaturityDate() const + { + return this->tx_->at(sfMaturityDate); + } + + /** + * @brief Get sfGracePeriod (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getGracePeriod() const + { + return this->tx_->at(sfGracePeriod); + } + + /** + * @brief Get sfData (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getData() const + { + if (hasData()) + { + return this->tx_->at(sfData); + } + return std::nullopt; + } + + /** + * @brief Check if sfData is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasData() const + { + return this->tx_->isFieldPresent(sfData); + } +}; + +/** + * @brief Builder for RepoCreate 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 RepoCreateBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new RepoCreateBuilder with required fields. + * @param account The account initiating the transaction. + * @param counterparty The sfCounterparty field value. + * @param collateralAmount The sfCollateralAmount field value. + * @param purchasePrice The sfPurchasePrice field value. + * @param interestRate The sfInterestRate field value. + * @param expiration The sfExpiration field value. + * @param maturityDate The sfMaturityDate field value. + * @param gracePeriod The sfGracePeriod field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + RepoCreateBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& counterparty, std::decay_t const& collateralAmount, std::decay_t const& purchasePrice, std::decay_t const& interestRate, std::decay_t const& expiration, std::decay_t const& maturityDate, std::decay_t const& gracePeriod, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttREPO_CREATE, account, sequence, fee) + { + setCounterparty(counterparty); + setCollateralAmount(collateralAmount); + setPurchasePrice(purchasePrice); + setInterestRate(interestRate); + setExpiration(expiration); + setMaturityDate(maturityDate); + setGracePeriod(gracePeriod); + } + + /** + * @brief Construct a RepoCreateBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + RepoCreateBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttREPO_CREATE) + { + throw std::runtime_error("Invalid transaction type for RepoCreateBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfCounterparty (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoCreateBuilder& + setCounterparty(std::decay_t const& value) + { + object_[sfCounterparty] = value; + return *this; + } + + /** + * @brief Set sfCollateralAmount (SoeRequired) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return Reference to this builder for method chaining. + */ + RepoCreateBuilder& + setCollateralAmount(std::decay_t const& value) + { + object_[sfCollateralAmount] = value; + return *this; + } + + /** + * @brief Set sfPurchasePrice (SoeRequired) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return Reference to this builder for method chaining. + */ + RepoCreateBuilder& + setPurchasePrice(std::decay_t const& value) + { + object_[sfPurchasePrice] = value; + return *this; + } + + /** + * @brief Set sfInterestRate (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoCreateBuilder& + setInterestRate(std::decay_t const& value) + { + object_[sfInterestRate] = value; + return *this; + } + + /** + * @brief Set sfExpiration (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoCreateBuilder& + setExpiration(std::decay_t const& value) + { + object_[sfExpiration] = value; + return *this; + } + + /** + * @brief Set sfMaturityDate (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoCreateBuilder& + setMaturityDate(std::decay_t const& value) + { + object_[sfMaturityDate] = value; + return *this; + } + + /** + * @brief Set sfGracePeriod (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoCreateBuilder& + setGracePeriod(std::decay_t const& value) + { + object_[sfGracePeriod] = value; + return *this; + } + + /** + * @brief Set sfData (SoeOptional) + * @return Reference to this builder for method chaining. + */ + RepoCreateBuilder& + setData(std::decay_t const& value) + { + object_[sfData] = value; + return *this; + } + + /** + * @brief Build and return the RepoCreate wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + RepoCreate + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return RepoCreate{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/RepoDefault.h b/include/xrpl/protocol_autogen/transactions/RepoDefault.h new file mode 100644 index 00000000000..44164d5d54f --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/RepoDefault.h @@ -0,0 +1,131 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class RepoDefaultBuilder; + +/** + * @brief Transaction: RepoDefault + * + * Type: ttREPO_DEFAULT (118) + * Delegable: Delegation::Delegable + * Amendment: featureRepo + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use RepoDefaultBuilder to construct new transactions. + */ +class RepoDefault : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttREPO_DEFAULT; + + /** + * @brief Construct a RepoDefault transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit RepoDefault(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for RepoDefault"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfRepoID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getRepoID() const + { + return this->tx_->at(sfRepoID); + } +}; + +/** + * @brief Builder for RepoDefault 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 RepoDefaultBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new RepoDefaultBuilder with required fields. + * @param account The account initiating the transaction. + * @param repoID The sfRepoID field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + RepoDefaultBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& repoID, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttREPO_DEFAULT, account, sequence, fee) + { + setRepoID(repoID); + } + + /** + * @brief Construct a RepoDefaultBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + RepoDefaultBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttREPO_DEFAULT) + { + throw std::runtime_error("Invalid transaction type for RepoDefaultBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfRepoID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + RepoDefaultBuilder& + setRepoID(std::decay_t const& value) + { + object_[sfRepoID] = value; + return *this; + } + + /** + * @brief Build and return the RepoDefault wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + RepoDefault + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return RepoDefault{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h index e8dafbd3017..94b7b632b86 100644 --- a/include/xrpl/tx/invariants/InvariantCheck.h +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -449,6 +450,7 @@ using InvariantChecks = std::tuple< ValidClawback, ValidMPTIssuance, ValidPermissionedDomain, + ValidRepo, ValidPermissionedDEX, ValidBookDirectory, ValidAMM, diff --git a/include/xrpl/tx/invariants/RepoInvariant.h b/include/xrpl/tx/invariants/RepoInvariant.h new file mode 100644 index 00000000000..1b5176b7e9c --- /dev/null +++ b/include/xrpl/tx/invariants/RepoInvariant.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/** + * A repo's terms are fixed when it is struck, and it only ever moves forward. + * + * The economics of a repurchase agreement are agreed at create and accepted + * against by the buyer. Nothing afterwards may restate them: not the parties, + * not the collateral, not the price, not the rate, and not the dates. A repo + * also moves in one direction only, from pending to active, so the start date + * may appear once and may never change or be removed. + * + * A repo also leaves the ledger by exactly three paths: cancel, close and + * default. Nothing else may delete one. + */ +class ValidRepo +{ + // . before is unseated when the entry is being created. + std::vector> repos_; + + // A repo leaves the ledger by exactly three transactions, so a deletion by + // anything else is a defect wherever it came from. + bool deleted_ = false; + +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/transactors/repo/RepoAccept.h b/include/xrpl/tx/transactors/repo/RepoAccept.h new file mode 100644 index 00000000000..dcf20866dce --- /dev/null +++ b/include/xrpl/tx/transactors/repo/RepoAccept.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * Accepts a pending repo, delivering the purchase price to the seller. + * + * Only the counterparty named at create may accept, and only before Expiration. + * Interest runs from this ledger's close time, not from when the offer was made. + */ +class RepoAccept : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit RepoAccept(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + 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/repo/RepoCancel.h b/include/xrpl/tx/transactors/repo/RepoCancel.h new file mode 100644 index 00000000000..6cbca80fcee --- /dev/null +++ b/include/xrpl/tx/transactors/repo/RepoCancel.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * Cancels a pending repo and returns the collateral to the seller. + * + * The seller may cancel at any time while pending; anyone may cancel once the + * offer has expired. + */ +class RepoCancel : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit RepoCancel(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + 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/repo/RepoClose.h b/include/xrpl/tx/transactors/repo/RepoClose.h new file mode 100644 index 00000000000..a235105debf --- /dev/null +++ b/include/xrpl/tx/transactors/repo/RepoClose.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * Repurchases the collateral by paying principal plus interest. + * + * The collateral unlocks only if the payment lands in the same transaction. A + * failed payment fails the whole transaction and the collateral stays locked. + */ +class RepoClose : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit RepoClose(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + 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/repo/RepoCreate.h b/include/xrpl/tx/transactors/repo/RepoCreate.h new file mode 100644 index 00000000000..047db52022a --- /dev/null +++ b/include/xrpl/tx/transactors/repo/RepoCreate.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * Offers a repurchase agreement and locks the collateral immediately. + * + * The buyer accepts against collateral that is already immobilized, not against + * a promise. The entry is pending until RepoAccept. + */ +class RepoCreate : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit RepoCreate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + 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/repo/RepoDefault.h b/include/xrpl/tx/transactors/repo/RepoDefault.h new file mode 100644 index 00000000000..c038c094837 --- /dev/null +++ b/include/xrpl/tx/transactors/repo/RepoDefault.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * Closes out a repo the seller did not repurchase in time. + * + * Submittable by anyone once the close time passes MaturityDate plus + * GracePeriod. The collateral goes to the buyer; no cash moves. + */ +class RepoDefault : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit RepoDefault(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + 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/src/libxrpl/ledger/helpers/RepoHelpers.cpp b/src/libxrpl/ledger/helpers/RepoHelpers.cpp new file mode 100644 index 00000000000..09ff3dbe861 --- /dev/null +++ b/src/libxrpl/ledger/helpers/RepoHelpers.cpp @@ -0,0 +1,263 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { +namespace repo { + +STAmount +repurchaseAmount(SLE::const_ref sleRepo, std::uint32_t closeTime) +{ + STAmount const price = (*sleRepo)[sfPurchasePrice]; + std::uint32_t const rate = (*sleRepo)[sfInterestRate]; + + // A pending repo has no start date and cannot be closed, so the caller is + // expected to have rejected that already. + std::uint32_t const start = + sleRepo->isFieldPresent(sfStartDate) ? sleRepo->getFieldU32(sfStartDate) : closeTime; + std::uint32_t const maturity = (*sleRepo)[sfMaturityDate]; + + // Time past maturity is not charged: the seller owes interest to maturity + // and no further. + std::uint32_t const end = std::min(closeTime, maturity); + std::uint32_t const elapsed = end > start ? end - start : 0; + + if (rate == 0 || elapsed == 0) + return price; + + // The rate is annualized in 1/10 basis points, matching sfInterestRate on + // ltLOAN, so kTenthBipsPerUnity of it is the whole of the purchase price. + Number const interest = tenthBipsOfValue(Number(price) * Number(elapsed), TenthBips32{rate}) / + Number(kSecondsInYear); + + // Round up, so rounding never favours the seller. + NumberRoundModeGuard const guard(Number::RoundingMode::Upward); + return price + STAmount(price.asset(), interest); +} + +TER +lockCollateral( + ApplyView& view, + AccountID const& issuer, + AccountID const& seller, + STAmount const& amount, + beast::Journal journal) +{ + // The issuer cannot be a party, checked at preflight, so this is defensive. + if (issuer == seller) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (amount.holds()) + return lockEscrowMPT(view, seller, amount, journal); + + return directSendNoFee(view, seller, issuer, amount, true, journal); +} + +namespace { + +TER +checkIouCollateral( + ReadView const& view, + AccountID const& seller, + AccountID const& buyer, + STAmount const& collateral, + beast::Journal journal) +{ + auto const& issue = collateral.get(); + AccountID const issuer = collateral.getIssuer(); + + auto const sleIssuer = view.read(keylet::account(issuer)); + if (!sleIssuer) + return tecNO_ISSUER; + + // The issuer has to have opted in to having its obligations locked. + if (!sleIssuer->isFlag(lsfAllowTrustLineLocking)) + return tecNO_PERMISSION; + + auto const sleLine = view.read(keylet::trustLine(seller, issuer, issue.currency)); + if (!sleLine) + return tecNO_LINE; + + if (auto const ter = requireAuth(view, issue, seller); !isTesSuccess(ter)) + return ter; + + if (auto const ter = requireAuth(view, issue, buyer); !isTesSuccess(ter)) + return ter; + + if (isFrozen(view, seller, issue) || isFrozen(view, buyer, issue)) + return tecFROZEN; + + STAmount const spendable = + accountHolds(view, seller, issue.currency, issuer, FreezeHandling::IgnoreFreeze, journal); + + if (spendable < collateral || spendable <= beast::kZero) + return tecINSUFFICIENT_FUNDS; + + if (!canAdd(spendable, collateral)) + return tecPRECISION_LOSS; + + return tesSUCCESS; +} + +TER +checkMptCollateral( + ReadView const& view, + AccountID const& seller, + AccountID const& buyer, + STAmount const& collateral, + beast::Journal journal) +{ + auto const& mptIssue = collateral.get(); + auto const issuanceKey = keylet::mptokenIssuance(mptIssue.getMptID()); + auto const sleIssuance = view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // The issuance has to permit locking, the same opt-in an escrow needs. + if (!sleIssuance->isFlag(lsfMPTCanEscrow)) + return tecNO_PERMISSION; + + if (!view.exists(keylet::mptoken(issuanceKey.key, seller))) + return tecOBJECT_NOT_FOUND; + + if (auto const ter = requireAuth(view, mptIssue, seller, AuthType::WeakAuth); + !isTesSuccess(ter)) + return ter; + + if (auto const ter = requireAuth(view, mptIssue, buyer, AuthType::WeakAuth); !isTesSuccess(ter)) + return ter; + + if (isFrozen(view, seller, *sleIssuance) || isFrozen(view, buyer, *sleIssuance)) + return tecLOCKED; + + if (auto const ter = canTransfer(view, mptIssue, seller, buyer); !isTesSuccess(ter)) + return ter; + + STAmount const spendable = accountHolds( + view, seller, mptIssue, FreezeHandling::IgnoreFreeze, AuthHandling::IgnoreAuth, journal); + + if (spendable < collateral || spendable <= beast::kZero) + return tecINSUFFICIENT_FUNDS; + + return tesSUCCESS; +} + +} // namespace + +TER +checkCollateral( + ReadView const& view, + AccountID const& seller, + AccountID const& buyer, + STAmount const& collateral, + beast::Journal journal) +{ + if (isXRP(collateral)) + return tesSUCCESS; + + if (collateral.holds()) + return checkMptCollateral(view, seller, buyer, collateral, journal); + + return checkIouCollateral(view, seller, buyer, collateral, journal); +} + +TER +releaseAndDelete( + ApplyViewContext ctx, + SLE::ref sleRepo, + AccountID const& receiver, + beast::Journal journal) +{ + auto& view = ctx.view; + STAmount const collateral = (*sleRepo)[sfCollateralAmount]; + AccountID const seller = (*sleRepo)[sfAccount]; + AccountID const counterparty = (*sleRepo)[sfCounterparty]; + + auto const sleSeller = view.peek(keylet::account(seller)); + if (!sleSeller) + return tefINTERNAL; // LCOV_EXCL_LINE + + // Return the collateral. XRP went out of the balance directly at create, + // so it comes back the same way. + if (isXRP(collateral)) + { + auto const sleReceiver = + receiver == seller ? sleSeller : view.peek(keylet::account(receiver)); + if (!sleReceiver) + return tefINTERNAL; // LCOV_EXCL_LINE + (*sleReceiver)[sfBalance] = (*sleReceiver)[sfBalance] + collateral; + view.update(sleReceiver); + } + else + { + AccountID const issuer = collateral.getIssuer(); + Rate const lockedRate = sleRepo->isFieldPresent(sfTransferRate) + ? Rate{(*sleRepo)[sfTransferRate]} + : kParityRate; + auto const sleReceiver = view.peek(keylet::account(receiver)); + if (!sleReceiver) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const ter = std::visit( + [&](T const&) { + return escrowUnlockApplyHelper( + ctx, + lockedRate, + sleReceiver, + STAmount((*sleReceiver)[sfBalance]).xrp(), + collateral, + issuer, + seller, + receiver, + true, + journal); + }, + collateral.asset().value()); + if (!isTesSuccess(ter)) + return ter; + } + + // Remove from both parties' directories, and the issuer's for an IOU. + if (!view.dirRemove(keylet::ownerDir(seller), (*sleRepo)[sfOwnerNode], sleRepo->key(), false)) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + if (!view.dirRemove( + keylet::ownerDir(counterparty), (*sleRepo)[sfDestinationNode], sleRepo->key(), false)) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + if (sleRepo->isFieldPresent(sfIssuerNode) && + !view.dirRemove( + keylet::ownerDir(collateral.getIssuer()), + (*sleRepo)[sfIssuerNode], + sleRepo->key(), + false)) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + decreaseOwnerCountForObject(view, sleSeller, sleRepo, 1, journal); + view.erase(sleRepo); + + return tesSUCCESS; +} + +} // namespace repo +} // namespace xrpl diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 91ed5c893fd..1b1bb9e923c 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -104,6 +104,7 @@ enum class LedgerNameSpace : std::uint16_t { LoanBroker = 'l', // lower-case L Loan = 'L', Sponsorship = '>', + Repo = 'Z', // No longer used or supported. Left here to reserve the space to avoid accidental reuse. Contract [[deprecated]] = 'c', @@ -610,6 +611,12 @@ permissionedDomain(uint256 const& domainID) noexcept return {ltPERMISSIONED_DOMAIN, domainID}; } +Keylet +repo(AccountID const& seller, SeqProxy const& seq) noexcept +{ + return {ltREPO, indexHash(LedgerNameSpace::Repo, seller, seq.value())}; +} + } // namespace keylet } // namespace xrpl diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index c6ebe986424..7ed82c58dbb 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -110,6 +110,8 @@ transResults() MAKE_ERROR(tecNO_SPONSOR_PERMISSION, "Sponsor has not authorized this transaction."), MAKE_ERROR(tecOUT_OF_GAS, "The WASM code ran out of gas during execution."), MAKE_ERROR(tecBYTECODE_REJECTED, "The custom WASM code that was run rejected your transaction."), + MAKE_ERROR(tecREPO_ACTIVE, "The repo has already been accepted."), + MAKE_ERROR(tecREPO_PENDING, "The repo has not been accepted yet."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index 96820d00bb5..5865bed2f44 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -154,6 +154,10 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref a if (isXRP((*before)[sfAmount])) drops_ -= (*before)[sfAmount].xrp().drops(); break; + case ltREPO: + if (isXRP((*before)[sfCollateralAmount])) + drops_ -= (*before)[sfCollateralAmount].xrp().drops(); + break; case ltSPONSORSHIP: if (before->isFieldPresent(sfFeeAmount)) { @@ -188,6 +192,10 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref a if (!isDelete && isXRP((*after)[sfAmount])) drops_ += (*after)[sfAmount].xrp().drops(); break; + case ltREPO: + if (!isDelete && isXRP((*after)[sfCollateralAmount])) + drops_ += (*after)[sfCollateralAmount].xrp().drops(); + break; case ltSPONSORSHIP: if (!isDelete && after->isFieldPresent(sfFeeAmount)) { diff --git a/src/libxrpl/tx/invariants/RepoInvariant.cpp b/src/libxrpl/tx/invariants/RepoInvariant.cpp new file mode 100644 index 00000000000..e0647e72cc4 --- /dev/null +++ b/src/libxrpl/tx/invariants/RepoInvariant.cpp @@ -0,0 +1,126 @@ +#include + +#include +#include +#include +#include // IWYU pragma: keep +#include +#include +#include + +namespace xrpl { + +void +ValidRepo::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) +{ + if (after && after->getType() == ltREPO) + { + repos_.emplace_back(before, after); + if (isDelete) + deleted_ = true; + } +} + +bool +ValidRepo::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + // A repo leaves the ledger only by cancel, close or default. + if (deleted_) + { + switch (tx.getTxnType()) + { + case ttREPO_CANCEL: + case ttREPO_CLOSE: + case ttREPO_DEFAULT: + break; + default: + JLOG(j.fatal()) << "Invariant failed: a repo was deleted by transaction type " + << tx.getTxnType(); + return false; + } + } + + // A repo cannot exist unless the amendment is enabled, so no separate + // amendment check is needed here. + for (auto const& [before, after] : repos_) + { + // The two parties must be distinct, or the seller would be paying + // itself and no collateral would be at risk. + if (after->getAccountID(sfAccount) == after->getAccountID(sfCounterparty)) + { + JLOG(j.fatal()) << "Invariant failed: a repo's seller is its counterparty"; + return false; + } + + STAmount const collateral = (*after)[sfCollateralAmount]; + STAmount const price = (*after)[sfPurchasePrice]; + + if (collateral <= beast::kZero || price <= beast::kZero) + { + JLOG(j.fatal()) << "Invariant failed: a repo's collateral or price is not positive"; + return false; + } + + // Settling in the collateral asset would let the seller repurchase + // with the very asset that is locked. + if (collateral.asset() == price.asset()) + { + JLOG(j.fatal()) << "Invariant failed: a repo's collateral and cash are the same asset"; + return false; + } + + // The repurchase deadline must fall after the offer stops being + // acceptable, or a repo could mature before it could be accepted. + if ((*after)[sfMaturityDate] <= (*after)[sfExpiration]) + { + JLOG(j.fatal()) << "Invariant failed: a repo matures before it expires"; + return false; + } + + if (!before) + continue; + + // Terms are struck once. Nothing after create may restate them. + auto const sameAccount = [&](SField const& f) { + return before->getAccountID(f) == after->getAccountID(f); + }; + auto const sameU32 = [&](SField const& f) { + return before->getFieldU32(f) == after->getFieldU32(f); + }; + + if (!sameAccount(sfAccount) || !sameAccount(sfCounterparty) || + (*before)[sfCollateralAmount] != collateral || (*before)[sfPurchasePrice] != price || + !sameU32(sfInterestRate) || !sameU32(sfMaturityDate) || !sameU32(sfExpiration) || + !sameU32(sfGracePeriod)) + { + JLOG(j.fatal()) << "Invariant failed: a repo's terms changed after creation"; + return false; + } + + // A repo moves from pending to active and never back. + bool const wasActive = before->isFieldPresent(sfStartDate); + bool const isActiveNow = after->isFieldPresent(sfStartDate); + + if (wasActive && !isActiveNow) + { + JLOG(j.fatal()) << "Invariant failed: an active repo reverted to pending"; + return false; + } + + if (wasActive && isActiveNow && + before->getFieldU32(sfStartDate) != after->getFieldU32(sfStartDate)) + { + JLOG(j.fatal()) << "Invariant failed: a repo's start date changed"; + return false; + } + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/repo/RepoAccept.cpp b/src/libxrpl/tx/transactors/repo/RepoAccept.cpp new file mode 100644 index 00000000000..74897612939 --- /dev/null +++ b/src/libxrpl/tx/transactors/repo/RepoAccept.cpp @@ -0,0 +1,87 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +std::uint32_t +RepoAccept::getFlagsMask(PreflightContext const& ctx) +{ + return tfUniversalMask; +} + +NotTEC +RepoAccept::preflight(PreflightContext const& ctx) +{ + return tesSUCCESS; +} + +TER +RepoAccept::preclaim(PreclaimContext const& ctx) +{ + auto const sleRepo = ctx.view.read(keylet::repo(ctx.tx[sfRepoID])); + if (!sleRepo) + return tecNO_ENTRY; + + if (repo::isActive(sleRepo)) + return tecREPO_ACTIVE; + + // Only the buyer named at create may accept. + if ((*sleRepo)[sfCounterparty] != ctx.tx[sfAccount]) + return tecNO_PERMISSION; + + if (ctx.view.parentCloseTime().time_since_epoch().count() >= (*sleRepo)[sfExpiration]) + return tecEXPIRED; + + return tesSUCCESS; +} + +TER +RepoAccept::doApply() +{ + auto const sleRepo = view().peek(keylet::repo(ctx_.tx[sfRepoID])); + if (!sleRepo) + return tefINTERNAL; // LCOV_EXCL_LINE + + STAmount const price = (*sleRepo)[sfPurchasePrice]; + AccountID const seller = (*sleRepo)[sfAccount]; + + // The cash leg. A failure here fails the accept, leaving the offer pending. + if (auto const ter = accountSend(view(), accountID_, seller, price, j_); !isTesSuccess(ter)) + return ter; + + // Interest runs from when the cash actually moved. + (*sleRepo)[sfStartDate] = view().parentCloseTime().time_since_epoch().count(); + view().update(sleRepo); + + return tesSUCCESS; +} + +void +RepoAccept::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ +} + +bool +RepoAccept::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/repo/RepoCancel.cpp b/src/libxrpl/tx/transactors/repo/RepoCancel.cpp new file mode 100644 index 00000000000..f5efcadc01d --- /dev/null +++ b/src/libxrpl/tx/transactors/repo/RepoCancel.cpp @@ -0,0 +1,76 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +std::uint32_t +RepoCancel::getFlagsMask(PreflightContext const& ctx) +{ + return tfUniversalMask; +} + +NotTEC +RepoCancel::preflight(PreflightContext const& ctx) +{ + return tesSUCCESS; +} + +TER +RepoCancel::preclaim(PreclaimContext const& ctx) +{ + auto const sleRepo = ctx.view.read(keylet::repo(ctx.tx[sfRepoID])); + if (!sleRepo) + return tecNO_ENTRY; + + if (repo::isActive(sleRepo)) + return tecREPO_ACTIVE; + + // The seller may withdraw the offer at any time; anyone may clear it once + // it has expired. + if ((*sleRepo)[sfAccount] != ctx.tx[sfAccount] && + ctx.view.parentCloseTime().time_since_epoch().count() < (*sleRepo)[sfExpiration]) + return tecNO_PERMISSION; + + return tesSUCCESS; +} + +TER +RepoCancel::doApply() +{ + auto const sleRepo = view().peek(keylet::repo(ctx_.tx[sfRepoID])); + if (!sleRepo) + return tefINTERNAL; // LCOV_EXCL_LINE + + // No cash moved, so the collateral simply goes back. + return repo::releaseAndDelete(ctx_.getApplyViewContext(), sleRepo, (*sleRepo)[sfAccount], j_); +} + +void +RepoCancel::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ +} + +bool +RepoCancel::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/repo/RepoClose.cpp b/src/libxrpl/tx/transactors/repo/RepoClose.cpp new file mode 100644 index 00000000000..c6c4398233b --- /dev/null +++ b/src/libxrpl/tx/transactors/repo/RepoClose.cpp @@ -0,0 +1,87 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +std::uint32_t +RepoClose::getFlagsMask(PreflightContext const& ctx) +{ + return tfUniversalMask; +} + +NotTEC +RepoClose::preflight(PreflightContext const& ctx) +{ + return tesSUCCESS; +} + +TER +RepoClose::preclaim(PreclaimContext const& ctx) +{ + auto const sleRepo = ctx.view.read(keylet::repo(ctx.tx[sfRepoID])); + if (!sleRepo) + return tecNO_ENTRY; + + if (!repo::isActive(sleRepo)) + return tecREPO_PENDING; + + // Only the seller repurchases. + if ((*sleRepo)[sfAccount] != ctx.tx[sfAccount]) + return tecNO_PERMISSION; + + auto const closeTime = ctx.view.parentCloseTime().time_since_epoch().count(); + if (closeTime > (*sleRepo)[sfMaturityDate] + (*sleRepo)[sfGracePeriod]) + return tecEXPIRED; + + return tesSUCCESS; +} + +TER +RepoClose::doApply() +{ + auto const sleRepo = view().peek(keylet::repo(ctx_.tx[sfRepoID])); + if (!sleRepo) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const closeTime = view().parentCloseTime().time_since_epoch().count(); + STAmount const owed = repo::repurchaseAmount(sleRepo, closeTime); + AccountID const counterparty = (*sleRepo)[sfCounterparty]; + + // The gate: the collateral is released only because this payment landed. + // If it fails, the whole transaction fails and the collateral stays locked. + if (auto const ter = accountSend(view(), accountID_, counterparty, owed, j_); + !isTesSuccess(ter)) + return ter; + + return repo::releaseAndDelete(ctx_.getApplyViewContext(), sleRepo, accountID_, j_); +} + +void +RepoClose::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ +} + +bool +RepoClose::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/repo/RepoCreate.cpp b/src/libxrpl/tx/transactors/repo/RepoCreate.cpp new file mode 100644 index 00000000000..6c2daf3ac84 --- /dev/null +++ b/src/libxrpl/tx/transactors/repo/RepoCreate.cpp @@ -0,0 +1,240 @@ +#include + +#include +#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 { + +/** + * The Data field carries a reference to the governing legal annex. + */ +static constexpr std::size_t kMaxRepoDataBytes = 256; + +std::uint32_t +RepoCreate::getFlagsMask(PreflightContext const& ctx) +{ + return tfUniversalMask; +} + +NotTEC +RepoCreate::preflight(PreflightContext const& ctx) +{ + AccountID const account = ctx.tx[sfAccount]; + AccountID const counterparty = ctx.tx[sfCounterparty]; + + if (account == counterparty) + { + JLOG(ctx.j.trace()) << "RepoCreate: the counterparty is the account"; + return temMALFORMED; + } + + STAmount const collateral = ctx.tx[sfCollateralAmount]; + STAmount const price = ctx.tx[sfPurchasePrice]; + + if (collateral <= beast::kZero || !isLegalNet(collateral)) + { + JLOG(ctx.j.trace()) << "RepoCreate: the collateral amount is not positive"; + return temBAD_AMOUNT; + } + + if (price <= beast::kZero || !isLegalNet(price)) + { + JLOG(ctx.j.trace()) << "RepoCreate: the purchase price is not positive"; + return temBAD_AMOUNT; + } + + // Settling the repurchase in the collateral itself would let the seller + // repay with the very asset that is locked. + if (collateral.asset() == price.asset()) + { + JLOG(ctx.j.trace()) << "RepoCreate: the collateral and the cash are the same asset"; + return temBAD_AMOUNT; + } + + // The repurchase deadline has to fall after the offer stops being + // acceptable, or the repo could mature before it could be accepted. + if (ctx.tx[sfMaturityDate] <= ctx.tx[sfExpiration]) + { + JLOG(ctx.j.trace()) << "RepoCreate: maturity is not after expiration"; + return temBAD_EXPIRATION; + } + + // The same ceiling LoanSet puts on sfInterestRate. + if (ctx.tx[sfInterestRate] > lending::kMaxInterestRate.value()) + { + JLOG(ctx.j.trace()) << "RepoCreate: the interest rate is above the maximum"; + return temBAD_AMOUNT; + } + + if (auto const data = ctx.tx[~sfData]; data && data->size() > kMaxRepoDataBytes) + { + JLOG(ctx.j.trace()) << "RepoCreate: sfData is too large"; + return temMALFORMED; + } + + return tesSUCCESS; +} + +TER +RepoCreate::preclaim(PreclaimContext const& ctx) +{ + AccountID const account = ctx.tx[sfAccount]; + AccountID const counterparty = ctx.tx[sfCounterparty]; + + if (!ctx.view.exists(keylet::account(counterparty))) + { + JLOG(ctx.j.trace()) << "RepoCreate: the counterparty account does not exist"; + return tecNO_DST; + } + + STAmount const collateral = ctx.tx[sfCollateralAmount]; + if (!isXRP(collateral)) + { + // An issuer holding its own obligation as collateral has nothing + // locked, so neither party may be the issuer. + AccountID const issuer = collateral.getIssuer(); + if (issuer == account || issuer == counterparty) + { + JLOG(ctx.j.trace()) << "RepoCreate: the collateral issuer is a party"; + return tecNO_PERMISSION; + } + + if (!ctx.view.exists(keylet::account(issuer))) + return tecNO_ISSUER; + } + + // The XLS-85 table: the asset must be lockable, both parties authorized + // and unfrozen, and the seller actually holding enough to lock. + if (auto const ter = repo::checkCollateral(ctx.view, account, counterparty, collateral, ctx.j); + !isTesSuccess(ter)) + return ter; + + return tesSUCCESS; +} + +TER +RepoCreate::doApply() +{ + auto applyViewContext = ctx_.getApplyViewContext(); + + auto const sle = view().peek(keylet::account(accountID_)); + if (!sle) + return tefINTERNAL; // LCOV_EXCL_LINE + + STAmount const collateral = ctx_.tx[sfCollateralAmount]; + AccountID const counterparty = ctx_.tx[sfCounterparty]; + + if (auto const ret = + checkReserve(applyViewContext, sle, preFeeBalance_, {.ownerCountDelta = 1}, j_); + !isTesSuccess(ret)) + return ret; + + // Locking XRP must leave the seller above its own reserve floor. + if (isXRP(collateral)) + { + auto const reserve = accountReserve(view(), sle, j_, {.ownerCountDelta = 1}); + if (preFeeBalance_ - collateral.xrp() < reserve) + return tecUNFUNDED; + } + + Keylet const repoKeylet = keylet::repo(accountID_, ctx_.tx.getSeqProxy()); + auto const sleRepo = std::make_shared(repoKeylet); + (*sleRepo)[sfAccount] = accountID_; + (*sleRepo)[sfCounterparty] = counterparty; + (*sleRepo)[sfCollateralAmount] = collateral; + (*sleRepo)[sfPurchasePrice] = ctx_.tx[sfPurchasePrice]; + (*sleRepo)[sfInterestRate] = ctx_.tx[sfInterestRate]; + (*sleRepo)[sfExpiration] = ctx_.tx[sfExpiration]; + (*sleRepo)[sfMaturityDate] = ctx_.tx[sfMaturityDate]; + (*sleRepo)[sfGracePeriod] = ctx_.tx[sfGracePeriod]; + (*sleRepo)[~sfData] = ctx_.tx[~sfData]; + + // The issuer's rate is captured now so a later change cannot alter the + // economics of a trade already struck. + if (!isXRP(collateral)) + { + auto const xferRate = transferRate(view(), collateral); + if (xferRate != kParityRate) + (*sleRepo)[sfTransferRate] = xferRate.value; + } + + view().insert(sleRepo); + + if (auto const page = view().dirInsert( + keylet::ownerDir(accountID_), repoKeylet, describeOwnerDir(accountID_))) + (*sleRepo)[sfOwnerNode] = *page; + else + return tecDIR_FULL; // LCOV_EXCL_LINE + + if (auto const page = view().dirInsert( + keylet::ownerDir(counterparty), repoKeylet, describeOwnerDir(counterparty))) + (*sleRepo)[sfDestinationNode] = *page; + else + return tecDIR_FULL; // LCOV_EXCL_LINE + + // An IOU issuer tracks locked balances through its own directory; an MPT + // issuance records the locked amount on the issuance itself. + if (!isXRP(collateral) && !collateral.holds()) + { + AccountID const issuer = collateral.getIssuer(); + if (auto const page = + view().dirInsert(keylet::ownerDir(issuer), repoKeylet, describeOwnerDir(issuer))) + (*sleRepo)[sfIssuerNode] = *page; + else + return tecDIR_FULL; // LCOV_EXCL_LINE + } + + if (isXRP(collateral)) + { + (*sle)[sfBalance] = (*sle)[sfBalance] - collateral; + view().update(sle); + } + else if ( + auto const ret = + repo::lockCollateral(view(), collateral.getIssuer(), accountID_, collateral, j_); + !isTesSuccess(ret)) + { + return ret; + } + + increaseOwnerCount(applyViewContext, sle, 1, j_); + addSponsorToLedgerEntry(applyViewContext, sleRepo); + + return tesSUCCESS; +} + +void +RepoCreate::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ +} + +bool +RepoCreate::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/repo/RepoDefault.cpp b/src/libxrpl/tx/transactors/repo/RepoDefault.cpp new file mode 100644 index 00000000000..11f659bb02e --- /dev/null +++ b/src/libxrpl/tx/transactors/repo/RepoDefault.cpp @@ -0,0 +1,77 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +std::uint32_t +RepoDefault::getFlagsMask(PreflightContext const& ctx) +{ + return tfUniversalMask; +} + +NotTEC +RepoDefault::preflight(PreflightContext const& ctx) +{ + return tesSUCCESS; +} + +TER +RepoDefault::preclaim(PreclaimContext const& ctx) +{ + auto const sleRepo = ctx.view.read(keylet::repo(ctx.tx[sfRepoID])); + if (!sleRepo) + return tecNO_ENTRY; + + if (!repo::isActive(sleRepo)) + return tecREPO_PENDING; + + // Anyone may close out a defaulted repo, but only once the seller's time + // to repurchase has run out. + auto const closeTime = ctx.view.parentCloseTime().time_since_epoch().count(); + if (closeTime <= (*sleRepo)[sfMaturityDate] + (*sleRepo)[sfGracePeriod]) + return tecTOO_SOON; + + return tesSUCCESS; +} + +TER +RepoDefault::doApply() +{ + auto const sleRepo = view().peek(keylet::repo(ctx_.tx[sfRepoID])); + if (!sleRepo) + return tefINTERNAL; // LCOV_EXCL_LINE + + // The buyer keeps the collateral; no cash moves. + return repo::releaseAndDelete( + ctx_.getApplyViewContext(), sleRepo, (*sleRepo)[sfCounterparty], j_); +} + +void +RepoDefault::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ +} + +bool +RepoDefault::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/test/app/Repo_test.cpp b/src/test/app/Repo_test.cpp new file mode 100644 index 00000000000..52010dec526 --- /dev/null +++ b/src/test/app/Repo_test.cpp @@ -0,0 +1,655 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +class Repo_test : public beast::unit_test::Suite +{ + static json::Value + create( + jtx::Account const& seller, + jtx::Account const& buyer, + STAmount const& collateral, + STAmount const& price, + std::uint32_t rate, + std::uint32_t expiration, + std::uint32_t maturity, + std::uint32_t grace) + { + json::Value jv; + jv[sfTransactionType] = jss::RepoCreate; + jv[sfAccount] = seller.human(); + jv[sfCounterparty] = buyer.human(); + jv[sfCollateralAmount] = collateral.getJson(JsonOptions::Values::None); + jv[sfPurchasePrice] = price.getJson(JsonOptions::Values::None); + jv[sfInterestRate] = rate; + jv[sfExpiration] = expiration; + jv[sfMaturityDate] = maturity; + jv[sfGracePeriod] = grace; + return jv; + } + + static json::Value + simple(json::StaticString const txType, jtx::Account const& account, uint256 const& repoID) + { + json::Value jv; + jv[sfTransactionType] = txType; + jv[sfAccount] = account.human(); + jv[sfRepoID] = to_string(repoID); + return jv; + } + + static uint256 + repoID(jtx::Env& env, jtx::Account const& seller) + { + return keylet::repo(seller.id(), SeqProxy::rawSequence(env.seq(seller))).key; + } + + static bool + exists(jtx::Env const& env, uint256 const& id) + { + return env.le(keylet::repo(id)) != nullptr; + } + + void + testEnabled(FeatureBitset features) + { + testcase("enabled"); + using namespace jtx; + + { + Env env{*this, features - featureRepo}; + Account const gw{"gw"}, seller{"seller"}, buyer{"buyer"}; + auto const USD = gw["USD"]; + env.fund(XRP(10000), gw, seller, buyer); + env.close(); + auto const now = env.now().time_since_epoch().count(); + env(create(seller, buyer, XRP(1000), USD(900), 0, now + 100, now + 200, 60), + Ter(temDISABLED)); + env.close(); + } + + Env env{*this, features}; + Account const gw{"gw"}, seller{"seller"}, buyer{"buyer"}; + auto const USD = gw["USD"]; + env.fund(XRP(10000), gw, seller, buyer); + env.close(); + env.trust(USD(100000), seller, buyer); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + auto const ownersBefore = ownerCount(env, seller); + auto const id = repoID(env, seller); + env(create(seller, buyer, XRP(1000), USD(900), 0, now + 100, now + 200, 60)); + env.close(); + + BEAST_EXPECT(exists(env, id)); + BEAST_EXPECT(ownerCount(env, seller) == ownersBefore + 1); + + auto const sle = env.le(keylet::repo(id)); + BEAST_EXPECT(sle && sle->getAccountID(sfAccount) == seller.id()); + BEAST_EXPECT(sle && sle->getAccountID(sfCounterparty) == buyer.id()); + // Pending until accepted. + BEAST_EXPECT(sle && !sle->isFieldPresent(sfStartDate)); + } + + void + testCreateMalformed(FeatureBitset features) + { + testcase("create malformed"); + using namespace jtx; + + Env env{*this, features}; + Account const gw{"gw"}, seller{"seller"}, buyer{"buyer"}; + auto const USD = gw["USD"]; + env.fund(XRP(10000), gw, seller, buyer); + env.close(); + env.trust(USD(100000), seller, buyer); + env.close(); + auto const now = env.now().time_since_epoch().count(); + + // The counterparty may not be the account. + env(create(seller, seller, XRP(1000), USD(900), 0, now + 100, now + 200, 60), + Ter(temMALFORMED)); + + // Collateral and cash must differ: repaying in the locked asset would + // let the seller settle with the very collateral that is immobilized. + env(create(seller, buyer, XRP(1000), XRP(900), 0, now + 100, now + 200, 60), + Ter(temBAD_AMOUNT)); + env(create(seller, buyer, XRP(1000), USD(900), 0, now + 100, now + 200, 60)); + env.close(); + + // Maturity must be after expiration. + env(create(seller, buyer, XRP(1000), USD(900), 0, now + 300, now + 200, 60), + Ter(temBAD_EXPIRATION)); + + // Amounts must be positive. + env(create(seller, buyer, XRP(0), USD(900), 0, now + 100, now + 200, 60), + Ter(temBAD_AMOUNT)); + env.close(); + } + + void + testAcceptAndClose(FeatureBitset features) + { + testcase("accept and close"); + using namespace jtx; + + Env env{*this, features}; + Account const gw{"gw"}, seller{"seller"}, buyer{"buyer"}; + auto const USD = gw["USD"]; + env.fund(XRP(10000), gw, seller, buyer); + env.close(); + env.trust(USD(100000), seller, buyer); + env.close(); + env(pay(gw, buyer, USD(5000))); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + auto const id = repoID(env, seller); + // Collateral XRP, cash USD, zero interest so the repurchase is exact. + env(create(seller, buyer, XRP(1000), USD(900), 0, now + 100, now + 1000, 60)); + env.close(); + + auto const sellerUsdBefore = env.balance(seller, USD.issue()); + + // Only the pinned buyer may accept. + env(simple(jss::RepoAccept, gw, id), Ter(tecNO_PERMISSION)); + + env(simple(jss::RepoAccept, buyer, id)); + env.close(); + + // The cash arrived and the repo is active. + BEAST_EXPECT(env.balance(seller, USD.issue()) == sellerUsdBefore + USD(900)); + auto const sle = env.le(keylet::repo(id)); + BEAST_EXPECT(sle && sle->isFieldPresent(sfStartDate)); + + // A second accept fails. + env(simple(jss::RepoAccept, buyer, id), Ter(tecREPO_ACTIVE)); + + // Only the seller may repurchase. + env(simple(jss::RepoClose, buyer, id), Ter(tecNO_PERMISSION)); + + auto const buyerUsdBefore = env.balance(buyer, USD.issue()); + env(simple(jss::RepoClose, seller, id)); + env.close(); + + // Zero interest, so the buyer gets exactly the purchase price back and + // the entry is gone. + BEAST_EXPECT(env.balance(buyer, USD.issue()) == buyerUsdBefore + USD(900)); + BEAST_EXPECT(!exists(env, id)); + } + + void + testCancel(FeatureBitset features) + { + testcase("cancel"); + using namespace jtx; + + Env env{*this, features}; + Account const gw{"gw"}, seller{"seller"}, buyer{"buyer"}, stranger{"stranger"}; + auto const USD = gw["USD"]; + env.fund(XRP(10000), gw, seller, buyer, stranger); + env.close(); + env.trust(USD(100000), seller, buyer); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + auto const ownersBefore = ownerCount(env, seller); + auto const id = repoID(env, seller); + env(create(seller, buyer, XRP(1000), USD(900), 0, now + 100, now + 1000, 60)); + env.close(); + + // A stranger cannot cancel while the offer is live. + env(simple(jss::RepoCancel, stranger, id), Ter(tecNO_PERMISSION)); + + // The seller can, at any time. + env(simple(jss::RepoCancel, seller, id)); + env.close(); + + BEAST_EXPECT(!exists(env, id)); + BEAST_EXPECT(ownerCount(env, seller) == ownersBefore); + + // Once expired, anyone may clear it. + auto const id2 = repoID(env, seller); + auto const now2 = env.now().time_since_epoch().count(); + env(create(seller, buyer, XRP(1000), USD(900), 0, now2 + 10, now2 + 1000, 60)); + env.close(); + env.close(std::chrono::seconds(60)); + env(simple(jss::RepoCancel, stranger, id2)); + env.close(); + BEAST_EXPECT(!exists(env, id2)); + } + + void + testDefault(FeatureBitset features) + { + testcase("default"); + using namespace jtx; + + Env env{*this, features}; + Account const gw{"gw"}, seller{"seller"}, buyer{"buyer"}, keeper{"keeper"}; + auto const USD = gw["USD"]; + env.fund(XRP(10000), gw, seller, buyer, keeper); + env.close(); + env.trust(USD(100000), seller, buyer); + env.close(); + env(pay(gw, buyer, USD(5000))); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + auto const id = repoID(env, seller); + env(create(seller, buyer, XRP(1000), USD(900), 0, now + 100, now + 200, 60)); + env.close(); + env(simple(jss::RepoAccept, buyer, id)); + env.close(); + + // Not yet in default. + env(simple(jss::RepoDefault, keeper, id), Ter(tecTOO_SOON)); + + auto const buyerXrpBefore = env.balance(buyer); + env.close(std::chrono::seconds(400)); + + // Past maturity plus grace, anyone may close it out. + env(simple(jss::RepoDefault, keeper, id)); + env.close(); + + BEAST_EXPECT(!exists(env, id)); + // The buyer keeps the collateral. + BEAST_EXPECT(env.balance(buyer) > buyerXrpBefore); + } + + void + testInterest(FeatureBitset features) + { + testcase("interest accrues and is capped at maturity"); + using namespace jtx; + + Env env{*this, features}; + Account const gw{"gw"}, seller{"seller"}, buyer{"buyer"}; + auto const USD = gw["USD"]; + env.fund(XRP(10000), gw, seller, buyer); + env.close(); + env.trust(USD(100000), seller, buyer); + env.close(); + env(pay(gw, seller, USD(5000))); + env(pay(gw, buyer, USD(5000))); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + auto const id = repoID(env, seller); + // kTenthBipsPerUnity is 100 percent annualized. + env(create( + seller, + buyer, + XRP(1000), + USD(1000), + kTenthBipsPerUnity.value(), + now + 100, + now + 400000, + 600)); + env.close(); + env(simple(jss::RepoAccept, buyer, id)); + env.close(); + + auto const start = (*env.le(keylet::repo(id)))[sfStartDate]; + auto const buyerBefore = env.balance(buyer, USD.issue()); + env.close(std::chrono::seconds(50000)); + + // The close time the transactor will use is the parent close time of the + // ledger that applies it, which is the current open ledger's. + auto const closeTime = env.current()->parentCloseTime().time_since_epoch().count(); + env(simple(jss::RepoClose, seller, id)); + env.close(); + + auto const elapsed = closeTime - start; + BEAST_EXPECT(elapsed > 0); + Number const expected = Number(1000) + + tenthBipsOfValue(Number(1000) * Number(elapsed), kTenthBipsPerUnity) / + Number(kSecondsInYear); + auto const paid = env.balance(buyer, USD.issue()).value() - buyerBefore.value(); + + // Exactly the repurchase amount, so the rate's scale is pinned rather + // than merely shown to be positive. + BEAST_EXPECT(paid == STAmount(USD.issue(), expected)); + BEAST_EXPECT(!exists(env, id)); + } + + // The rate carries the same ceiling LoanSet puts on sfInterestRate. + void + testInterestRateBound(FeatureBitset features) + { + testcase("the interest rate is bounded"); + using namespace jtx; + + Env env{*this, features}; + Account const gw{"gw"}, seller{"seller"}, buyer{"buyer"}; + auto const USD = gw["USD"]; + env.fund(XRP(10000), gw, seller, buyer); + env.close(); + env.trust(USD(100000), seller, buyer); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + env(create( + seller, + buyer, + XRP(1000), + USD(1000), + lending::kMaxInterestRate.value() + 1, + now + 100, + now + 1000, + 600), + Ter(temBAD_AMOUNT)); + env.close(); + + auto const id = repoID(env, seller); + env(create( + seller, + buyer, + XRP(1000), + USD(1000), + lending::kMaxInterestRate.value(), + now + 100, + now + 1000, + 600)); + env.close(); + BEAST_EXPECT(exists(env, id)); + } + + // Collateral that is not XRP goes through the XLS-85 locking path, which + // needs the issuer's opt-in and unfrozen, authorized parties on both sides. + void + testIouCollateral(FeatureBitset features) + { + testcase("IOU collateral"); + using namespace jtx; + + Env env{*this, features}; + Account const gw{"gw"}, cash{"cash"}, seller{"seller"}, buyer{"buyer"}; + auto const CLT = gw["CLT"]; + auto const USD = cash["USD"]; + env.fund(XRP(10000), gw, cash, seller, buyer); + env.close(); + env.trust(CLT(100000), seller, buyer); + env.trust(USD(100000), seller, buyer); + env.close(); + env(pay(gw, seller, CLT(5000))); + env(pay(cash, buyer, USD(5000))); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + + // Without the issuer's locking opt-in the collateral cannot be locked. + env(create(seller, buyer, CLT(1000), USD(900), 0, now + 100, now + 1000, 60), + Ter(tecNO_PERMISSION)); + env.close(); + + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + auto const id = repoID(env, seller); + env(create(seller, buyer, CLT(1000), USD(900), 0, now + 100, now + 1000, 60)); + env.close(); + BEAST_EXPECT(exists(env, id)); + + // The collateral left the seller's spendable balance. + BEAST_EXPECT(env.balance(seller, CLT.issue()) == CLT(4000)); + + env(simple(jss::RepoAccept, buyer, id)); + env.close(); + + env(simple(jss::RepoClose, seller, id)); + env.close(); + + // Closing returns the collateral and the entry is gone. + BEAST_EXPECT(!exists(env, id)); + BEAST_EXPECT(env.balance(seller, CLT.issue()) == CLT(5000)); + } + + // On default the collateral must reach the buyer, so a frozen or + // unauthorized buyer is rejected before anything is locked. + void + testIouCollateralFrozen(FeatureBitset features) + { + testcase("IOU collateral, frozen party"); + using namespace jtx; + + Env env{*this, features}; + Account const gw{"gw"}, cash{"cash"}, seller{"seller"}, buyer{"buyer"}; + auto const CLT = gw["CLT"]; + auto const USD = cash["USD"]; + env.fund(XRP(10000), gw, cash, seller, buyer); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(CLT(100000), seller, buyer); + env.trust(USD(100000), seller, buyer); + env.close(); + env(pay(gw, seller, CLT(5000))); + env(pay(cash, buyer, USD(5000))); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + + // Freezing the buyer blocks the create, because the buyer is where the + // collateral goes if the seller defaults. + env(trust(gw, CLT(100000), buyer, tfSetFreeze)); + env.close(); + env(create(seller, buyer, CLT(1000), USD(900), 0, now + 100, now + 1000, 60), + Ter(tecFROZEN)); + env.close(); + + env(trust(gw, CLT(100000), buyer, tfClearFreeze)); + env.close(); + + // And freezing the seller blocks it too. + env(trust(gw, CLT(100000), seller, tfSetFreeze)); + env.close(); + env(create(seller, buyer, CLT(1000), USD(900), 0, now + 100, now + 1000, 60), + Ter(tecFROZEN)); + env.close(); + } + + // MPT collateral locks through the issuance's locked-amount accounting + // rather than a trustline, and needs the issuance to permit escrow. + void + testMptCollateral(FeatureBitset features) + { + testcase("MPT collateral"); + using namespace jtx; + + Env env{*this, features}; + Account const gw{"gw"}, cash{"cash"}, seller{"seller"}, buyer{"buyer"}; + auto const USD = cash["USD"]; + env.fund(XRP(10000), gw, cash, seller, buyer); + env.close(); + env.trust(USD(100000), seller, buyer); + env.close(); + env(pay(cash, buyer, USD(5000))); + env.close(); + + // An issuance that does not permit escrow cannot be locked. + { + MPTTester noLock(env, gw, {.holders = {seller, buyer}, .fund = false}); + noLock.create({.flags = tfMPTCanTransfer}); + noLock.authorize({.account = seller}); + noLock.authorize({.account = buyer}); + auto const mpt = noLock["MPT"]; + env(pay(gw, seller, mpt(10000))); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + env(create(seller, buyer, mpt(1000), USD(900), 0, now + 100, now + 1000, 60), + Ter(tecNO_PERMISSION)); + env.close(); + } + + MPTTester mptGw(env, gw, {.holders = {seller, buyer}, .fund = false}); + mptGw.create({.flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = seller}); + mptGw.authorize({.account = buyer}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, seller, mpt(10000))); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + auto const id = repoID(env, seller); + env(create(seller, buyer, mpt(1000), USD(900), 0, now + 100, now + 1000, 60)); + env.close(); + BEAST_EXPECT(exists(env, id)); + + env(simple(jss::RepoAccept, buyer, id)); + env.close(); + + // Default hands the collateral to the buyer, exercising the MPT + // unlock-to-a-third-party path. + env.close(std::chrono::seconds(2000)); + env(simple(jss::RepoDefault, buyer, id)); + env.close(); + + BEAST_EXPECT(!exists(env, id)); + } + + // The spec requires an open repo to block account deletion, which falls out + // of ltREPO being absent from nonObligationDeleter. Both parties carry the + // entry in their owner directory, so both are blocked. + void + testAccountDeleteBlocked(FeatureBitset features) + { + testcase("an open repo blocks account deletion"); + using namespace jtx; + + Env env{*this, features}; + Account const gw{"gw"}, seller{"seller"}, buyer{"buyer"}, sink{"sink"}; + auto const USD = gw["USD"]; + env.fund(XRP(10000), gw, seller, buyer, sink); + env.close(); + env.trust(USD(100000), seller, buyer); + env.close(); + env(pay(gw, buyer, USD(5000))); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + auto const id = repoID(env, seller); + env(create(seller, buyer, XRP(1000), USD(1000), 0, now + 100, now + 100000, 600)); + env.close(); + env(simple(jss::RepoAccept, buyer, id)); + env.close(); + + // Both parties carry the entry, so both are refused. + incLgrSeqForAccDel(env, seller); + env(acctdelete(seller, sink), + Fee(drops(env.current()->fees().increment)), + Ter(tecHAS_OBLIGATIONS)); + env.close(); + env(acctdelete(buyer, sink), + Fee(drops(env.current()->fees().increment)), + Ter(tecHAS_OBLIGATIONS)); + env.close(); + + // With the repo gone and the zero-balance trust line removed, the same + // account deletes, so the refusal above was the repo and nothing else. + env(simple(jss::RepoClose, seller, id)); + env.close(); + BEAST_EXPECT(!exists(env, id)); + BEAST_EXPECT(env.balance(seller, USD.issue()) == USD(0)); + + env(trust(seller, USD(0))); + env.close(); + env(acctdelete(seller, sink), Fee(drops(env.current()->fees().increment))); + env.close(); + BEAST_EXPECT(!env.le(keylet::account(seller.id()))); + } + + // account_objects and ledger_entry both resolve the entry under the name the + // LEDGER_ENTRY declaration gives it. + void + testRpc(FeatureBitset features) + { + testcase("account_objects and ledger_entry"); + using namespace jtx; + + Env env{*this, features}; + Account const gw{"gw"}, seller{"seller"}, buyer{"buyer"}; + auto const USD = gw["USD"]; + env.fund(XRP(10000), gw, seller, buyer); + env.close(); + env.trust(USD(100000), seller, buyer); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + auto const id = repoID(env, seller); + env(create(seller, buyer, XRP(1000), USD(1000), 500, now + 100, now + 100000, 600)); + env.close(); + + auto const objects = [&](jtx::Account const& who) { + json::Value params; + params[jss::account] = who.human(); + params[jss::type] = "repo"; + return env.rpc("json", "account_objects", to_string(params))[jss::result]; + }; + + for (auto const& who : {seller, buyer}) + { + auto const jv = objects(who); + BEAST_EXPECT(jv[jss::account_objects].size() == 1); + auto const& object = jv[jss::account_objects][0u]; + BEAST_EXPECT(object["LedgerEntryType"].asString() == "Repo"); + BEAST_EXPECT(object[jss::index].asString() == to_string(id)); + } + + { + json::Value params; + params[jss::repo] = json::ValueType::Object; + params[jss::repo][jss::account] = seller.human(); + params[jss::repo][jss::seq] = env.seq(seller) - 1; + auto const jv = env.rpc("json", "ledger_entry", to_string(params))[jss::result]; + BEAST_EXPECT(jv[jss::index].asString() == to_string(id)); + } + } + +public: + void + run() override + { + using namespace jtx; + auto const all = jtx::testableAmendments(); + testEnabled(all); + testCreateMalformed(all); + testAcceptAndClose(all); + testCancel(all); + testDefault(all); + testInterest(all); + testInterestRateBound(all); + testIouCollateral(all); + testIouCollateralFrozen(all); + testMptCollateral(all); + testAccountDeleteBlocked(all); + testRpc(all); + } +}; + +BEAST_DEFINE_TESTSUITE(Repo, app, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/invariants/InvariantsRepo_test.cpp b/src/test/app/invariants/InvariantsRepo_test.cpp new file mode 100644 index 00000000000..9d36e1a6568 --- /dev/null +++ b/src/test/app/invariants/InvariantsRepo_test.cpp @@ -0,0 +1,239 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl::test { + +class InvariantsRepo_test : public InvariantsBase +{ + // A repo's terms are struck once. Each case below restates one of them + // after the fact, which the invariant must reject. + void + testRepoTermsAreImmutable() + { + testcase("repo terms are immutable"); + using namespace jtx; + + uint256 repoKey; + Account const gw{"gw"}; + + auto const createRepo = [&](Account const& a1, Account const& a2, Env& env) { + auto const USD = gw["USD"]; + env.fund(XRP(10000), gw); + env.close(); + env.trust(USD(100000), a1, a2); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + repoKey = keylet::repo(a1.id(), SeqProxy::rawSequence(env.seq(a1))).key; + + json::Value jv; + jv[sfTransactionType] = jss::RepoCreate; + jv[sfAccount] = a1.human(); + jv[sfCounterparty] = a2.human(); + jv[sfCollateralAmount] = XRP(10).value().getJson(JsonOptions::Values::None); + jv[sfPurchasePrice] = USD(9).value().getJson(JsonOptions::Values::None); + jv[sfInterestRate] = 0; + jv[sfExpiration] = now + 100; + jv[sfMaturityDate] = now + 1000; + jv[sfGracePeriod] = 60; + env(jv); + env.close(); + return true; + }; + + struct Mod + { + std::string expected; + std::function func; + }; + + std::vector const mods{ + {"a repo's seller is its counterparty", + [](SLE::pointer& sle) { (*sle)[sfCounterparty] = (*sle)[sfAccount]; }}, + {"a repo's collateral or price is not positive", + [](SLE::pointer& sle) { (*sle)[sfCollateralAmount] = STAmount{XRPAmount{0}}; }}, + {"a repo matures before it expires", + [](SLE::pointer& sle) { (*sle)[sfMaturityDate] = (*sle)[sfExpiration] - 1; }}, + {"a repo's terms changed after creation", + [](SLE::pointer& sle) { (*sle)[sfInterestRate] = (*sle)[sfInterestRate] + 1; }}, + {"a repo's terms changed after creation", + [](SLE::pointer& sle) { (*sle)[sfCollateralAmount] = STAmount{XRPAmount{1}}; }}, + }; + + for (auto const& mod : mods) + { + doInvariantCheck( + {{mod.expected}}, + [&](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::repo(repoKey)); + if (!sle) + return false; + mod.func(sle); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createRepo); + } + } + + // A repo moves from pending to active and never back, and the start date + // it was stamped with is the one interest is computed from. + void + testRepoOnlyMovesForward() + { + testcase("repo only moves forward"); + using namespace jtx; + + uint256 repoKey; + Account const gw{"gw"}; + + auto const createActiveRepo = [&](Account const& a1, Account const& a2, Env& env) { + auto const USD = gw["USD"]; + env.fund(XRP(10000), gw); + env.close(); + env.trust(USD(100000), a1, a2); + env.close(); + env(pay(gw, a2, USD(5000))); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + repoKey = keylet::repo(a1.id(), SeqProxy::rawSequence(env.seq(a1))).key; + + json::Value jv; + jv[sfTransactionType] = jss::RepoCreate; + jv[sfAccount] = a1.human(); + jv[sfCounterparty] = a2.human(); + jv[sfCollateralAmount] = XRP(10).value().getJson(JsonOptions::Values::None); + jv[sfPurchasePrice] = USD(9).value().getJson(JsonOptions::Values::None); + jv[sfInterestRate] = 0; + jv[sfExpiration] = now + 100; + jv[sfMaturityDate] = now + 1000; + jv[sfGracePeriod] = 60; + env(jv); + env.close(); + + json::Value accept; + accept[sfTransactionType] = jss::RepoAccept; + accept[sfAccount] = a2.human(); + accept[sfRepoID] = to_string(repoKey); + env(accept); + env.close(); + return true; + }; + + doInvariantCheck( + {{"an active repo reverted to pending"}}, + [&](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::repo(repoKey)); + if (!sle || !sle->isFieldPresent(sfStartDate)) + return false; + sle->makeFieldAbsent(sfStartDate); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createActiveRepo); + + doInvariantCheck( + {{"a repo's start date changed"}}, + [&](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::repo(repoKey)); + if (!sle || !sle->isFieldPresent(sfStartDate)) + return false; + (*sle)[sfStartDate] = (*sle)[sfStartDate] + 1; + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createActiveRepo); + } + + // A repo may only be removed by cancel, close or default. + void + testRepoDeletedByWrongTransaction() + { + testcase("repo deleted by the wrong transaction"); + using namespace jtx; + + uint256 repoKey; + Account const gw{"gw"}; + + auto const createRepo = [&](Account const& a1, Account const& a2, Env& env) { + auto const USD = gw["USD"]; + env.fund(XRP(10000), gw); + env.close(); + env.trust(USD(100000), a1, a2); + env.close(); + + auto const now = env.now().time_since_epoch().count(); + repoKey = keylet::repo(a1.id(), SeqProxy::rawSequence(env.seq(a1))).key; + + json::Value jv; + jv[sfTransactionType] = jss::RepoCreate; + jv[sfAccount] = a1.human(); + jv[sfCounterparty] = a2.human(); + jv[sfCollateralAmount] = XRP(10).value().getJson(JsonOptions::Values::None); + jv[sfPurchasePrice] = USD(9).value().getJson(JsonOptions::Values::None); + jv[sfInterestRate] = 0; + jv[sfExpiration] = now + 100; + jv[sfMaturityDate] = now + 1000; + jv[sfGracePeriod] = 60; + env(jv); + env.close(); + return true; + }; + + doInvariantCheck( + {{"a repo was deleted by transaction type"}}, + [&](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::repo(repoKey)); + if (!sle) + return false; + ac.view().erase(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createRepo); + } + +public: + void + run() override + { + testRepoTermsAreImmutable(); + testRepoOnlyMovesForward(); + testRepoDeletedByWrongTransaction(); + } +}; + +BEAST_DEFINE_TESTSUITE(InvariantsRepo, app, xrpl); + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/RepoTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/RepoTests.cpp new file mode 100644 index 00000000000..17140a2a363 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/RepoTests.cpp @@ -0,0 +1,488 @@ +// Auto-generated unit tests for ledger entry Repo + + +#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(RepoTests, BuilderSettersRoundTrip) +{ + uint256 const index{1u}; + + auto const accountValue = canonical_ACCOUNT(); + auto const counterpartyValue = canonical_ACCOUNT(); + auto const collateralAmountValue = canonical_AMOUNT(); + auto const purchasePriceValue = canonical_AMOUNT(); + auto const interestRateValue = canonical_UINT32(); + auto const expirationValue = canonical_UINT32(); + auto const startDateValue = canonical_UINT32(); + auto const maturityDateValue = canonical_UINT32(); + auto const gracePeriodValue = canonical_UINT32(); + auto const transferRateValue = canonical_UINT32(); + auto const dataValue = canonical_VL(); + auto const ownerNodeValue = canonical_UINT64(); + auto const destinationNodeValue = canonical_UINT64(); + auto const issuerNodeValue = canonical_UINT64(); + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + + RepoBuilder builder{ + accountValue, + counterpartyValue, + collateralAmountValue, + purchasePriceValue, + interestRateValue, + expirationValue, + maturityDateValue, + gracePeriodValue, + ownerNodeValue, + destinationNodeValue, + previousTxnIDValue, + previousTxnLgrSeqValue + }; + + builder.setStartDate(startDateValue); + builder.setTransferRate(transferRateValue); + builder.setData(dataValue); + builder.setIssuerNode(issuerNodeValue); + + 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 = counterpartyValue; + auto const actual = entry.getCounterparty(); + expectEqualField(expected, actual, "sfCounterparty"); + } + + { + auto const& expected = collateralAmountValue; + auto const actual = entry.getCollateralAmount(); + expectEqualField(expected, actual, "sfCollateralAmount"); + } + + { + auto const& expected = purchasePriceValue; + auto const actual = entry.getPurchasePrice(); + expectEqualField(expected, actual, "sfPurchasePrice"); + } + + { + auto const& expected = interestRateValue; + auto const actual = entry.getInterestRate(); + expectEqualField(expected, actual, "sfInterestRate"); + } + + { + auto const& expected = expirationValue; + auto const actual = entry.getExpiration(); + expectEqualField(expected, actual, "sfExpiration"); + } + + { + auto const& expected = maturityDateValue; + auto const actual = entry.getMaturityDate(); + expectEqualField(expected, actual, "sfMaturityDate"); + } + + { + auto const& expected = gracePeriodValue; + auto const actual = entry.getGracePeriod(); + expectEqualField(expected, actual, "sfGracePeriod"); + } + + { + auto const& expected = ownerNodeValue; + auto const actual = entry.getOwnerNode(); + expectEqualField(expected, actual, "sfOwnerNode"); + } + + { + auto const& expected = destinationNodeValue; + auto const actual = entry.getDestinationNode(); + expectEqualField(expected, actual, "sfDestinationNode"); + } + + { + 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 = startDateValue; + auto const actualOpt = entry.getStartDate(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfStartDate"); + EXPECT_TRUE(entry.hasStartDate()); + } + + { + auto const& expected = transferRateValue; + auto const actualOpt = entry.getTransferRate(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfTransferRate"); + EXPECT_TRUE(entry.hasTransferRate()); + } + + { + auto const& expected = dataValue; + auto const actualOpt = entry.getData(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfData"); + EXPECT_TRUE(entry.hasData()); + } + + { + auto const& expected = issuerNodeValue; + auto const actualOpt = entry.getIssuerNode(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfIssuerNode"); + EXPECT_TRUE(entry.hasIssuerNode()); + } + + 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(RepoTests, BuilderFromSleRoundTrip) +{ + uint256 const index{2u}; + + auto const accountValue = canonical_ACCOUNT(); + auto const counterpartyValue = canonical_ACCOUNT(); + auto const collateralAmountValue = canonical_AMOUNT(); + auto const purchasePriceValue = canonical_AMOUNT(); + auto const interestRateValue = canonical_UINT32(); + auto const expirationValue = canonical_UINT32(); + auto const startDateValue = canonical_UINT32(); + auto const maturityDateValue = canonical_UINT32(); + auto const gracePeriodValue = canonical_UINT32(); + auto const transferRateValue = canonical_UINT32(); + auto const dataValue = canonical_VL(); + auto const ownerNodeValue = canonical_UINT64(); + auto const destinationNodeValue = canonical_UINT64(); + auto const issuerNodeValue = canonical_UINT64(); + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + + auto sle = std::make_shared(Repo::entryType, index); + + sle->at(sfAccount) = accountValue; + sle->at(sfCounterparty) = counterpartyValue; + sle->at(sfCollateralAmount) = collateralAmountValue; + sle->at(sfPurchasePrice) = purchasePriceValue; + sle->at(sfInterestRate) = interestRateValue; + sle->at(sfExpiration) = expirationValue; + sle->at(sfStartDate) = startDateValue; + sle->at(sfMaturityDate) = maturityDateValue; + sle->at(sfGracePeriod) = gracePeriodValue; + sle->at(sfTransferRate) = transferRateValue; + sle->at(sfData) = dataValue; + sle->at(sfOwnerNode) = ownerNodeValue; + sle->at(sfDestinationNode) = destinationNodeValue; + sle->at(sfIssuerNode) = issuerNodeValue; + sle->at(sfPreviousTxnID) = previousTxnIDValue; + sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue; + + RepoBuilder builderFromSle{sle}; + EXPECT_TRUE(builderFromSle.validate()); + + auto const entryFromBuilder = builderFromSle.build(index); + + Repo 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 = counterpartyValue; + + auto const fromSle = entryFromSle.getCounterparty(); + auto const fromBuilder = entryFromBuilder.getCounterparty(); + + expectEqualField(expected, fromSle, "sfCounterparty"); + expectEqualField(expected, fromBuilder, "sfCounterparty"); + } + + { + auto const& expected = collateralAmountValue; + + auto const fromSle = entryFromSle.getCollateralAmount(); + auto const fromBuilder = entryFromBuilder.getCollateralAmount(); + + expectEqualField(expected, fromSle, "sfCollateralAmount"); + expectEqualField(expected, fromBuilder, "sfCollateralAmount"); + } + + { + auto const& expected = purchasePriceValue; + + auto const fromSle = entryFromSle.getPurchasePrice(); + auto const fromBuilder = entryFromBuilder.getPurchasePrice(); + + expectEqualField(expected, fromSle, "sfPurchasePrice"); + expectEqualField(expected, fromBuilder, "sfPurchasePrice"); + } + + { + auto const& expected = interestRateValue; + + auto const fromSle = entryFromSle.getInterestRate(); + auto const fromBuilder = entryFromBuilder.getInterestRate(); + + expectEqualField(expected, fromSle, "sfInterestRate"); + expectEqualField(expected, fromBuilder, "sfInterestRate"); + } + + { + auto const& expected = expirationValue; + + auto const fromSle = entryFromSle.getExpiration(); + auto const fromBuilder = entryFromBuilder.getExpiration(); + + expectEqualField(expected, fromSle, "sfExpiration"); + expectEqualField(expected, fromBuilder, "sfExpiration"); + } + + { + auto const& expected = maturityDateValue; + + auto const fromSle = entryFromSle.getMaturityDate(); + auto const fromBuilder = entryFromBuilder.getMaturityDate(); + + expectEqualField(expected, fromSle, "sfMaturityDate"); + expectEqualField(expected, fromBuilder, "sfMaturityDate"); + } + + { + auto const& expected = gracePeriodValue; + + auto const fromSle = entryFromSle.getGracePeriod(); + auto const fromBuilder = entryFromBuilder.getGracePeriod(); + + expectEqualField(expected, fromSle, "sfGracePeriod"); + expectEqualField(expected, fromBuilder, "sfGracePeriod"); + } + + { + 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 = destinationNodeValue; + + auto const fromSle = entryFromSle.getDestinationNode(); + auto const fromBuilder = entryFromBuilder.getDestinationNode(); + + expectEqualField(expected, fromSle, "sfDestinationNode"); + expectEqualField(expected, fromBuilder, "sfDestinationNode"); + } + + { + 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 = startDateValue; + + auto const fromSleOpt = entryFromSle.getStartDate(); + auto const fromBuilderOpt = entryFromBuilder.getStartDate(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfStartDate"); + expectEqualField(expected, *fromBuilderOpt, "sfStartDate"); + } + + { + auto const& expected = transferRateValue; + + auto const fromSleOpt = entryFromSle.getTransferRate(); + auto const fromBuilderOpt = entryFromBuilder.getTransferRate(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfTransferRate"); + expectEqualField(expected, *fromBuilderOpt, "sfTransferRate"); + } + + { + auto const& expected = dataValue; + + auto const fromSleOpt = entryFromSle.getData(); + auto const fromBuilderOpt = entryFromBuilder.getData(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfData"); + expectEqualField(expected, *fromBuilderOpt, "sfData"); + } + + { + auto const& expected = issuerNodeValue; + + auto const fromSleOpt = entryFromSle.getIssuerNode(); + auto const fromBuilderOpt = entryFromBuilder.getIssuerNode(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfIssuerNode"); + expectEqualField(expected, *fromBuilderOpt, "sfIssuerNode"); + } + + EXPECT_EQ(entryFromSle.getKey(), index); + EXPECT_EQ(entryFromBuilder.getKey(), index); +} + +// 3) Verify wrapper throws when constructed from wrong ledger entry type. +TEST(RepoTests, 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(Repo{wrongEntry.getSle()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong ledger entry type. +TEST(RepoTests, 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(RepoBuilder{wrongEntry.getSle()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(RepoTests, OptionalFieldsReturnNullopt) +{ + uint256 const index{3u}; + + auto const accountValue = canonical_ACCOUNT(); + auto const counterpartyValue = canonical_ACCOUNT(); + auto const collateralAmountValue = canonical_AMOUNT(); + auto const purchasePriceValue = canonical_AMOUNT(); + auto const interestRateValue = canonical_UINT32(); + auto const expirationValue = canonical_UINT32(); + auto const maturityDateValue = canonical_UINT32(); + auto const gracePeriodValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const destinationNodeValue = canonical_UINT64(); + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + + RepoBuilder builder{ + accountValue, + counterpartyValue, + collateralAmountValue, + purchasePriceValue, + interestRateValue, + expirationValue, + maturityDateValue, + gracePeriodValue, + ownerNodeValue, + destinationNodeValue, + previousTxnIDValue, + previousTxnLgrSeqValue + }; + + auto const entry = builder.build(index); + + // Verify optional fields are not present + EXPECT_FALSE(entry.hasStartDate()); + EXPECT_FALSE(entry.getStartDate().has_value()); + EXPECT_FALSE(entry.hasTransferRate()); + EXPECT_FALSE(entry.getTransferRate().has_value()); + EXPECT_FALSE(entry.hasData()); + EXPECT_FALSE(entry.getData().has_value()); + EXPECT_FALSE(entry.hasIssuerNode()); + EXPECT_FALSE(entry.getIssuerNode().has_value()); +} +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/RepoAcceptTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/RepoAcceptTests.cpp new file mode 100644 index 00000000000..431f389ff1c --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/RepoAcceptTests.cpp @@ -0,0 +1,146 @@ +// Auto-generated unit tests for transaction RepoAccept + + +#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(TransactionsRepoAcceptTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testRepoAccept")); + + // 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 repoIDValue = canonical_UINT256(); + + RepoAcceptBuilder builder{ + accountValue, + repoIDValue, + 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 = repoIDValue; + auto const actual = tx.getRepoID(); + expectEqualField(expected, actual, "sfRepoID"); + } + + // Verify optional fields +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsRepoAcceptTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testRepoAcceptFromTx")); + + // 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 repoIDValue = canonical_UINT256(); + + // Build an initial transaction + RepoAcceptBuilder initialBuilder{ + accountValue, + repoIDValue, + sequenceValue, + feeValue + }; + + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + RepoAcceptBuilder 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 = repoIDValue; + auto const actual = rebuiltTx.getRepoID(); + expectEqualField(expected, actual, "sfRepoID"); + } + + // Verify optional fields +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsRepoAcceptTests, 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(RepoAccept{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsRepoAcceptTests, 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(RepoAcceptBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/RepoCancelTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/RepoCancelTests.cpp new file mode 100644 index 00000000000..0ee580a977d --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/RepoCancelTests.cpp @@ -0,0 +1,146 @@ +// Auto-generated unit tests for transaction RepoCancel + + +#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(TransactionsRepoCancelTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testRepoCancel")); + + // 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 repoIDValue = canonical_UINT256(); + + RepoCancelBuilder builder{ + accountValue, + repoIDValue, + 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 = repoIDValue; + auto const actual = tx.getRepoID(); + expectEqualField(expected, actual, "sfRepoID"); + } + + // Verify optional fields +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsRepoCancelTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testRepoCancelFromTx")); + + // 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 repoIDValue = canonical_UINT256(); + + // Build an initial transaction + RepoCancelBuilder initialBuilder{ + accountValue, + repoIDValue, + sequenceValue, + feeValue + }; + + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + RepoCancelBuilder 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 = repoIDValue; + auto const actual = rebuiltTx.getRepoID(); + expectEqualField(expected, actual, "sfRepoID"); + } + + // Verify optional fields +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsRepoCancelTests, 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(RepoCancel{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsRepoCancelTests, 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(RepoCancelBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/RepoCloseTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/RepoCloseTests.cpp new file mode 100644 index 00000000000..6ea592f977b --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/RepoCloseTests.cpp @@ -0,0 +1,146 @@ +// Auto-generated unit tests for transaction RepoClose + + +#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(TransactionsRepoCloseTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testRepoClose")); + + // 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 repoIDValue = canonical_UINT256(); + + RepoCloseBuilder builder{ + accountValue, + repoIDValue, + 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 = repoIDValue; + auto const actual = tx.getRepoID(); + expectEqualField(expected, actual, "sfRepoID"); + } + + // Verify optional fields +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsRepoCloseTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testRepoCloseFromTx")); + + // 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 repoIDValue = canonical_UINT256(); + + // Build an initial transaction + RepoCloseBuilder initialBuilder{ + accountValue, + repoIDValue, + sequenceValue, + feeValue + }; + + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + RepoCloseBuilder 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 = repoIDValue; + auto const actual = rebuiltTx.getRepoID(); + expectEqualField(expected, actual, "sfRepoID"); + } + + // Verify optional fields +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsRepoCloseTests, 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(RepoClose{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsRepoCloseTests, 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(RepoCloseBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/RepoCreateTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/RepoCreateTests.cpp new file mode 100644 index 00000000000..7e91ceb9dbb --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/RepoCreateTests.cpp @@ -0,0 +1,303 @@ +// Auto-generated unit tests for transaction RepoCreate + + +#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(TransactionsRepoCreateTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testRepoCreate")); + + // 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 collateralAmountValue = canonical_AMOUNT(); + auto const purchasePriceValue = canonical_AMOUNT(); + auto const interestRateValue = canonical_UINT32(); + auto const expirationValue = canonical_UINT32(); + auto const maturityDateValue = canonical_UINT32(); + auto const gracePeriodValue = canonical_UINT32(); + auto const dataValue = canonical_VL(); + + RepoCreateBuilder builder{ + accountValue, + counterpartyValue, + collateralAmountValue, + purchasePriceValue, + interestRateValue, + expirationValue, + maturityDateValue, + gracePeriodValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setData(dataValue); + + 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 = counterpartyValue; + auto const actual = tx.getCounterparty(); + expectEqualField(expected, actual, "sfCounterparty"); + } + + { + auto const& expected = collateralAmountValue; + auto const actual = tx.getCollateralAmount(); + expectEqualField(expected, actual, "sfCollateralAmount"); + } + + { + auto const& expected = purchasePriceValue; + auto const actual = tx.getPurchasePrice(); + expectEqualField(expected, actual, "sfPurchasePrice"); + } + + { + auto const& expected = interestRateValue; + auto const actual = tx.getInterestRate(); + expectEqualField(expected, actual, "sfInterestRate"); + } + + { + auto const& expected = expirationValue; + auto const actual = tx.getExpiration(); + expectEqualField(expected, actual, "sfExpiration"); + } + + { + auto const& expected = maturityDateValue; + auto const actual = tx.getMaturityDate(); + expectEqualField(expected, actual, "sfMaturityDate"); + } + + { + auto const& expected = gracePeriodValue; + auto const actual = tx.getGracePeriod(); + expectEqualField(expected, actual, "sfGracePeriod"); + } + + // Verify optional fields + { + auto const& expected = dataValue; + auto const actualOpt = tx.getData(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfData should be present"; + expectEqualField(expected, *actualOpt, "sfData"); + EXPECT_TRUE(tx.hasData()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsRepoCreateTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testRepoCreateFromTx")); + + // 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 collateralAmountValue = canonical_AMOUNT(); + auto const purchasePriceValue = canonical_AMOUNT(); + auto const interestRateValue = canonical_UINT32(); + auto const expirationValue = canonical_UINT32(); + auto const maturityDateValue = canonical_UINT32(); + auto const gracePeriodValue = canonical_UINT32(); + auto const dataValue = canonical_VL(); + + // Build an initial transaction + RepoCreateBuilder initialBuilder{ + accountValue, + counterpartyValue, + collateralAmountValue, + purchasePriceValue, + interestRateValue, + expirationValue, + maturityDateValue, + gracePeriodValue, + sequenceValue, + feeValue + }; + + initialBuilder.setData(dataValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + RepoCreateBuilder 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 = counterpartyValue; + auto const actual = rebuiltTx.getCounterparty(); + expectEqualField(expected, actual, "sfCounterparty"); + } + + { + auto const& expected = collateralAmountValue; + auto const actual = rebuiltTx.getCollateralAmount(); + expectEqualField(expected, actual, "sfCollateralAmount"); + } + + { + auto const& expected = purchasePriceValue; + auto const actual = rebuiltTx.getPurchasePrice(); + expectEqualField(expected, actual, "sfPurchasePrice"); + } + + { + auto const& expected = interestRateValue; + auto const actual = rebuiltTx.getInterestRate(); + expectEqualField(expected, actual, "sfInterestRate"); + } + + { + auto const& expected = expirationValue; + auto const actual = rebuiltTx.getExpiration(); + expectEqualField(expected, actual, "sfExpiration"); + } + + { + auto const& expected = maturityDateValue; + auto const actual = rebuiltTx.getMaturityDate(); + expectEqualField(expected, actual, "sfMaturityDate"); + } + + { + auto const& expected = gracePeriodValue; + auto const actual = rebuiltTx.getGracePeriod(); + expectEqualField(expected, actual, "sfGracePeriod"); + } + + // Verify optional fields + { + auto const& expected = dataValue; + auto const actualOpt = rebuiltTx.getData(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfData should be present"; + expectEqualField(expected, *actualOpt, "sfData"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsRepoCreateTests, 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(RepoCreate{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsRepoCreateTests, 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(RepoCreateBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsRepoCreateTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testRepoCreateNullopt")); + + // 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 counterpartyValue = canonical_ACCOUNT(); + auto const collateralAmountValue = canonical_AMOUNT(); + auto const purchasePriceValue = canonical_AMOUNT(); + auto const interestRateValue = canonical_UINT32(); + auto const expirationValue = canonical_UINT32(); + auto const maturityDateValue = canonical_UINT32(); + auto const gracePeriodValue = canonical_UINT32(); + + RepoCreateBuilder builder{ + accountValue, + counterpartyValue, + collateralAmountValue, + purchasePriceValue, + interestRateValue, + expirationValue, + maturityDateValue, + gracePeriodValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasData()); + EXPECT_FALSE(tx.getData().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/RepoDefaultTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/RepoDefaultTests.cpp new file mode 100644 index 00000000000..d85401c257d --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/RepoDefaultTests.cpp @@ -0,0 +1,146 @@ +// Auto-generated unit tests for transaction RepoDefault + + +#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(TransactionsRepoDefaultTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testRepoDefault")); + + // 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 repoIDValue = canonical_UINT256(); + + RepoDefaultBuilder builder{ + accountValue, + repoIDValue, + 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 = repoIDValue; + auto const actual = tx.getRepoID(); + expectEqualField(expected, actual, "sfRepoID"); + } + + // Verify optional fields +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsRepoDefaultTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testRepoDefaultFromTx")); + + // 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 repoIDValue = canonical_UINT256(); + + // Build an initial transaction + RepoDefaultBuilder initialBuilder{ + accountValue, + repoIDValue, + sequenceValue, + feeValue + }; + + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + RepoDefaultBuilder 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 = repoIDValue; + auto const actual = rebuiltTx.getRepoID(); + expectEqualField(expected, actual, "sfRepoID"); + } + + // Verify optional fields +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsRepoDefaultTests, 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(RepoDefault{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsRepoDefaultTests, 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(RepoDefaultBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + + +} diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp index 5271720b34f..e8233371f92 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp @@ -212,6 +212,30 @@ parseCredential( return keylet::credential(*subject, *issuer, Slice(credType->data(), credType->size())).key; } +static std::expected +parseRepo( + json::Value const& params, + json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const seller = + ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress"); + if (!seller) + return std::unexpected(seller.error()); + + if (!params.isMember(jss::seq) || !params[jss::seq].isIntegral()) + { + return ledger_entry_helpers::invalidFieldError("malformedRequest", jss::seq, "number"); + } + + return keylet::repo(*seller, SeqProxy::rawSequence(params[jss::seq].asUInt())).key; +} + static std::expected parseDelegate( json::Value const& params,