diff --git a/include/xrpl/protocol_autogen/transactions/VaultDelete.h b/include/xrpl/protocol_autogen/transactions/VaultDelete.h index 89a4ef2a2f..bc89ce2bb7 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultDelete.h +++ b/include/xrpl/protocol_autogen/transactions/VaultDelete.h @@ -57,6 +57,32 @@ public: { return this->tx_->at(sfVaultID); } + + /** + * @brief Get sfMemoData (soeOPTIONAL) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getMemoData() const + { + if (hasMemoData()) + { + return this->tx_->at(sfMemoData); + } + return std::nullopt; + } + + /** + * @brief Check if sfMemoData is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasMemoData() const + { + return this->tx_->isFieldPresent(sfMemoData); + } }; /** @@ -112,6 +138,17 @@ public: return *this; } + /** + * @brief Set sfMemoData (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + VaultDeleteBuilder& + setMemoData(std::decay_t const& value) + { + object_[sfMemoData] = value; + return *this; + } + /** * @brief Build and return the VaultDelete wrapper. * @param publicKey The public key for signing. diff --git a/src/libxrpl/tx/transactors/lending/LoanManage.cpp b/src/libxrpl/tx/transactors/lending/LoanManage.cpp index 87eca60135..768135dc66 100644 --- a/src/libxrpl/tx/transactors/lending/LoanManage.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanManage.cpp @@ -140,40 +140,39 @@ LoanManage::defaultLoan( // Apply the First-Loss Capital to the Default Amount TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; - TenthBips32 const coverRateLiquidation{brokerSle->at(sfCoverRateLiquidation)}; + auto const coverRateLiquidation{brokerSle->at(~sfCoverRateLiquidation)}; - auto const covered = [&]() { + auto const defaultCovered = [&]() { // Always round the minimum required up. NumberRoundModeGuard mg(Number::upward); + Number covered; - if (view.rules().enabled(fixLendingProtocolV1_1)) + if (view.rules().enabled(featureLendingProtocolV1_1) && !coverRateLiquidation) { // New formula: DefaultCovered = min(DefaultAmount × CoverRateMinimum, CoverAvailable) // Round the liquidation amount up - return roundToAsset(vaultAsset, tenthBipsOfValue(totalDefaultAmount, coverRateMinimum), loanScale); + covered = roundToAsset( + vaultAsset, tenthBipsOfValue(totalDefaultAmount, coverRateMinimum), loanScale); } else { - auto const minimumCover = tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum); + auto const minimumCover = + tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum); // Round the liquidation amount up - return roundToAsset( + covered = roundToAsset( vaultAsset, /* * This formula is from the XLS-66 spec, section 3.2.3.2 (State * Changes), specifically "if the `tfLoanDefault` flag is set" / * "Apply the First-Loss Capital to the Default Amount" */ - std::min(tenthBipsOfValue(minimumCover, coverRateLiquidation), totalDefaultAmount), + std::min( + tenthBipsOfValue(minimumCover, TenthBips32{coverRateLiquidation.value_or(0)}), + totalDefaultAmount), loanScale); } - }(); - - auto const defaultCovered = [&]() { - // Always round the minimum required up. - NumberRoundModeGuard mg(Number::upward); auto const coverAvailable = *brokerSle->at(sfCoverAvailable); - return std::min(covered, coverAvailable); }(); diff --git a/src/libxrpl/tx/transactors/system/Batch.cpp b/src/libxrpl/tx/transactors/system/Batch.cpp index 2147f16c47..b2a111a182 100644 --- a/src/libxrpl/tx/transactors/system/Batch.cpp +++ b/src/libxrpl/tx/transactors/system/Batch.cpp @@ -262,8 +262,7 @@ Batch::preflight(PreflightContext const& ctx) return temINVALID; } - if (!ctx.rules.enabled(fixLendingProtocolV1_1) && - std::any_of( + if (std::any_of( disabledTxTypes.begin(), disabledTxTypes.end(), [txType](auto const& disabled) { return txType == disabled; })) diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index 632334c101..ec12fbb21f 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -2600,18 +2600,16 @@ class Batch_test : public beast::unit_test::suite { testcase("loan"); + bool const lendingBatchEnabled = !std::any_of( + Batch::disabledTxTypes.begin(), Batch::disabledTxTypes.end(), [](auto const& disabled) { + return disabled == ttLOAN_BROKER_SET; + }); + using namespace test::jtx; test::jtx::Env env{ *this, features | featureSingleAssetVault | featureLendingProtocol | featureMPTokensV1}; - bool const lendingBatchEnabled = - !std::any_of( - Batch::disabledTxTypes.begin(), - Batch::disabledTxTypes.end(), - [](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; }) || - env.enabled(fixLendingProtocolV1_1); - Account const issuer{"issuer"}; // For simplicity, lender will be the sole actor for the vault & // brokers. diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 88c0ed5f91..9177a0c797 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1946,7 +1946,7 @@ protected: NumberRoundModeGuard mg(Number::upward); auto const totalDefaultAmount = state.totalValue - state.managementFeeOutstanding; auto const defaultAmount = [&] { - if (env.enabled(fixLendingProtocolV1_1)) + if (env.enabled(featureLendingProtocolV1_1)) { // DefaultCovered = min(DefaultAmount × CoverRateMinimum, CoverAvailable) return roundToAsset( @@ -1957,13 +1957,14 @@ protected: else { // From XLS-66 spec, section 3.2.3.2: - // DefaultCovered = min(DebtTotal × CoverRateMinimum × CoverRateLiquidation, DefaultAmount, - // CoverAvailable) + // DefaultCovered = min(DebtTotal × CoverRateMinimum × CoverRateLiquidation, + // DefaultAmount, CoverAvailable) return roundToAsset( broker.asset, std::min( tenthBipsOfValue( - tenthBipsOfValue(brokerSle->at(sfDebtTotal), broker.params.coverRateMin), + tenthBipsOfValue( + brokerSle->at(sfDebtTotal), broker.params.coverRateMin), broker.params.coverRateLiquidation), state.totalValue - state.managementFeeOutstanding), state.loanScale); @@ -3603,23 +3604,15 @@ protected: // From FIND-001 testcase << "Batch Bypass Counterparty"; + bool const lendingBatchEnabled = !std::any_of( + Batch::disabledTxTypes.begin(), Batch::disabledTxTypes.end(), [](auto const& disabled) { + return disabled == ttLOAN_BROKER_SET; + }); + using namespace jtx; using namespace std::chrono_literals; Env env(*this, all); - bool const lendingBatchEnabled = - !std::any_of( - Batch::disabledTxTypes.begin(), - Batch::disabledTxTypes.end(), - [](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; }) || - env.enabled(fixLendingProtocolV1_1); - - if (!lendingBatchEnabled) - { - pass(); - return; - } - Account const lender{"lender"}; Account const borrower{"borrower"}; @@ -6986,7 +6979,7 @@ protected: auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal); auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable); - if (env.enabled(fixLendingProtocolV1_1)) + if (env.enabled(featureLendingProtocolV1_1)) { // Proportional default cover // Loan 1 Defaults: 20% of Loan A (50,134) = 10,027 seizure @@ -7023,7 +7016,7 @@ protected: // Both scenarios: DebtTotal should be 0 after both loans default BEAST_EXPECT(afterSecondDebtTotal == 0); - if (env.enabled(fixLendingProtocolV1_1)) + if (env.enabled(featureLendingProtocolV1_1)) { // Proportional default cover // Loan 2 Defaults: 20% of Loan B (50,134) = 10,027 seizure @@ -7226,7 +7219,7 @@ public: testLoanPayBrokerOwnerUnauthorizedMPT(); testLoanPayBrokerOwnerNoPermissionedDomainMPT(); testLoanSetBrokerOwnerNoPermissionedDomainMPT(); - testSequentialFLCDepletion(all - fixLendingProtocolV1_1); + testSequentialFLCDepletion(all - featureLendingProtocolV1_1); testSequentialFLCDepletion(all); } }; diff --git a/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp index 24c89d249a..2b4ec486a6 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp @@ -30,6 +30,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip) // Transaction-specific field values auto const vaultIDValue = canonical_UINT256(); + auto const memoDataValue = canonical_VL(); VaultDeleteBuilder builder{ accountValue, @@ -39,6 +40,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip) }; // Set optional fields + builder.setMemoData(memoDataValue); auto tx = builder.build(publicKey, secretKey); @@ -62,6 +64,14 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip) } // Verify optional fields + { + auto const& expected = memoDataValue; + auto const actualOpt = tx.getMemoData(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present"; + expectEqualField(expected, *actualOpt, "sfMemoData"); + EXPECT_TRUE(tx.hasMemoData()); + } + } // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, @@ -79,6 +89,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip) // Transaction-specific field values auto const vaultIDValue = canonical_UINT256(); + auto const memoDataValue = canonical_VL(); // Build an initial transaction VaultDeleteBuilder initialBuilder{ @@ -88,6 +99,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip) feeValue }; + initialBuilder.setMemoData(memoDataValue); auto initialTx = initialBuilder.build(publicKey, secretKey); @@ -112,6 +124,13 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip) } // Verify optional fields + { + auto const& expected = memoDataValue; + auto const actualOpt = rebuiltTx.getMemoData(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present"; + expectEqualField(expected, *actualOpt, "sfMemoData"); + } + } // 3) Verify wrapper throws when constructed from wrong transaction type. @@ -142,5 +161,35 @@ TEST(TransactionsVaultDeleteTests, BuilderThrowsOnWrongTxType) EXPECT_THROW(VaultDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error); } +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsVaultDeleteTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testVaultDeleteNullopt")); + + // 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 vaultIDValue = canonical_UINT256(); + + VaultDeleteBuilder builder{ + accountValue, + vaultIDValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasMemoData()); + EXPECT_FALSE(tx.getMemoData().has_value()); +} }