featureRepo

This commit is contained in:
Denis Angell
2026-09-12 06:35:47 -04:00
parent 9403736199
commit 6badf9b5aa
39 changed files with 5137 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <cstdint>
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

View File

@@ -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)
{

View File

@@ -377,6 +377,8 @@ enum TECcodes : TERUnderlyingType {
tecNO_SPONSOR_PERMISSION = 200,
tecOUT_OF_GAS = 201,
tecBYTECODE_REJECTED = 202,
tecREPO_ACTIVE = 211,
tecREPO_PENDING = 212,
};
//------------------------------------------------------------------------------

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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 <xrpl/tx/transactors/repo/RepoCreate.h>
#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 <xrpl/tx/transactors/repo/RepoAccept.h>
#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 <xrpl/tx/transactors/repo/RepoCancel.h>
#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 <xrpl/tx/transactors/repo/RepoClose.h>
#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 <xrpl/tx/transactors/repo/RepoDefault.h>
#endif
TRANSACTION(ttREPO_DEFAULT, 118, RepoDefault,
({.delegable = Delegation::Delegable, .amendment = featureRepo}),
({
{sfRepoID, SoeRequired},
}))

View File

@@ -0,0 +1,525 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/LedgerEntryBase.h>
#include <xrpl/protocol_autogen/LedgerEntryBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::ledger_entries {
class 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<SF_UINT32::type::value_type>
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<SF_UINT32::type::value_type>
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<SF_VL::type::value_type>
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<SF_UINT64::type::value_type>
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<RepoBuilder>
{
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<typename SF_ACCOUNT::type::value_type> const& account,std::decay_t<typename SF_ACCOUNT::type::value_type> const& counterparty,std::decay_t<typename SF_AMOUNT::type::value_type> const& collateralAmount,std::decay_t<typename SF_AMOUNT::type::value_type> const& purchasePrice,std::decay_t<typename SF_UINT32::type::value_type> const& interestRate,std::decay_t<typename SF_UINT32::type::value_type> const& expiration,std::decay_t<typename SF_UINT32::type::value_type> const& maturityDate,std::decay_t<typename SF_UINT32::type::value_type> const& gracePeriod,std::decay_t<typename SF_UINT64::type::value_type> const& ownerNode,std::decay_t<typename SF_UINT64::type::value_type> const& destinationNode,std::decay_t<typename SF_UINT256::type::value_type> const& previousTxnID,std::decay_t<typename SF_UINT32::type::value_type> const& previousTxnLgrSeq)
: LedgerEntryBuilderBase<RepoBuilder>(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<typename SF_ACCOUNT::type::value_type> const& value)
{
object_[sfAccount] = value;
return *this;
}
/**
* @brief Set sfCounterparty (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setCounterparty(std::decay_t<typename SF_ACCOUNT::type::value_type> const& value)
{
object_[sfCounterparty] = value;
return *this;
}
/**
* @brief Set sfCollateralAmount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setCollateralAmount(std::decay_t<typename SF_AMOUNT::type::value_type> const& value)
{
object_[sfCollateralAmount] = value;
return *this;
}
/**
* @brief Set sfPurchasePrice (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setPurchasePrice(std::decay_t<typename SF_AMOUNT::type::value_type> const& value)
{
object_[sfPurchasePrice] = value;
return *this;
}
/**
* @brief Set sfInterestRate (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setInterestRate(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfInterestRate] = value;
return *this;
}
/**
* @brief Set sfExpiration (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setExpiration(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfExpiration] = value;
return *this;
}
/**
* @brief Set sfStartDate (SoeOptional)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setStartDate(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfStartDate] = value;
return *this;
}
/**
* @brief Set sfMaturityDate (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setMaturityDate(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfMaturityDate] = value;
return *this;
}
/**
* @brief Set sfGracePeriod (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setGracePeriod(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfGracePeriod] = value;
return *this;
}
/**
* @brief Set sfTransferRate (SoeOptional)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setTransferRate(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfTransferRate] = value;
return *this;
}
/**
* @brief Set sfData (SoeOptional)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setData(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfData] = value;
return *this;
}
/**
* @brief Set sfOwnerNode (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setOwnerNode(std::decay_t<typename SF_UINT64::type::value_type> const& value)
{
object_[sfOwnerNode] = value;
return *this;
}
/**
* @brief Set sfDestinationNode (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setDestinationNode(std::decay_t<typename SF_UINT64::type::value_type> const& value)
{
object_[sfDestinationNode] = value;
return *this;
}
/**
* @brief Set sfIssuerNode (SoeOptional)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setIssuerNode(std::decay_t<typename SF_UINT64::type::value_type> const& value)
{
object_[sfIssuerNode] = value;
return *this;
}
/**
* @brief Set sfPreviousTxnID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setPreviousTxnID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfPreviousTxnID] = value;
return *this;
}
/**
* @brief Set sfPreviousTxnLgrSeq (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoBuilder&
setPreviousTxnLgrSeq(std::decay_t<typename SF_UINT32::type::value_type> 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<SLE>(std::move(object_), index)};
}
};
} // namespace xrpl::ledger_entries

View File

@@ -0,0 +1,131 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class 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<STTx const> 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<RepoAcceptBuilder>
{
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<typename SF_UINT256::type::value_type> const& repoID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<RepoAcceptBuilder>(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<STTx const> 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<typename SF_UINT256::type::value_type> 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<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -0,0 +1,131 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class 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<STTx const> 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<RepoCancelBuilder>
{
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<typename SF_UINT256::type::value_type> const& repoID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<RepoCancelBuilder>(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<STTx const> 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<typename SF_UINT256::type::value_type> 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<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -0,0 +1,131 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class 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<STTx const> 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<RepoCloseBuilder>
{
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<typename SF_UINT256::type::value_type> const& repoID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<RepoCloseBuilder>(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<STTx const> 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<typename SF_UINT256::type::value_type> 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<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -0,0 +1,316 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class 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<STTx const> 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<SF_VL::type::value_type>
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<RepoCreateBuilder>
{
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<typename SF_ACCOUNT::type::value_type> const& counterparty, std::decay_t<typename SF_AMOUNT::type::value_type> const& collateralAmount, std::decay_t<typename SF_AMOUNT::type::value_type> const& purchasePrice, std::decay_t<typename SF_UINT32::type::value_type> const& interestRate, std::decay_t<typename SF_UINT32::type::value_type> const& expiration, std::decay_t<typename SF_UINT32::type::value_type> const& maturityDate, std::decay_t<typename SF_UINT32::type::value_type> const& gracePeriod, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<RepoCreateBuilder>(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<STTx const> 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<typename SF_ACCOUNT::type::value_type> 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<typename SF_AMOUNT::type::value_type> 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<typename SF_AMOUNT::type::value_type> const& value)
{
object_[sfPurchasePrice] = value;
return *this;
}
/**
* @brief Set sfInterestRate (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoCreateBuilder&
setInterestRate(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfInterestRate] = value;
return *this;
}
/**
* @brief Set sfExpiration (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoCreateBuilder&
setExpiration(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfExpiration] = value;
return *this;
}
/**
* @brief Set sfMaturityDate (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoCreateBuilder&
setMaturityDate(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfMaturityDate] = value;
return *this;
}
/**
* @brief Set sfGracePeriod (SoeRequired)
* @return Reference to this builder for method chaining.
*/
RepoCreateBuilder&
setGracePeriod(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfGracePeriod] = value;
return *this;
}
/**
* @brief Set sfData (SoeOptional)
* @return Reference to this builder for method chaining.
*/
RepoCreateBuilder&
setData(std::decay_t<typename SF_VL::type::value_type> 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<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -0,0 +1,131 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class 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<STTx const> 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<RepoDefaultBuilder>
{
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<typename SF_UINT256::type::value_type> const& repoID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<RepoDefaultBuilder>(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<STTx const> 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<typename SF_UINT256::type::value_type> 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<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -15,6 +15,7 @@
#include <xrpl/tx/invariants/NFTInvariant.h>
#include <xrpl/tx/invariants/PermissionedDEXInvariant.h>
#include <xrpl/tx/invariants/PermissionedDomainInvariant.h>
#include <xrpl/tx/invariants/RepoInvariant.h>
#include <xrpl/tx/invariants/SponsorshipInvariant.h>
#include <xrpl/tx/invariants/VaultInvariant.h>
@@ -449,6 +450,7 @@ using InvariantChecks = std::tuple<
ValidClawback,
ValidMPTIssuance,
ValidPermissionedDomain,
ValidRepo,
ValidPermissionedDEX,
ValidBookDirectory,
ValidAMM,

View File

@@ -0,0 +1,44 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <utility>
#include <vector>
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, after>. before is unseated when the entry is being created.
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> 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

View File

@@ -0,0 +1,55 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
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

View File

@@ -0,0 +1,55 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
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

View File

@@ -0,0 +1,55 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
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

View File

@@ -0,0 +1,55 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
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

View File

@@ -0,0 +1,55 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
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

View File

@@ -0,0 +1,263 @@
#include <xrpl/ledger/helpers/RepoHelpers.h>
#include <xrpl/basics/Number.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <algorithm>
#include <variant>
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<MPTIssue>())
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<Issue>();
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<MPTIssue>();
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<MPTIssue>())
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(
[&]<typename T>(T const&) {
return escrowUnlockApplyHelper<T>(
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

View File

@@ -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

View File

@@ -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."),

View File

@@ -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))
{

View File

@@ -0,0 +1,126 @@
#include <xrpl/tx/invariants/RepoInvariant.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAccount.h> // IWYU pragma: keep
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TxFormats.h>
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

View File

@@ -0,0 +1,87 @@
#include <xrpl/tx/transactors/repo/RepoAccept.h>
#include <xrpl/basics/Log.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/RepoHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
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

View File

@@ -0,0 +1,76 @@
#include <xrpl/tx/transactors/repo/RepoCancel.h>
#include <xrpl/basics/Log.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/RepoHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
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

View File

@@ -0,0 +1,87 @@
#include <xrpl/tx/transactors/repo/RepoClose.h>
#include <xrpl/basics/Log.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/RepoHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
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

View File

@@ -0,0 +1,240 @@
#include <xrpl/tx/transactors/repo/RepoCreate.h>
#include <xrpl/basics/Log.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/RepoHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <memory>
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<SLE>(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<MPTIssue>())
{
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

View File

@@ -0,0 +1,77 @@
#include <xrpl/tx/transactors/repo/RepoDefault.h>
#include <xrpl/basics/Log.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/RepoHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
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

655
src/test/app/Repo_test.cpp Normal file
View File

@@ -0,0 +1,655 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/acctdelete.h>
#include <test/jtx/amount.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/mpt.h>
#include <test/jtx/owners.h>
#include <test/jtx/pay.h>
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <test/jtx/utility.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/jss.h>
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

View File

@@ -0,0 +1,239 @@
#include <test/app/invariants/InvariantsBase.h>
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/pay.h>
#include <test/jtx/trust.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/jss.h>
#include <functional>
#include <string>
#include <vector>
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<void(SLE::pointer&)> func;
};
std::vector<Mod> 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

View File

@@ -0,0 +1,488 @@
// Auto-generated unit tests for ledger entry Repo
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol_autogen/ledger_entries/Repo.h>
#include <xrpl/protocol_autogen/ledger_entries/Ticket.h>
#include <string>
namespace xrpl::ledger_entries {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed for both the
// builder's STObject and the wrapper's SLE.
TEST(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<SLE>(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());
}
}

View File

@@ -0,0 +1,146 @@
// Auto-generated unit tests for transaction RepoAccept
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/RepoAccept.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(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);
}
}

View File

@@ -0,0 +1,146 @@
// Auto-generated unit tests for transaction RepoCancel
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/RepoCancel.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(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);
}
}

View File

@@ -0,0 +1,146 @@
// Auto-generated unit tests for transaction RepoClose
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/RepoClose.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(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);
}
}

View File

@@ -0,0 +1,303 @@
// Auto-generated unit tests for transaction RepoCreate
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/RepoCreate.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(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());
}
}

View File

@@ -0,0 +1,146 @@
// Auto-generated unit tests for transaction RepoDefault
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/RepoDefault.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(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);
}
}

View File

@@ -212,6 +212,30 @@ parseCredential(
return keylet::credential(*subject, *issuer, Slice(credType->data(), credType->size())).key;
}
static std::expected<uint256, json::Value>
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<uint256, json::Value>
parseDelegate(
json::Value const& params,