mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-13 18:20:22 +00:00
Compare commits
16 Commits
bthomee/no
...
a1q123456/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee7282d62c | ||
|
|
23e19827d7 | ||
|
|
7f3a0cbb8a | ||
|
|
9a24c623ee | ||
|
|
080d5de281 | ||
|
|
f2444ff61f | ||
|
|
b25eb1871c | ||
|
|
2b9c78a752 | ||
|
|
1f3382c3d3 | ||
|
|
dd6c4e35c0 | ||
|
|
6d84c1c422 | ||
|
|
c509ad3474 | ||
|
|
7432427045 | ||
|
|
4d92e6a19d | ||
|
|
6647b60167 | ||
|
|
e538aeb59a |
@@ -356,3 +356,4 @@ words:
|
||||
- xxhash
|
||||
- xxhasher
|
||||
- CGNAT
|
||||
- unimpairs
|
||||
|
||||
@@ -208,8 +208,8 @@ enum LedgerEntryType : std::uint16_t {
|
||||
LEDGER_OBJECT(Loan, \
|
||||
LSF_FLAG(lsfLoanDefault, 0x00010000) \
|
||||
LSF_FLAG(lsfLoanImpaired, 0x00020000) \
|
||||
LSF_FLAG(lsfLoanOverpayment, 0x00040000)) /* True, loan allows overpayments */
|
||||
|
||||
LSF_FLAG(lsfLoanOverpayment, 0x00040000) /* True, loan allows overpayments */ \
|
||||
LSF_FLAG(lsfLoanPending, 0x00080000)) /* True, loan is pending acceptance by the borrower */
|
||||
// clang-format on
|
||||
|
||||
// Create all the flag values as an enum.
|
||||
|
||||
@@ -500,6 +500,7 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
|
||||
{sfShareMPTID, SoeRequired},
|
||||
{sfWithdrawalPolicy, SoeRequired},
|
||||
{sfScale, SoeDefault},
|
||||
{sfAssetsReserved, SoeDefault},
|
||||
// no SharesTotal ever (use MPTIssuance.sfOutstandingAmount)
|
||||
// no PermissionedDomainID ever (use MPTIssuance.sfDomainID)
|
||||
}))
|
||||
@@ -537,7 +538,7 @@ LEDGER_ENTRY(ltLOAN_BROKER, 0x0088, LoanBroker, loan_broker, ({
|
||||
LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({
|
||||
{sfPreviousTxnID, SoeRequired},
|
||||
{sfPreviousTxnLgrSeq, SoeRequired},
|
||||
{sfOwnerNode, SoeRequired},
|
||||
{sfOwnerNode, SoeOptional},
|
||||
{sfLoanBrokerNode, SoeRequired},
|
||||
{sfLoanBrokerID, SoeRequired},
|
||||
{sfLoanSequence, SoeRequired},
|
||||
|
||||
@@ -228,6 +228,7 @@ TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14)
|
||||
TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16)
|
||||
TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfAssetsReserved, NUMBER, 18, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
|
||||
// int32
|
||||
TYPED_SFIELD(sfLoanScale, INT32, 1)
|
||||
|
||||
@@ -1026,6 +1026,7 @@ TRANSACTION(ttLOAN_SET, 80, LoanSet,
|
||||
MayAuthorizeMpt | MustModifyVault, ({
|
||||
{sfLoanBrokerID, SoeRequired},
|
||||
{sfData, SoeOptional},
|
||||
{sfBorrower, SoeOptional},
|
||||
{sfCounterparty, SoeOptional},
|
||||
{sfCounterpartySignature, SoeOptional},
|
||||
{sfLoanOriginationFee, SoeOptional},
|
||||
@@ -1041,6 +1042,7 @@ TRANSACTION(ttLOAN_SET, 80, LoanSet,
|
||||
{sfPaymentTotal, SoeOptional},
|
||||
{sfPaymentInterval, SoeOptional},
|
||||
{sfGracePeriod, SoeOptional},
|
||||
{sfStartDate, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This transaction deletes an existing Loan */
|
||||
@@ -1068,6 +1070,17 @@ TRANSACTION(ttLOAN_MANAGE, 82, LoanManage,
|
||||
{sfLoanID, SoeRequired},
|
||||
}))
|
||||
|
||||
/** This transaction accepts a Loan. Currently a no-op. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/lending/LoanAccept.h>
|
||||
#endif
|
||||
TRANSACTION(ttLOAN_ACCEPT, 83, LoanAccept,
|
||||
Delegation::NotDelegable,
|
||||
featureLendingProtocolV1_1,
|
||||
NoPriv, ({
|
||||
{sfLoanID, SoeRequired},
|
||||
}))
|
||||
|
||||
/** The Borrower uses this transaction to make a Payment on the Loan. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/lending/LoanPay.h>
|
||||
|
||||
@@ -68,14 +68,27 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfOwnerNode (SoeRequired)
|
||||
* @return The field value.
|
||||
* @brief Get sfOwnerNode (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT64::type::value_type
|
||||
protocol_autogen::Optional<SF_UINT64::type::value_type>
|
||||
getOwnerNode() const
|
||||
{
|
||||
return this->sle_->at(sfOwnerNode);
|
||||
if (hasOwnerNode())
|
||||
return this->sle_->at(sfOwnerNode);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfOwnerNode is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasOwnerNode() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfOwnerNode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -578,7 +591,6 @@ public:
|
||||
* @brief Construct a new LoanBuilder with required fields.
|
||||
* @param previousTxnID The sfPreviousTxnID field value.
|
||||
* @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value.
|
||||
* @param ownerNode The sfOwnerNode field value.
|
||||
* @param loanBrokerNode The sfLoanBrokerNode field value.
|
||||
* @param loanBrokerID The sfLoanBrokerID field value.
|
||||
* @param loanSequence The sfLoanSequence field value.
|
||||
@@ -587,12 +599,11 @@ public:
|
||||
* @param paymentInterval The sfPaymentInterval field value.
|
||||
* @param periodicPayment The sfPeriodicPayment field value.
|
||||
*/
|
||||
LoanBuilder(std::decay_t<typename SF_UINT256::type::value_type> const& previousTxnID,std::decay_t<typename SF_UINT32::type::value_type> const& previousTxnLgrSeq,std::decay_t<typename SF_UINT64::type::value_type> const& ownerNode,std::decay_t<typename SF_UINT64::type::value_type> const& loanBrokerNode,std::decay_t<typename SF_UINT256::type::value_type> const& loanBrokerID,std::decay_t<typename SF_UINT32::type::value_type> const& loanSequence,std::decay_t<typename SF_ACCOUNT::type::value_type> const& borrower,std::decay_t<typename SF_UINT32::type::value_type> const& startDate,std::decay_t<typename SF_UINT32::type::value_type> const& paymentInterval,std::decay_t<typename SF_NUMBER::type::value_type> const& periodicPayment)
|
||||
LoanBuilder(std::decay_t<typename SF_UINT256::type::value_type> const& previousTxnID,std::decay_t<typename SF_UINT32::type::value_type> const& previousTxnLgrSeq,std::decay_t<typename SF_UINT64::type::value_type> const& loanBrokerNode,std::decay_t<typename SF_UINT256::type::value_type> const& loanBrokerID,std::decay_t<typename SF_UINT32::type::value_type> const& loanSequence,std::decay_t<typename SF_ACCOUNT::type::value_type> const& borrower,std::decay_t<typename SF_UINT32::type::value_type> const& startDate,std::decay_t<typename SF_UINT32::type::value_type> const& paymentInterval,std::decay_t<typename SF_NUMBER::type::value_type> const& periodicPayment)
|
||||
: LedgerEntryBuilderBase<LoanBuilder>(ltLOAN)
|
||||
{
|
||||
setPreviousTxnID(previousTxnID);
|
||||
setPreviousTxnLgrSeq(previousTxnLgrSeq);
|
||||
setOwnerNode(ownerNode);
|
||||
setLoanBrokerNode(loanBrokerNode);
|
||||
setLoanBrokerID(loanBrokerID);
|
||||
setLoanSequence(loanSequence);
|
||||
@@ -641,7 +652,7 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfOwnerNode (SoeRequired)
|
||||
* @brief Set sfOwnerNode (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
LoanBuilder&
|
||||
|
||||
@@ -287,6 +287,30 @@ public:
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfAssetsReserved (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_NUMBER::type::value_type>
|
||||
getAssetsReserved() const
|
||||
{
|
||||
if (hasAssetsReserved())
|
||||
return this->sle_->at(sfAssetsReserved);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfAssetsReserved is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasAssetsReserved() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfAssetsReserved);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -506,6 +530,17 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfAssetsReserved (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultBuilder&
|
||||
setAssetsReserved(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
|
||||
{
|
||||
object_[sfAssetsReserved] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the completed Vault wrapper.
|
||||
* @param index The ledger entry index.
|
||||
|
||||
129
include/xrpl/protocol_autogen/transactions/LoanAccept.h
Normal file
129
include/xrpl/protocol_autogen/transactions/LoanAccept.h
Normal file
@@ -0,0 +1,129 @@
|
||||
// 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 LoanAcceptBuilder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: LoanAccept
|
||||
*
|
||||
* Type: ttLOAN_ACCEPT (83)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureLendingProtocolV1_1
|
||||
* Privileges: NoPriv
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use LoanAcceptBuilder to construct new transactions.
|
||||
*/
|
||||
class LoanAccept : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ttLOAN_ACCEPT;
|
||||
|
||||
/**
|
||||
* @brief Construct a LoanAccept transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit LoanAccept(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 LoanAccept");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
|
||||
/**
|
||||
* @brief Get sfLoanID (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT256::type::value_type
|
||||
getLoanID() const
|
||||
{
|
||||
return this->tx_->at(sfLoanID);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for LoanAccept 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 LoanAcceptBuilder : public TransactionBuilderBase<LoanAcceptBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new LoanAcceptBuilder with required fields.
|
||||
* @param account The account initiating the transaction.
|
||||
* @param loanID The sfLoanID field value.
|
||||
* @param sequence Optional sequence number for the transaction.
|
||||
* @param fee Optional fee for the transaction.
|
||||
*/
|
||||
LoanAcceptBuilder(SF_ACCOUNT::type::value_type account,
|
||||
std::decay_t<typename SF_UINT256::type::value_type> const& loanID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
|
||||
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
|
||||
)
|
||||
: TransactionBuilderBase<LoanAcceptBuilder>(ttLOAN_ACCEPT, account, sequence, fee)
|
||||
{
|
||||
setLoanID(loanID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a LoanAcceptBuilder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
LoanAcceptBuilder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ttLOAN_ACCEPT)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for LoanAcceptBuilder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/** @brief Transaction-specific field setters */
|
||||
|
||||
/**
|
||||
* @brief Set sfLoanID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
LoanAcceptBuilder&
|
||||
setLoanID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
|
||||
{
|
||||
object_[sfLoanID] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the LoanAccept wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
LoanAccept
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return LoanAccept{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
@@ -84,6 +84,32 @@ public:
|
||||
return this->tx_->isFieldPresent(sfData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfBorrower (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_ACCOUNT::type::value_type>
|
||||
getBorrower() const
|
||||
{
|
||||
if (hasBorrower())
|
||||
{
|
||||
return this->tx_->at(sfBorrower);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfBorrower is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasBorrower() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfBorrower);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfCounterparty (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -524,6 +550,17 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfBorrower (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
LoanSetBuilder&
|
||||
setBorrower(std::decay_t<typename SF_ACCOUNT::type::value_type> const& value)
|
||||
{
|
||||
object_[sfBorrower] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfCounterparty (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -17,12 +17,60 @@ namespace xrpl {
|
||||
*
|
||||
* 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`
|
||||
*
|
||||
* The following invariants only apply once the `LendingProtocolV1_1`
|
||||
* amendment is enabled (the two-step "pending loan" flow):
|
||||
*
|
||||
* 2. A loan's `OwnerNode` may only be added, removed, or changed on an
|
||||
* existing loan by `LoanAccept`.
|
||||
* 3. A loan's `lsfLoanPending` flag may only be cleared (never set) on an
|
||||
* existing loan, and only by `LoanAccept`.
|
||||
* 4. A `LoanAccept` may only modify an existing loan when:
|
||||
* a. the loan was pending (`lsfLoanPending` set);
|
||||
* b. the submitting account (`Account`) is the loan's `Borrower`;
|
||||
* c. the loan's `StartDate` is still in the future.
|
||||
* 5. A `LoanSet` that creates a loan must use exactly one of two mutually
|
||||
* exclusive creation paths: it either names a `Borrower`, or it carries a
|
||||
* a `CounterpartySignature`. Specifically:
|
||||
* a. If `Borrower` is present, `Counterparty` and `CounterpartySignature`
|
||||
* must be absent.
|
||||
* b. If `CounterpartySignature` is present, `Borrower` must be absent.
|
||||
* c. Either `Borrower`, or `CounterpartySignature`,
|
||||
* must be present.
|
||||
* d. If `Borrower` is present, it must differ from the submitting
|
||||
* `Account`.
|
||||
* 6. A `LoanSet` that creates a loan must set `lsfLoanPending` if and only if it
|
||||
* starts the two-step flow, i.e. `Borrower` and `StartDate` are present while
|
||||
* `Counterparty` and `CounterpartySignature` are absent.
|
||||
* 7. A `LoanSet` that creates a loan must set the loan's `Borrower`.
|
||||
* Additionally, a pending loan's `StartDate` must be in the future (so it is
|
||||
* still in the future when `LoanAccept` finalizes it).
|
||||
* 8. A pending loan (`lsfLoanPending` set) must not be linked into the
|
||||
* borrower's directory (`OwnerNode` absent), and a non-pending loan must
|
||||
* be linked (`OwnerNode` present).
|
||||
*
|
||||
* While the `LendingProtocolV1_1` amendment is not enabled, a `LoanSet` that
|
||||
* creates a loan must not use any of the two-step flow's inputs:
|
||||
*
|
||||
* 9. It must not create a pending loan (`lsfLoanPending` must be clear).
|
||||
* 10. It must not be given a `Borrower`.
|
||||
* 11. It must always carry a `CounterpartySignature`.
|
||||
*
|
||||
* A loan may only be deleted once it is fully paid off (no payments remaining)
|
||||
* or, once the `LendingProtocolV1_1` amendment is enabled, while it is still
|
||||
* pending:
|
||||
*
|
||||
* 12. A loan may only be deleted by a `LoanDelete` transaction.
|
||||
* 13. A pending loan must not be deleted while the amendment is not enabled.
|
||||
* 14. A loan that is neither fully paid off nor pending must not be deleted.
|
||||
*
|
||||
*/
|
||||
class ValidLoan
|
||||
{
|
||||
// Pair is <before, after>. After is used for most of the checks, except
|
||||
// those that check changed values.
|
||||
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> loans_;
|
||||
// Loans removed from the ledger (final state captured at deletion).
|
||||
std::vector<SLE::const_pointer> deletedLoans_;
|
||||
|
||||
public:
|
||||
void
|
||||
|
||||
@@ -36,8 +36,27 @@ namespace xrpl {
|
||||
* - vault withdrawal and clawback reduce assets and share issuance, and
|
||||
* subtracts from: total assets, assets available, shares outstanding
|
||||
* - vault set must not alter the vault assets or shares balance
|
||||
* - loan set moves the requested principal out of the vault: it must create
|
||||
* exactly one loan, decreases assets available (and the vault balance) by the
|
||||
* principal, grows assets outstanding by exactly the interest due booked on
|
||||
* the created loan, and does not change shares
|
||||
* - loan manage never removes assets from the vault: assets available may only
|
||||
* grow (and the vault balance grows with it, by the returned first-loss
|
||||
* capital on a default), assets outstanding may only shrink (realized loss),
|
||||
* loss unrealized stays non-negative, and shares do not change; a loan
|
||||
* manage with none of the sub-operation flags (impair, unimpair, default)
|
||||
* is a no-op and must not modify the vault
|
||||
* - loan pay adds the paid principal and interest to the vault: assets
|
||||
* available (and the vault balance) increase by the same amount, which is at
|
||||
* most the amount paid, loss unrealized stays non-negative, and shares do not
|
||||
* change; and assets outstanding move in lock-step: their change equals the
|
||||
* cash received plus the change in the paid loan's claim on the vault, which
|
||||
* verifies the payment was split correctly between principal and interest
|
||||
* - no vault transaction can change loss unrealized (it's updated by loan
|
||||
* transactions)
|
||||
* - AssetsReserved (principal held back for pending loans) may only be changed
|
||||
* by LoanDelete, LoanAccept, or a LoanSet that creates a pending loan
|
||||
* (Borrower present, CounterpartySignature absent)
|
||||
*
|
||||
*/
|
||||
class ValidVault
|
||||
@@ -55,6 +74,9 @@ class ValidVault
|
||||
Number assetsAvailable = 0;
|
||||
Number assetsMaximum = 0;
|
||||
Number lossUnrealized = 0;
|
||||
Number assetsReserved = 0;
|
||||
std::uint8_t withdrawalPolicy = 0;
|
||||
std::uint8_t scale = 0;
|
||||
|
||||
Vault static make(SLE const&);
|
||||
};
|
||||
@@ -68,6 +90,26 @@ class ValidVault
|
||||
Shares static make(SLE const&);
|
||||
};
|
||||
|
||||
struct Loan final
|
||||
{
|
||||
uint256 key = beast::kZero;
|
||||
Number principalOutstanding = 0;
|
||||
Number totalValueOutstanding = 0;
|
||||
Number managementFeeOutstanding = 0;
|
||||
|
||||
// Interest booked to the vault at loan creation: the portion of the
|
||||
// total value owed that is neither principal nor broker management fee.
|
||||
[[nodiscard]] Number
|
||||
interestDue() const;
|
||||
|
||||
// The vault's claim on the loan: the total value owed less the broker's
|
||||
// management fee (which belongs to the broker, not the vault).
|
||||
[[nodiscard]] Number
|
||||
claim() const;
|
||||
|
||||
Loan static make(SLE const&);
|
||||
};
|
||||
|
||||
public:
|
||||
struct DeltaInfo final
|
||||
{
|
||||
@@ -82,8 +124,10 @@ public:
|
||||
private:
|
||||
std::vector<Vault> afterVault_;
|
||||
std::vector<Shares> afterMPTs_;
|
||||
std::vector<Loan> afterLoan_;
|
||||
std::vector<Vault> beforeVault_;
|
||||
std::vector<Shares> beforeMPTs_;
|
||||
std::vector<Loan> beforeLoan_;
|
||||
std::unordered_map<uint256, DeltaInfo> deltas_;
|
||||
|
||||
/**
|
||||
|
||||
42
include/xrpl/tx/transactors/lending/LoanAccept.h
Normal file
42
include/xrpl/tx/transactors/lending/LoanAccept.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.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>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class LoanAccept : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
|
||||
explicit LoanAccept(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext 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
|
||||
@@ -18,6 +18,11 @@ namespace xrpl {
|
||||
|
||||
class LoanSet : public Transactor
|
||||
{
|
||||
private:
|
||||
/* Returns true if the transaction is using the two-step (Borrower) flow. */
|
||||
static bool
|
||||
isTwoStepFlow(STTx const& tx, Rules const& rules);
|
||||
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -17,7 +19,13 @@ namespace xrpl {
|
||||
void
|
||||
ValidLoan::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
if (after && after->getType() == ltLOAN)
|
||||
if (isDelete)
|
||||
{
|
||||
// On deletion `after` holds the loan's final state.
|
||||
if (after && after->getType() == ltLOAN)
|
||||
deletedLoans_.emplace_back(after);
|
||||
}
|
||||
else if (after && after->getType() == ltLOAN)
|
||||
{
|
||||
loans_.emplace_back(before, after);
|
||||
}
|
||||
@@ -34,6 +42,10 @@ ValidLoan::finalize(
|
||||
// Loans will not exist on ledger if the Lending Protocol amendment
|
||||
// is not enabled, so there's no need to check it.
|
||||
|
||||
auto const txType = tx.getTxnType();
|
||||
// The pending-loan checks below only apply once the two-step flow exists.
|
||||
bool const lendingV11Enabled = view.rules().enabled(featureLendingProtocolV1_1);
|
||||
|
||||
for (auto const& [before, after] : loans_)
|
||||
{
|
||||
// https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants
|
||||
@@ -62,6 +74,197 @@ ValidLoan::finalize(
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan Overpayment flag changed";
|
||||
return false;
|
||||
}
|
||||
if (before)
|
||||
{
|
||||
// The lsfLoanPending flag may only be cleared (finalising the
|
||||
// loan), and only by LoanAccept. It must never be set on an
|
||||
// existing loan.
|
||||
bool const wasPending = before->isFlag(lsfLoanPending);
|
||||
bool const isPending = after->isFlag(lsfLoanPending);
|
||||
|
||||
if (!lendingV11Enabled && (wasPending || isPending))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan Pending flag changed "
|
||||
"when the amendment is not enabled";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wasPending != isPending &&
|
||||
(!lendingV11Enabled || txType != ttLOAN_ACCEPT || (!wasPending && isPending)))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan Pending flag changed "
|
||||
"by an unauthorized transaction";
|
||||
return false;
|
||||
}
|
||||
|
||||
// LoanAccept may only process a loan that was pending.
|
||||
if (txType == ttLOAN_ACCEPT && !wasPending)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LoanAccept modified a "
|
||||
"Loan that was not pending";
|
||||
return false;
|
||||
}
|
||||
|
||||
// The OwnerNode may only be added to an existing loan, and only by
|
||||
// LoanAccept (which links the loan into the borrower's directory
|
||||
// once accepted). It must never be removed or changed.
|
||||
bool const beforeHasNode = before->isFieldPresent(sfOwnerNode);
|
||||
bool const afterHasNode = after->isFieldPresent(sfOwnerNode);
|
||||
if (beforeHasNode &&
|
||||
(!afterHasNode ||
|
||||
before->getFieldU64(sfOwnerNode) != after->getFieldU64(sfOwnerNode)))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan OwnerNode removed "
|
||||
"or changed";
|
||||
return false;
|
||||
}
|
||||
if (!beforeHasNode && afterHasNode && (!lendingV11Enabled || txType != ttLOAN_ACCEPT))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan OwnerNode added "
|
||||
"by an unauthorized transaction";
|
||||
return false;
|
||||
}
|
||||
|
||||
// LoanAccept must be submitted by the loan's Borrower, and may only
|
||||
// finalise a loan whose StartDate is still in the future.
|
||||
if (lendingV11Enabled && txType == ttLOAN_ACCEPT)
|
||||
{
|
||||
if (after->getAccountID(sfBorrower) != tx.getAccountID(sfAccount))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LoanAccept submitted "
|
||||
"by an account other than the Borrower";
|
||||
return false;
|
||||
}
|
||||
if (after->getFieldU32(sfStartDate) <=
|
||||
view.parentCloseTime().time_since_epoch().count())
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LoanAccept processed a "
|
||||
"Loan whose StartDate is not in the future";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// On creation, LoanSet must use exactly one of two mutually exclusive
|
||||
// paths: it either names a Borrower (starting the two-step flow) or
|
||||
// carries a Counterparty together with its CounterpartySignature. A
|
||||
// Borrower with no CounterpartySignature starts the two-step flow and
|
||||
// must create a pending loan; any other LoanSet must create an active
|
||||
// (non-pending) loan.
|
||||
if (lendingV11Enabled && !before && txType == ttLOAN_SET)
|
||||
{
|
||||
bool const hasBorrower = tx.isFieldPresent(sfBorrower);
|
||||
bool const hasCounterpartySig = tx.isFieldPresent(sfCounterpartySignature);
|
||||
bool const hasStartDate = tx.isFieldPresent(sfStartDate);
|
||||
|
||||
// If a LoanSet carries a Counterparty and a CounterpartySignature it
|
||||
// must not also name a Borrower.
|
||||
if (hasCounterpartySig && hasBorrower)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LoanSet specified a "
|
||||
"Counterparty and CounterpartySignature "
|
||||
"together with a Borrower";
|
||||
return false;
|
||||
}
|
||||
// If a LoanSet names a Borrower it must not also carry a
|
||||
// Counterparty or a CounterpartySignature.
|
||||
if (hasBorrower && hasCounterpartySig)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LoanSet specified a "
|
||||
"Borrower together with a Counterparty or "
|
||||
"CounterpartySignature";
|
||||
return false;
|
||||
}
|
||||
// A LoanSet must use one of the two creation paths.
|
||||
if (!hasBorrower && !hasCounterpartySig)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LoanSet specified neither "
|
||||
"a Borrower nor a CounterpartySignature";
|
||||
return false;
|
||||
}
|
||||
// In the two-step flow the named Borrower must be a different
|
||||
// account from the one submitting the LoanSet.
|
||||
if (hasBorrower && tx.getAccountID(sfBorrower) == tx.getAccountID(sfAccount))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LoanSet Borrower is the "
|
||||
"submitting account";
|
||||
return false;
|
||||
}
|
||||
|
||||
// The two-step flow is started (and the loan created pending) when
|
||||
// the LoanSet names a Borrower and a StartDate but carries neither a
|
||||
// Counterparty nor a CounterpartySignature.
|
||||
bool const shouldPend = hasBorrower && hasStartDate && !hasCounterpartySig;
|
||||
if (shouldPend != after->isFlag(lsfLoanPending))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LoanSet pending flag does "
|
||||
"not match the two-step flow inputs";
|
||||
return false;
|
||||
}
|
||||
|
||||
// A pending loan (the two-step flow) is accepted in a later ledger,
|
||||
// so its StartDate must be strictly in the future at creation to
|
||||
// remain in the future when LoanAccept finalizes it.
|
||||
if (shouldPend &&
|
||||
after->getFieldU32(sfStartDate) <=
|
||||
view.parentCloseTime().time_since_epoch().count())
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LoanSet created a pending "
|
||||
"Loan whose StartDate is not in the future";
|
||||
return false;
|
||||
}
|
||||
|
||||
// A created loan must always record its Borrower.
|
||||
if (!after->isFieldPresent(sfBorrower))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LoanSet did not set the "
|
||||
"Loan Borrower";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Without the two-step flow amendment, LoanSet must not make use of any
|
||||
// of its inputs: it must not create a pending loan, must not be given a
|
||||
// Borrower, and must always carry a CounterpartySignature.
|
||||
if (!lendingV11Enabled && !before && txType == ttLOAN_SET)
|
||||
{
|
||||
if (after->isFlag(lsfLoanPending))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LoanSet set the Loan "
|
||||
"Pending flag when the amendment is not enabled";
|
||||
return false;
|
||||
}
|
||||
if (tx.isFieldPresent(sfBorrower))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LoanSet specified a "
|
||||
"Borrower when the amendment is not enabled";
|
||||
return false;
|
||||
}
|
||||
if (!tx.isFieldPresent(sfCounterpartySignature))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LoanSet omitted the "
|
||||
"CounterpartySignature when the amendment is "
|
||||
"not enabled";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// A pending loan must not be linked into the borrower's directory, and
|
||||
// a non-pending loan must be linked.
|
||||
if (lendingV11Enabled)
|
||||
{
|
||||
bool const isPending = after->isFlag(lsfLoanPending);
|
||||
bool const hasNode = after->isFieldPresent(sfOwnerNode);
|
||||
if (isPending && hasNode)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: pending Loan is linked "
|
||||
"into the borrower's directory";
|
||||
return false;
|
||||
}
|
||||
if (!isPending && !hasNode)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: active Loan is not linked "
|
||||
"into the borrower's directory";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Must not be negative - STNumber
|
||||
for (auto const field :
|
||||
{&sfLoanServiceFee,
|
||||
@@ -77,6 +280,15 @@ ValidLoan::finalize(
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Interest due (the total value owed less principal and management fee)
|
||||
// must never be negative.
|
||||
if (after->at(sfTotalValueOutstanding) - after->at(sfPrincipalOutstanding) -
|
||||
after->at(sfManagementFeeOutstanding) <
|
||||
beast::kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan interest due is negative";
|
||||
return false;
|
||||
}
|
||||
// Must be positive - STNumber
|
||||
for (auto const field : {
|
||||
&sfPeriodicPayment,
|
||||
@@ -90,6 +302,42 @@ ValidLoan::finalize(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A loan may only be deleted by a LoanDelete transaction, and only once it
|
||||
// is fully paid off (no payments remaining) or, once the two-step flow
|
||||
// exists, while it is still pending acceptance. Deleting an active loan with
|
||||
// outstanding obligations is a violation.
|
||||
for (auto const& loan : deletedLoans_)
|
||||
{
|
||||
if (txType != ttLOAN_DELETE)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan deleted by a transaction "
|
||||
"other than LoanDelete";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool const wasPending = loan->isFlag(lsfLoanPending);
|
||||
// A pending loan cannot exist while the amendment is not enabled.
|
||||
if (wasPending && !lendingV11Enabled)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: pending Loan deleted when the "
|
||||
"amendment is not enabled";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool const paidOff = loan->at(sfPaymentRemaining) == 0 &&
|
||||
loan->at(sfTotalValueOutstanding) == beast::kZero &&
|
||||
loan->at(sfPrincipalOutstanding) == beast::kZero &&
|
||||
loan->at(sfManagementFeeOutstanding) == beast::kZero;
|
||||
bool const pending = lendingV11Enabled && wasPending;
|
||||
if (!paidOff && !pending)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan deleted while not fully "
|
||||
"paid off and not pending";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
|
||||
@@ -44,6 +45,9 @@ ValidVault::Vault::make(SLE const& from)
|
||||
self.assetsAvailable = from.at(sfAssetsAvailable);
|
||||
self.assetsMaximum = from.at(sfAssetsMaximum);
|
||||
self.lossUnrealized = from.at(sfLossUnrealized);
|
||||
self.assetsReserved = from.at(sfAssetsReserved);
|
||||
self.withdrawalPolicy = from.at(sfWithdrawalPolicy);
|
||||
self.scale = from.at(sfScale);
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -61,6 +65,31 @@ ValidVault::Shares::make(SLE const& from)
|
||||
return self;
|
||||
}
|
||||
|
||||
Number
|
||||
ValidVault::Loan::interestDue() const
|
||||
{
|
||||
return totalValueOutstanding - principalOutstanding - managementFeeOutstanding;
|
||||
}
|
||||
|
||||
Number
|
||||
ValidVault::Loan::claim() const
|
||||
{
|
||||
return totalValueOutstanding - managementFeeOutstanding;
|
||||
}
|
||||
|
||||
ValidVault::Loan
|
||||
ValidVault::Loan::make(SLE const& from)
|
||||
{
|
||||
XRPL_ASSERT(from.getType() == ltLOAN, "ValidVault::Loan::make : from Loan object");
|
||||
|
||||
ValidVault::Loan self;
|
||||
self.key = from.key();
|
||||
self.principalOutstanding = from.at(sfPrincipalOutstanding);
|
||||
self.totalValueOutstanding = from.at(sfTotalValueOutstanding);
|
||||
self.managementFeeOutstanding = from.at(sfManagementFeeOutstanding);
|
||||
return self;
|
||||
}
|
||||
|
||||
void
|
||||
ValidVault::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
@@ -119,6 +148,12 @@ ValidVault::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref afte
|
||||
sign = -1;
|
||||
break;
|
||||
}
|
||||
case ltLOAN:
|
||||
// A loan carries no vault balance of its own; capture its prior
|
||||
// state so the loan pay invariant can verify the change in the
|
||||
// vault's claim on the loan.
|
||||
beforeLoan_.push_back(Loan::make(*before));
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
}
|
||||
@@ -164,6 +199,11 @@ ValidVault::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref afte
|
||||
sign = -1;
|
||||
break;
|
||||
}
|
||||
case ltLOAN:
|
||||
// A loan carries no vault balance of its own; capture it so the
|
||||
// loan set invariant can verify the interest booked to the vault.
|
||||
afterLoan_.push_back(Loan::make(*after));
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
}
|
||||
@@ -423,11 +463,29 @@ ValidVault::finalize(
|
||||
{
|
||||
auto const& beforeVault = beforeVault_[0];
|
||||
if (afterVault.asset != beforeVault.asset || afterVault.pseudoId != beforeVault.pseudoId ||
|
||||
afterVault.shareMPTID != beforeVault.shareMPTID)
|
||||
afterVault.shareMPTID != beforeVault.shareMPTID ||
|
||||
afterVault.owner != beforeVault.owner ||
|
||||
afterVault.withdrawalPolicy != beforeVault.withdrawalPolicy ||
|
||||
afterVault.scale != beforeVault.scale)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: violation of vault immutable data";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// AssetsReserved (principal held back for pending loans) may only be
|
||||
// changed by LoanDelete, LoanAccept, or a LoanSet that creates a
|
||||
// pending loan (Borrower present, CounterpartySignature absent).
|
||||
if (afterVault.assetsReserved != beforeVault.assetsReserved)
|
||||
{
|
||||
bool const pendingLoanSet = txnType == ttLOAN_SET && tx.isFieldPresent(sfBorrower) &&
|
||||
!tx.isFieldPresent(sfCounterpartySignature);
|
||||
if (txnType != ttLOAN_DELETE && txnType != ttLOAN_ACCEPT && !pendingLoanSet)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: vault AssetsReserved changed "
|
||||
"by an unauthorized transaction";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!updatedShares)
|
||||
@@ -1042,15 +1100,340 @@ ValidVault::finalize(
|
||||
return result;
|
||||
}
|
||||
|
||||
case ttLOAN_SET:
|
||||
case ttLOAN_MANAGE:
|
||||
case ttLOAN_PAY: {
|
||||
// TBD
|
||||
return true;
|
||||
case ttLOAN_SET: {
|
||||
bool result = true;
|
||||
|
||||
XRPL_ASSERT(
|
||||
!beforeVault_.empty(), "xrpl::ValidVault::finalize : loan set updated a vault");
|
||||
auto const& beforeVault = beforeVault_[0];
|
||||
|
||||
// Funding a loan moves the requested principal out of the vault
|
||||
// pseudo-account to the borrower (and, if any, the origination
|
||||
// fee to the broker owner).
|
||||
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
if (!maybeVaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan set must change vault balance";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
// Get the posterior scale to round calculations to
|
||||
auto const minScale = computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
|
||||
|
||||
// The vault only releases the requested principal, which must
|
||||
// match the reduction of both the vault (pseudo-account) balance
|
||||
// and the assets available.
|
||||
auto const principalDelta =
|
||||
roundToAsset(vaultAsset, -tx[sfPrincipalRequested], minScale);
|
||||
|
||||
auto const vaultDeltaAssets =
|
||||
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
|
||||
if (vaultDeltaAssets != principalDelta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan set must decrease vault balance "
|
||||
"by the principal requested";
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
if (assetAvailableDelta != principalDelta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan set must decrease assets "
|
||||
"available by the principal requested";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// A loan set must create exactly one loan object; the interest
|
||||
// it books is the only permitted change to assets outstanding.
|
||||
if (afterLoan_.size() != 1 || !beforeLoan_.empty())
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan set must create exactly one loan";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
auto const& loan = afterLoan_[0];
|
||||
|
||||
// The created loan must record exactly the principal the vault
|
||||
// released. Otherwise the borrower's claim (and thus the assets
|
||||
// booked back to the vault on repayment) is decoupled from the
|
||||
// assets actually lent, which would skew the vault's share price.
|
||||
if (loan.principalOutstanding != tx[sfPrincipalRequested])
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan set principal outstanding must "
|
||||
"equal principal requested";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// Interest accrues to the vault: assets outstanding must grow by
|
||||
// exactly the interest due booked on the newly created loan.
|
||||
auto const assetsTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
auto const interestDue = roundToAsset(vaultAsset, loan.interestDue(), minScale);
|
||||
if (assetsTotalDelta != interestDue)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan set must increase assets "
|
||||
"outstanding by the interest due";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (afterVault.assetsMaximum > kZero &&
|
||||
afterVault.assetsTotal > afterVault.assetsMaximum)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan set assets outstanding must not "
|
||||
"exceed assets maximum";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// A loan set neither mints nor burns vault shares.
|
||||
if (beforeShares && updatedShares &&
|
||||
beforeShares->sharesTotal != updatedShares->sharesTotal)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan set must not change shares outstanding";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
case ttLOAN_MANAGE: {
|
||||
bool result = true;
|
||||
|
||||
XRPL_ASSERT(
|
||||
!beforeVault_.empty(),
|
||||
"xrpl::ValidVault::finalize : loan manage updated a vault");
|
||||
auto const& beforeVault = beforeVault_[0];
|
||||
|
||||
// Loan management (impair / unimpair / default) never removes
|
||||
// assets from the vault. Only a default returns first-loss
|
||||
// capital from the broker to the vault pseudo-account; impair
|
||||
// and unimpair merely adjust the paper (unrealized) loss and
|
||||
// touch no balances.
|
||||
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
auto const vaultDelta = maybeVaultDeltaAssets.value_or(
|
||||
DeltaInfo{.delta = kZero, .scale = scale(afterVault.assetsTotal, vaultAsset)});
|
||||
|
||||
// Get the posterior scale to round calculations to
|
||||
auto const minScale = computeVaultMinScale(vaultDelta, view.rules());
|
||||
|
||||
auto const vaultDeltaAssets = roundToAsset(vaultAsset, vaultDelta.delta, minScale);
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
auto const assetTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
|
||||
// --- Checks common to every loan manage sub-operation ---
|
||||
|
||||
// Assets available always tracks the real vault balance: any
|
||||
// change to one is matched by the other.
|
||||
if (assetAvailableDelta != vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan manage vault balance and assets "
|
||||
"available must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// Loan management adjusts the unrealized (paper) loss but must
|
||||
// never drive it negative.
|
||||
if (afterVault.lossUnrealized < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan manage must not make loss "
|
||||
"unrealized negative";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// A loan manage neither mints nor burns vault shares.
|
||||
if (beforeShares && updatedShares &&
|
||||
beforeShares->sharesTotal != updatedShares->sharesTotal)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan manage must not change shares "
|
||||
"outstanding";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// --- Checks specific to each loan manage sub-operation ---
|
||||
|
||||
if (tx.isFlag(tfLoanImpair) || tx.isFlag(tfLoanUnimpair))
|
||||
{
|
||||
// Impair / unimpair only move the paper (unrealized) loss;
|
||||
// they touch neither balances nor assets.
|
||||
if (assetAvailableDelta != kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan impair/unimpair must not "
|
||||
"change assets available";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (assetTotalDelta != kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan impair/unimpair must not "
|
||||
"change assets outstanding";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
else if (tx.isFlag(tfLoanDefault))
|
||||
{
|
||||
// A default returns first-loss capital to the vault, so
|
||||
// assets available (and the vault balance) may only grow.
|
||||
if (assetAvailableDelta < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan default must not decrease "
|
||||
"assets available";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// A default realizes (writes off) the uncovered portion of
|
||||
// the loan, so assets outstanding may only shrink.
|
||||
if (assetTotalDelta > kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan default must not increase "
|
||||
"assets outstanding";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// A loan manage with none of the sub-operation flags
|
||||
// (impair, unimpair, default) is a no-op and must not
|
||||
// modify the vault.
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan manage without a sub-operation "
|
||||
"must not modify the vault";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
case ttLOAN_PAY: {
|
||||
bool result = true;
|
||||
|
||||
XRPL_ASSERT(
|
||||
!beforeVault_.empty(), "xrpl::ValidVault::finalize : loan pay updated a vault");
|
||||
auto const& beforeVault = beforeVault_[0];
|
||||
|
||||
// A loan payment moves the paid principal and interest into the
|
||||
// vault pseudo-account (fees go to the broker), so the vault
|
||||
// balance and the assets available both increase by that amount.
|
||||
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
if (!maybeVaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan pay must change vault balance";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
// Get the posterior scale to round calculations to
|
||||
auto const minScale = computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
|
||||
|
||||
auto const vaultDeltaAssets =
|
||||
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
|
||||
// Assets available always tracks the real vault balance: any
|
||||
// change to one is matched by the other.
|
||||
if (assetAvailableDelta != vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan pay vault balance and assets "
|
||||
"available must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// A payment always adds funds to the vault, so assets available
|
||||
// (and the vault balance) must increase.
|
||||
if (assetAvailableDelta <= kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan pay must increase assets "
|
||||
"available";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// The vault only receives the principal and interest portion of
|
||||
// the borrower's payment (the fee goes to the broker), so it can
|
||||
// never grow by more than the amount paid.
|
||||
auto const amountPaid = roundToAsset(vaultAsset, tx[sfAmount], minScale);
|
||||
if (assetAvailableDelta > amountPaid)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan pay must not increase assets "
|
||||
"available by more than the amount paid";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// A payment unimpairs the loan, so it may reduce the unrealized
|
||||
// (paper) loss, but must never drive it negative.
|
||||
if (afterVault.lossUnrealized < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan pay must not make loss "
|
||||
"unrealized negative";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// A loan pay neither mints nor burns vault shares.
|
||||
if (beforeShares && updatedShares &&
|
||||
beforeShares->sharesTotal != updatedShares->sharesTotal)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan pay must not change shares "
|
||||
"outstanding";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// The vault's total assets equal its available cash plus the
|
||||
// claim it holds on outstanding loans (each loan's total value
|
||||
// owed, less the broker's management fee, which belongs to the
|
||||
// broker). A payment only moves value between those two pools,
|
||||
// so the change in assets outstanding must equal the cash
|
||||
// received plus the change in the paid loan's claim on the
|
||||
// vault. This is an independent check that the borrower's
|
||||
// payment was split correctly between principal and interest.
|
||||
if (afterLoan_.size() != 1 || beforeLoan_.size() != 1 ||
|
||||
afterLoan_[0].key != beforeLoan_[0].key)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan pay must modify exactly one "
|
||||
"loan";
|
||||
result = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const claimDelta = roundToAsset(
|
||||
vaultAsset, afterLoan_[0].claim() - beforeLoan_[0].claim(), minScale);
|
||||
auto const assetsTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
if (assetsTotalDelta != assetAvailableDelta + claimDelta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan pay assets outstanding must "
|
||||
"match the cash received and the change in the loan "
|
||||
"claim";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// LCOV_EXCL_START
|
||||
default:
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::ValidVault::finalize : unknown transaction type");
|
||||
return false;
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
38
src/libxrpl/tx/transactors/lending/LoanAccept.cpp
Normal file
38
src/libxrpl/tx/transactors/lending/LoanAccept.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#include <xrpl/tx/transactors/lending/LoanAccept.h>
|
||||
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
LoanAccept::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
LoanAccept::doApply()
|
||||
{
|
||||
// Intentionally does nothing for now.
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
LoanAccept::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
}
|
||||
|
||||
bool
|
||||
LoanAccept::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
return true;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -49,6 +49,19 @@ LoanSet::getFlagsMask(PreflightContext const& ctx)
|
||||
return tfLoanSetMask;
|
||||
}
|
||||
|
||||
bool
|
||||
LoanSet::isTwoStepFlow(STTx const& tx, Rules const& rules)
|
||||
{
|
||||
if (!rules.enabled(featureLendingProtocolV1_1))
|
||||
return false;
|
||||
|
||||
// The two-step (Borrower) flow is started when the LoanSet names a
|
||||
// Borrower and a StartDate but carries neither a Counterparty nor a
|
||||
// CounterpartySignature.
|
||||
return tx.isFieldPresent(sfBorrower) && tx.isFieldPresent(sfStartDate) &&
|
||||
!tx.isFieldPresent(sfCounterparty) && !tx.isFieldPresent(sfCounterpartySignature);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
LoanSet::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -72,7 +85,12 @@ LoanSet::preflight(PreflightContext const& ctx)
|
||||
return tx.getFieldObject(sfCounterpartySignature);
|
||||
return std::nullopt;
|
||||
}();
|
||||
if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig)
|
||||
|
||||
bool twoStepFlow = isTwoStepFlow(tx, ctx.rules);
|
||||
// In the two-step (Borrower) flow introduced by V1.1, a CounterpartySignature
|
||||
// is not required even for non-batch transactions. The immediate flow still
|
||||
// requires one.
|
||||
if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig && !twoStepFlow)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanSet transaction must have a CounterpartySignature.";
|
||||
return temBAD_SIGNER;
|
||||
@@ -143,6 +161,11 @@ LoanSet::checkSign(PreclaimContext const& ctx)
|
||||
if (auto ret = Transactor::checkSign(ctx))
|
||||
return ret;
|
||||
|
||||
// In the two-step (Borrower) flow introduced by V1.1 there is no
|
||||
// counterparty, so there is no CounterpartySignature to check.
|
||||
if (isTwoStepFlow(ctx.tx, ctx.view.rules()))
|
||||
return tesSUCCESS;
|
||||
|
||||
// Counter signer is optional. If it's not specified, it's assumed to be
|
||||
// `LoanBroker.Owner`. Note that we have not checked whether the
|
||||
// loanbroker exists at this point.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -51,7 +51,6 @@ TEST(LoanTests, BuilderSettersRoundTrip)
|
||||
LoanBuilder builder{
|
||||
previousTxnIDValue,
|
||||
previousTxnLgrSeqValue,
|
||||
ownerNodeValue,
|
||||
loanBrokerNodeValue,
|
||||
loanBrokerIDValue,
|
||||
loanSequenceValue,
|
||||
@@ -61,6 +60,7 @@ TEST(LoanTests, BuilderSettersRoundTrip)
|
||||
periodicPaymentValue
|
||||
};
|
||||
|
||||
builder.setOwnerNode(ownerNodeValue);
|
||||
builder.setLoanOriginationFee(loanOriginationFeeValue);
|
||||
builder.setLoanServiceFee(loanServiceFeeValue);
|
||||
builder.setLatePaymentFee(latePaymentFeeValue);
|
||||
@@ -100,12 +100,6 @@ TEST(LoanTests, BuilderSettersRoundTrip)
|
||||
expectEqualField(expected, actual, "sfPreviousTxnLgrSeq");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = ownerNodeValue;
|
||||
auto const actual = entry.getOwnerNode();
|
||||
expectEqualField(expected, actual, "sfOwnerNode");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = loanBrokerNodeValue;
|
||||
auto const actual = entry.getLoanBrokerNode();
|
||||
@@ -148,6 +142,14 @@ TEST(LoanTests, BuilderSettersRoundTrip)
|
||||
expectEqualField(expected, actual, "sfPeriodicPayment");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = ownerNodeValue;
|
||||
auto const actualOpt = entry.getOwnerNode();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfOwnerNode");
|
||||
EXPECT_TRUE(entry.hasOwnerNode());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = loanOriginationFeeValue;
|
||||
auto const actualOpt = entry.getLoanOriginationFee();
|
||||
@@ -384,16 +386,6 @@ TEST(LoanTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq");
|
||||
}
|
||||
|
||||
{
|
||||
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 = loanBrokerNodeValue;
|
||||
|
||||
@@ -464,6 +456,19 @@ TEST(LoanTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, fromBuilder, "sfPeriodicPayment");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = ownerNodeValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getOwnerNode();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getOwnerNode();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfOwnerNode");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfOwnerNode");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = loanOriginationFeeValue;
|
||||
|
||||
@@ -732,7 +737,6 @@ TEST(LoanTests, OptionalFieldsReturnNullopt)
|
||||
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
auto const ownerNodeValue = canonical_UINT64();
|
||||
auto const loanBrokerNodeValue = canonical_UINT64();
|
||||
auto const loanBrokerIDValue = canonical_UINT256();
|
||||
auto const loanSequenceValue = canonical_UINT32();
|
||||
@@ -744,7 +748,6 @@ TEST(LoanTests, OptionalFieldsReturnNullopt)
|
||||
LoanBuilder builder{
|
||||
previousTxnIDValue,
|
||||
previousTxnLgrSeqValue,
|
||||
ownerNodeValue,
|
||||
loanBrokerNodeValue,
|
||||
loanBrokerIDValue,
|
||||
loanSequenceValue,
|
||||
@@ -757,6 +760,8 @@ TEST(LoanTests, OptionalFieldsReturnNullopt)
|
||||
auto const entry = builder.build(index);
|
||||
|
||||
// Verify optional fields are not present
|
||||
EXPECT_FALSE(entry.hasOwnerNode());
|
||||
EXPECT_FALSE(entry.getOwnerNode().has_value());
|
||||
EXPECT_FALSE(entry.hasLoanOriginationFee());
|
||||
EXPECT_FALSE(entry.getLoanOriginationFee().has_value());
|
||||
EXPECT_FALSE(entry.hasLoanServiceFee());
|
||||
|
||||
@@ -35,6 +35,7 @@ TEST(VaultTests, BuilderSettersRoundTrip)
|
||||
auto const shareMPTIDValue = canonical_UINT192();
|
||||
auto const withdrawalPolicyValue = canonical_UINT8();
|
||||
auto const scaleValue = canonical_UINT8();
|
||||
auto const assetsReservedValue = canonical_NUMBER();
|
||||
|
||||
VaultBuilder builder{
|
||||
previousTxnIDValue,
|
||||
@@ -54,6 +55,7 @@ TEST(VaultTests, BuilderSettersRoundTrip)
|
||||
builder.setAssetsMaximum(assetsMaximumValue);
|
||||
builder.setLossUnrealized(lossUnrealizedValue);
|
||||
builder.setScale(scaleValue);
|
||||
builder.setAssetsReserved(assetsReservedValue);
|
||||
|
||||
builder.setLedgerIndex(index);
|
||||
builder.setFlags(0x1u);
|
||||
@@ -166,6 +168,14 @@ TEST(VaultTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(entry.hasScale());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = assetsReservedValue;
|
||||
auto const actualOpt = entry.getAssetsReserved();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfAssetsReserved");
|
||||
EXPECT_TRUE(entry.hasAssetsReserved());
|
||||
}
|
||||
|
||||
EXPECT_TRUE(entry.hasLedgerIndex());
|
||||
auto const ledgerIndex = entry.getLedgerIndex();
|
||||
ASSERT_TRUE(ledgerIndex.has_value());
|
||||
@@ -194,6 +204,7 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
|
||||
auto const shareMPTIDValue = canonical_UINT192();
|
||||
auto const withdrawalPolicyValue = canonical_UINT8();
|
||||
auto const scaleValue = canonical_UINT8();
|
||||
auto const assetsReservedValue = canonical_NUMBER();
|
||||
|
||||
auto sle = std::make_shared<SLE>(Vault::entryType, index);
|
||||
|
||||
@@ -212,6 +223,7 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
|
||||
sle->at(sfShareMPTID) = shareMPTIDValue;
|
||||
sle->at(sfWithdrawalPolicy) = withdrawalPolicyValue;
|
||||
sle->at(sfScale) = scaleValue;
|
||||
sle->at(sfAssetsReserved) = assetsReservedValue;
|
||||
|
||||
VaultBuilder builderFromSle{sle};
|
||||
EXPECT_TRUE(builderFromSle.validate());
|
||||
@@ -390,6 +402,19 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfScale");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = assetsReservedValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getAssetsReserved();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getAssetsReserved();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfAssetsReserved");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfAssetsReserved");
|
||||
}
|
||||
|
||||
EXPECT_EQ(entryFromSle.getKey(), index);
|
||||
EXPECT_EQ(entryFromBuilder.getKey(), index);
|
||||
}
|
||||
@@ -472,5 +497,7 @@ TEST(VaultTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(entry.getLossUnrealized().has_value());
|
||||
EXPECT_FALSE(entry.hasScale());
|
||||
EXPECT_FALSE(entry.getScale().has_value());
|
||||
EXPECT_FALSE(entry.hasAssetsReserved());
|
||||
EXPECT_FALSE(entry.getAssetsReserved().has_value());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// Auto-generated unit tests for transaction LoanAccept
|
||||
|
||||
|
||||
#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/LoanAccept.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(TransactionsLoanAcceptTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testLoanAccept"));
|
||||
|
||||
// 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 loanIDValue = canonical_UINT256();
|
||||
|
||||
LoanAcceptBuilder builder{
|
||||
accountValue,
|
||||
loanIDValue,
|
||||
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 = loanIDValue;
|
||||
auto const actual = tx.getLoanID();
|
||||
expectEqualField(expected, actual, "sfLoanID");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
// and verify all fields match.
|
||||
TEST(TransactionsLoanAcceptTests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testLoanAcceptFromTx"));
|
||||
|
||||
// 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 loanIDValue = canonical_UINT256();
|
||||
|
||||
// Build an initial transaction
|
||||
LoanAcceptBuilder initialBuilder{
|
||||
accountValue,
|
||||
loanIDValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
LoanAcceptBuilder 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 = loanIDValue;
|
||||
auto const actual = rebuiltTx.getLoanID();
|
||||
expectEqualField(expected, actual, "sfLoanID");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsLoanAcceptTests, 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(LoanAccept{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsLoanAcceptTests, 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(LoanAcceptBuilder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -31,6 +31,7 @@ TEST(TransactionsLoanSetTests, BuilderSettersRoundTrip)
|
||||
// Transaction-specific field values
|
||||
auto const loanBrokerIDValue = canonical_UINT256();
|
||||
auto const dataValue = canonical_VL();
|
||||
auto const borrowerValue = canonical_ACCOUNT();
|
||||
auto const counterpartyValue = canonical_ACCOUNT();
|
||||
auto const counterpartySignatureValue = canonical_OBJECT();
|
||||
auto const loanOriginationFeeValue = canonical_NUMBER();
|
||||
@@ -57,6 +58,7 @@ TEST(TransactionsLoanSetTests, BuilderSettersRoundTrip)
|
||||
|
||||
// Set optional fields
|
||||
builder.setData(dataValue);
|
||||
builder.setBorrower(borrowerValue);
|
||||
builder.setCounterparty(counterpartyValue);
|
||||
builder.setCounterpartySignature(counterpartySignatureValue);
|
||||
builder.setLoanOriginationFee(loanOriginationFeeValue);
|
||||
@@ -108,6 +110,14 @@ TEST(TransactionsLoanSetTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(tx.hasData());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = borrowerValue;
|
||||
auto const actualOpt = tx.getBorrower();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBorrower should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfBorrower");
|
||||
EXPECT_TRUE(tx.hasBorrower());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = counterpartyValue;
|
||||
auto const actualOpt = tx.getCounterparty();
|
||||
@@ -238,6 +248,7 @@ TEST(TransactionsLoanSetTests, BuilderFromStTxRoundTrip)
|
||||
// Transaction-specific field values
|
||||
auto const loanBrokerIDValue = canonical_UINT256();
|
||||
auto const dataValue = canonical_VL();
|
||||
auto const borrowerValue = canonical_ACCOUNT();
|
||||
auto const counterpartyValue = canonical_ACCOUNT();
|
||||
auto const counterpartySignatureValue = canonical_OBJECT();
|
||||
auto const loanOriginationFeeValue = canonical_NUMBER();
|
||||
@@ -264,6 +275,7 @@ TEST(TransactionsLoanSetTests, BuilderFromStTxRoundTrip)
|
||||
};
|
||||
|
||||
initialBuilder.setData(dataValue);
|
||||
initialBuilder.setBorrower(borrowerValue);
|
||||
initialBuilder.setCounterparty(counterpartyValue);
|
||||
initialBuilder.setCounterpartySignature(counterpartySignatureValue);
|
||||
initialBuilder.setLoanOriginationFee(loanOriginationFeeValue);
|
||||
@@ -315,6 +327,13 @@ TEST(TransactionsLoanSetTests, BuilderFromStTxRoundTrip)
|
||||
expectEqualField(expected, *actualOpt, "sfData");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = borrowerValue;
|
||||
auto const actualOpt = rebuiltTx.getBorrower();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBorrower should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfBorrower");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = counterpartyValue;
|
||||
auto const actualOpt = rebuiltTx.getCounterparty();
|
||||
@@ -474,6 +493,8 @@ TEST(TransactionsLoanSetTests, OptionalFieldsReturnNullopt)
|
||||
// Verify optional fields are not present
|
||||
EXPECT_FALSE(tx.hasData());
|
||||
EXPECT_FALSE(tx.getData().has_value());
|
||||
EXPECT_FALSE(tx.hasBorrower());
|
||||
EXPECT_FALSE(tx.getBorrower().has_value());
|
||||
EXPECT_FALSE(tx.hasCounterparty());
|
||||
EXPECT_FALSE(tx.getCounterparty().has_value());
|
||||
EXPECT_FALSE(tx.hasCounterpartySignature());
|
||||
|
||||
Reference in New Issue
Block a user