From e3ba569187c7e435069fa0f03eefe40adf166431 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:33:46 +0000 Subject: [PATCH] fix: Check credential for LoanBrokerCoverWithdraw and VaultWithdraw (#7107) Co-authored-by: Peter Chen Co-authored-by: Ayaz Salikhov --- include/xrpl/ledger/View.h | 25 +++- .../xrpl/protocol/detail/transactions.macro | 2 + .../transactions/LoanBrokerCoverWithdraw.h | 37 +++++ .../transactions/VaultWithdraw.h | 37 +++++ .../xrpl/tx/transactors/vault/VaultWithdraw.h | 3 + src/libxrpl/ledger/View.cpp | 35 ++++- .../lending/LoanBrokerCoverWithdraw.cpp | 16 ++- .../tx/transactors/vault/VaultWithdraw.cpp | 20 ++- src/test/app/lending/LoanBroker_test.cpp | 131 ++++++++++++++++++ src/test/app/vault/VaultDomain_test.cpp | 110 +++++++++++++++ .../LoanBrokerCoverWithdrawTests.cpp | 21 +++ .../transactions/VaultWithdrawTests.cpp | 21 +++ 12 files changed, 445 insertions(+), 13 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index f7fd5b5a8c..bb0817673c 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -24,6 +24,7 @@ #include #include #include +#include namespace xrpl { @@ -198,7 +199,10 @@ dirLink( * if withdrawing to self. * - If withdrawing to self, succeed. * - If not, checks if the receiver requires deposit authorization, and if - * the sender has it. + * the sender has it (account-based or credential-based). + * - Expects any credentials passed in to already exist in the ledger, and + * returns an internal error otherwise. Validate them beforehand with + * credentials::valid(). * - Checks that the receiver will not exceed the limit (IOU trustline limit * or MPT MaximumAmount). */ @@ -209,7 +213,8 @@ canWithdraw( AccountID const& to, SLE::const_ref toSle, STAmount const& amount, - bool hasDestinationTag); + bool hasDestinationTag, + std::optional> const& credentialIDs = std::nullopt); /** * Checks that can withdraw funds from an object to itself or a destination. @@ -222,7 +227,10 @@ canWithdraw( * if withdrawing to self. * - If withdrawing to self, succeed. * - If not, checks if the receiver requires deposit authorization, and if - * the sender has it. + * the sender has it (account-based or credential-based). + * - Expects any credentials passed in to already exist in the ledger, and + * returns an internal error otherwise. Validate them beforehand with + * credentials::valid(). * - Checks that the receiver will not exceed the limit (IOU trustline limit * or MPT MaximumAmount). */ @@ -232,20 +240,25 @@ canWithdraw( AccountID const& from, AccountID const& to, STAmount const& amount, - bool hasDestinationTag); + bool hasDestinationTag, + std::optional> const& credentialIDs = std::nullopt); /** * Checks that can withdraw funds from an object to itself or a destination. * * The receiver may be either the submitting account (sfAccount) or a different - * destination account (sfDestination). + * destination account (sfDestination). Credentials, if any, are taken from the + * transaction's sfCredentialIDs field. * * - Checks that the receiver account exists. * - If the receiver requires a destination tag, check that one exists, even * if withdrawing to self. * - If withdrawing to self, succeed. * - If not, checks if the receiver requires deposit authorization, and if - * the sender has it. + * the sender has it (account-based or credential-based). + * - Expects any credentials in sfCredentialIDs to already exist in the + * ledger, and returns an internal error otherwise. Validate them + * beforehand with credentials::valid(). * - Checks that the receiver will not exceed the limit (IOU trustline limit * or MPT MaximumAmount). */ diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index f8676d3b63..997f368638 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -921,6 +921,7 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, {sfAmount, SoeRequired, SoeMptSupported}, {sfDestination, SoeOptional}, {sfDestinationTag, SoeOptional}, + {sfCredentialIDs, SoeOptional}, })) /** This transaction claws back tokens from a vault. */ @@ -1004,6 +1005,7 @@ TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, {sfAmount, SoeRequired, SoeMptSupported}, {sfDestination, SoeOptional}, {sfDestinationTag, SoeOptional}, + {sfCredentialIDs, SoeOptional}, })) /** This transaction claws back First Loss Capital from a Loan Broker to diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h index 56a93acbb4..148db4292c 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h @@ -121,6 +121,32 @@ public: { return this->tx_->isFieldPresent(sfDestinationTag); } + + /** + * @brief Get sfCredentialIDs (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCredentialIDs() const + { + if (hasCredentialIDs()) + { + return this->tx_->at(sfCredentialIDs); + } + return std::nullopt; + } + + /** + * @brief Check if sfCredentialIDs is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCredentialIDs() const + { + return this->tx_->isFieldPresent(sfCredentialIDs); + } }; /** @@ -214,6 +240,17 @@ public: return *this; } + /** + * @brief Set sfCredentialIDs (SoeOptional) + * @return Reference to this builder for method chaining. + */ + LoanBrokerCoverWithdrawBuilder& + setCredentialIDs(std::decay_t const& value) + { + object_[sfCredentialIDs] = value; + return *this; + } + /** * @brief Build and return the LoanBrokerCoverWithdraw wrapper. * @param publicKey The public key for signing. diff --git a/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h b/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h index 3211524e1f..17208cd76c 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h +++ b/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h @@ -121,6 +121,32 @@ public: { return this->tx_->isFieldPresent(sfDestinationTag); } + + /** + * @brief Get sfCredentialIDs (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCredentialIDs() const + { + if (hasCredentialIDs()) + { + return this->tx_->at(sfCredentialIDs); + } + return std::nullopt; + } + + /** + * @brief Check if sfCredentialIDs is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCredentialIDs() const + { + return this->tx_->isFieldPresent(sfCredentialIDs); + } }; /** @@ -214,6 +240,17 @@ public: return *this; } + /** + * @brief Set sfCredentialIDs (SoeOptional) + * @return Reference to this builder for method chaining. + */ + VaultWithdrawBuilder& + setCredentialIDs(std::decay_t const& value) + { + object_[sfCredentialIDs] = value; + return *this; + } + /** * @brief Build and return the VaultWithdraw wrapper. * @param publicKey The public key for signing. diff --git a/include/xrpl/tx/transactors/vault/VaultWithdraw.h b/include/xrpl/tx/transactors/vault/VaultWithdraw.h index 22ad39d26d..b61af8b323 100644 --- a/include/xrpl/tx/transactors/vault/VaultWithdraw.h +++ b/include/xrpl/tx/transactors/vault/VaultWithdraw.h @@ -20,6 +20,9 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index e01ae2e492..0cd082ff47 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -35,6 +35,7 @@ #include #include #include +#include namespace xrpl { @@ -467,7 +468,8 @@ canWithdraw( AccountID const& to, SLE::const_ref toSle, STAmount const& amount, - bool hasDestinationTag) + bool hasDestinationTag, + std::optional> const& credentialIDs) { if (auto const ret = checkDestinationAndTag(toSle, hasDestinationTag)) return ret; @@ -478,7 +480,28 @@ canWithdraw( if (toSle->isFlag(lsfDepositAuth)) { if (!view.exists(keylet::depositPreauth(to, from))) - return tecNO_PERMISSION; + { + if (credentialIDs.has_value()) + { + STVector256 const credIDs{*credentialIDs}; + + // Callers must have validated these in preclaim, so a missing + // credential here is an invariant violation. + for (auto const& h : credIDs) + { + if (!view.exists(keylet::credential(h))) + return tecINTERNAL; // LCOV_EXCL_LINE + } + + if (auto const ret = credentials::authorizedDepositPreauth(view, credIDs, to); + !isTesSuccess(ret)) + return ret; + } + else + { + return tecNO_PERMISSION; + } + } } return withdrawToDestExceedsLimit(view, from, to, amount); @@ -490,11 +513,12 @@ canWithdraw( AccountID const& from, AccountID const& to, STAmount const& amount, - bool hasDestinationTag) + bool hasDestinationTag, + std::optional> const& credentialIDs) { auto const toSle = view.read(keylet::account(to)); - return canWithdraw(view, from, to, toSle, amount, hasDestinationTag); + return canWithdraw(view, from, to, toSle, amount, hasDestinationTag, credentialIDs); } [[nodiscard]] TER @@ -503,7 +527,8 @@ canWithdraw(ReadView const& view, STTx const& tx) auto const from = tx[sfAccount]; auto const to = tx[~sfDestination].value_or(from); - return canWithdraw(view, from, to, tx[sfAmount], tx.isFieldPresent(sfDestinationTag)); + return canWithdraw( + view, from, to, tx[sfAmount], tx.isFieldPresent(sfDestinationTag), tx[~sfCredentialIDs]); } TER diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp index 498f3c99eb..e914596599 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -25,7 +26,11 @@ namespace xrpl { bool LoanBrokerCoverWithdraw::checkExtraFeatures(PreflightContext const& ctx) { - return checkLendingProtocolDependencies(ctx.rules, ctx.tx); + if (!checkLendingProtocolDependencies(ctx.rules, ctx.tx)) + return false; + + return !ctx.tx.isFieldPresent(sfCredentialIDs) || + (ctx.rules.enabled(featureCredentials) && ctx.rules.enabled(fixCleanup3_4_0)); } NotTEC @@ -49,6 +54,9 @@ LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx) } } + if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err)) + return err; + return tesSUCCESS; } @@ -109,6 +117,12 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) if (auto const ret = canTransfer(ctx.view, vaultAsset, pseudoAccountID, dstAcct, waive)) return ret; + // Validate credentials (if any) before canWithdraw, since canWithdraw may + // call credentials::authorizedDepositPreauth which assumes credentials + // already exist. + if (auto const err = credentials::valid(ctx.tx, ctx.view, account, ctx.j); !isTesSuccess(err)) + return err; + // Withdrawal to a 3rd party destination account is essentially a transfer. // Enforce all the usual asset transfer checks. AuthType authType = AuthType::WeakAuth; diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp index 7e32e720d6..40689572a0 100644 --- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,13 @@ namespace xrpl { +bool +VaultWithdraw::checkExtraFeatures(PreflightContext const& ctx) +{ + return !ctx.tx.isFieldPresent(sfCredentialIDs) || + (ctx.rules.enabled(featureCredentials) && ctx.rules.enabled(fixCleanup3_4_0)); +} + static WaiveUnrealizedLoss shouldWaiveWithdrawal(ReadView const& view, AccountID const& account, SLE::const_ref issuance) { @@ -59,6 +67,9 @@ VaultWithdraw::preflight(PreflightContext const& ctx) } } + if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err)) + return err; + return tesSUCCESS; } @@ -113,6 +124,12 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } + // Validate credentials (if any) before canWithdraw, since canWithdraw may + // call credentials::authorizedDepositPreauth which assumes credentials + // already exist. + if (auto const err = credentials::valid(ctx.tx, ctx.view, account, ctx.j); !isTesSuccess(err)) + return err; + if (fix313Enabled && amount.asset() == vaultShare) { // Post-fixCleanup3_1_3: if the user specified shares, convert @@ -144,7 +161,8 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) account, dstAcct, *maybeAssets, - ctx.tx.isFieldPresent(sfDestinationTag))) + ctx.tx.isFieldPresent(sfDestinationTag), + ctx.tx[~sfCredentialIDs])) return ret; } catch (std::overflow_error const&) diff --git a/src/test/app/lending/LoanBroker_test.cpp b/src/test/app/lending/LoanBroker_test.cpp index 5efa65d506..321ed5168f 100644 --- a/src/test/app/lending/LoanBroker_test.cpp +++ b/src/test/app/lending/LoanBroker_test.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include #include @@ -2532,6 +2534,132 @@ class LoanBroker_test : public beast::unit_test::Suite testRIPD4274MPT(); } + void + testCoverWithdrawCredentialDepositPreauth(FeatureBitset features) + { + testcase( + std::string{"CoverWithdraw with credential-based deposit preauth "} + + (features[fixCleanup3_4_0] ? "post-fix" : "pre-fix")); + using namespace jtx; + using namespace std::chrono_literals; + + bool const fixEnabled = features[fixCleanup3_4_0]; + + Env env(*this, features); + + Account const broker{"broker"}; + Account const dest{"dest"}; + Account const credIssuer{"credIssuer"}; + char const credType[] = "abcde"; + + env.fund(XRP(10'000), broker, dest, credIssuer); + env(fset(dest, asfDepositAuth)); + env.close(); + + PrettyAsset const asset{xrpIssue(), 1'000'000}; + + Vault const vault(env); + auto const [vaultTx, vaultKeylet] = vault.create({.owner = broker, .asset = asset}); + env(vaultTx); + env.close(); + + env(vault.deposit({.depositor = broker, .id = vaultKeylet.key, .amount = asset(1'000)})); + env.close(); + + auto const brokerKeylet = + keylet::loanBroker(broker.id(), SeqProxy::rawSequence(env.seq(broker))); + env(loan_broker::set(broker, vaultKeylet.key)); + env.close(); + + env(loan_broker::coverDeposit(broker, brokerKeylet.key, asset(500))); + env.close(); + + auto coverWithdrawToDest = [&]() { + return loan_broker::coverWithdraw(broker, brokerKeylet.key, asset(10)); + }; + + // Without any preauth, coverWithdraw to dest fails + env(coverWithdrawToDest(), loan_broker::kDestination(dest), Ter{tecNO_PERMISSION}); + env.close(); + + // Issue and accept a credential for the broker (with expiration) + auto jv = credentials::create(broker, credIssuer, credType); + std::uint32_t const expiration = + env.current()->header().parentCloseTime.time_since_epoch().count() + 100; + jv[sfExpiration.jsonName] = expiration; + env(jv); + env(credentials::accept(broker, credIssuer, credType)); + env.close(); + + auto const credKeylet = credentials::keylet(broker, credIssuer, credType); + auto const credIdx = + credentials::ledgerEntry(env, broker, credIssuer, credType)[jss::result][jss::index] + .asString(); + + // dest authorizes deposits from holders of credentials issued by credIssuer + env(deposit::authCredentials(dest, {{.issuer = credIssuer, .credType = credType}})); + env.close(); + + // Without supplying credentials, still fails + env(coverWithdrawToDest(), loan_broker::kDestination(dest), Ter{tecNO_PERMISSION}); + env.close(); + + if (!fixEnabled) + { + // Pre-fix: sfCredentialIDs in LoanBrokerCoverWithdraw is disabled + env(coverWithdrawToDest(), + loan_broker::kDestination(dest), + credentials::Ids({credIdx}), + Ter{temDISABLED}); + env.close(); + return; + } + + // With credentials, succeeds + env(coverWithdrawToDest(), loan_broker::kDestination(dest), credentials::Ids({credIdx})); + env.close(); + + // Bad credential id is rejected + std::string const invalidIdx = + "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E01E034"; + env(coverWithdrawToDest(), + loan_broker::kDestination(dest), + credentials::Ids({invalidIdx}), + Ter{tecBAD_CREDENTIALS}); + env.close(); + + // Malformed credential array (duplicates) is rejected by checkFields + env(coverWithdrawToDest(), + loan_broker::kDestination(dest), + credentials::Ids({credIdx, credIdx}), + Ter{temMALFORMED}); + env.close(); + + // Valid credential not authorized by dest hits authorizedDepositPreauth error path + char const credType2[] = "fghij"; + env(credentials::create(broker, credIssuer, credType2)); + env(credentials::accept(broker, credIssuer, credType2)); + env.close(); + auto const credIdx2 = + credentials::ledgerEntry(env, broker, credIssuer, credType2)[jss::result][jss::index] + .asString(); + env(coverWithdrawToDest(), + loan_broker::kDestination(dest), + credentials::Ids({credIdx2}), + Ter{tecNO_PERMISSION}); + env.close(); + + // Advance time past expiration: credentials yield tecEXPIRED and are deleted + env.close(150s); + BEAST_EXPECT(env.le(credKeylet)); + env(coverWithdrawToDest(), + loan_broker::kDestination(dest), + credentials::Ids({credIdx}), + Ter{tecEXPIRED}); + env.close(); + BEAST_EXPECT(!env.le(credKeylet)); + } + // Exercises canApplyToBrokerCover (fixCleanup3_2_0): a deposit, withdraw, // or clawback whose amount rounds to zero at sfCoverAvailable's precision // scale must be rejected with tecPRECISION_LOSS once the amendment is on, @@ -2770,6 +2898,9 @@ public: testRIPD4274(); + testCoverWithdrawCredentialDepositPreauth(all_ - fixCleanup3_4_0); + testCoverWithdrawCredentialDepositPreauth(all_); + testLoanBrokerDeleteLockedMPT(all_); testLoanBrokerDeleteLockedMPT(all_ - fixCleanup3_2_0); diff --git a/src/test/app/vault/VaultDomain_test.cpp b/src/test/app/vault/VaultDomain_test.cpp index db8943921b..5af0842962 100644 --- a/src/test/app/vault/VaultDomain_test.cpp +++ b/src/test/app/vault/VaultDomain_test.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -570,6 +572,112 @@ private: } } + void + testWithdrawCredentialDepositPreauth(FeatureBitset features) + { + testcase( + "withdraw with credential-based deposit preauth " + + std::string{features[fixCleanup3_4_0] ? "post-fix" : "pre-fix"}); + using namespace test::jtx; + using namespace std::chrono_literals; + + bool const fixEnabled = features[fixCleanup3_4_0]; + + Env env{*this, features}; + + Account const owner{"owner"}; + Account const depositor{"depositor"}; + Account const dest{"dest"}; + Account const credIssuer{"credIssuer"}; + char const credType[] = "abcde"; + + env.fund(XRP(1000), owner, depositor, dest, credIssuer); + env(fset(dest, asfDepositAuth)); + env.close(); + + PrettyAsset const asset{xrpIssue(), 1'000'000}; + Vault vault{env}; + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)})); + env.close(); + + auto withdrawToDest = [&]() { + auto wtx = + vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)}); + wtx[sfDestination] = dest.human(); + return wtx; + }; + + // Without any preauth, withdraw to dest fails + env(withdrawToDest(), Ter{tecNO_PERMISSION}); + env.close(); + + // Issue and accept a credential for the depositor (with expiration) + auto jv = credentials::create(depositor, credIssuer, credType); + std::uint32_t const expiration = + env.current()->header().parentCloseTime.time_since_epoch().count() + 100; + jv[sfExpiration.jsonName] = expiration; + env(jv); + env(credentials::accept(depositor, credIssuer, credType)); + env.close(); + + auto const credKeylet = credentials::keylet(depositor, credIssuer, credType); + auto const credIdx = + credentials::ledgerEntry(env, depositor, credIssuer, credType)[jss::result][jss::index] + .asString(); + + // dest authorizes deposits from holders of credentials issued by credIssuer + env(deposit::authCredentials(dest, {{.issuer = credIssuer, .credType = credType}})); + env.close(); + + // Withdraw without supplying credentials still fails + env(withdrawToDest(), Ter{tecNO_PERMISSION}); + env.close(); + + if (!fixEnabled) + { + // Pre-fix: sfCredentialIDs in VaultWithdraw is rejected as disabled + env(withdrawToDest(), credentials::Ids({credIdx}), Ter{temDISABLED}); + env.close(); + return; + } + + // Withdraw with credentials succeeds + env(withdrawToDest(), credentials::Ids({credIdx})); + env.close(); + + // Bad credential id is rejected + std::string const invalidIdx = + "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E01E034"; + env(withdrawToDest(), credentials::Ids({invalidIdx}), Ter{tecBAD_CREDENTIALS}); + env.close(); + + // Malformed credential array (duplicates) is rejected by checkFields + env(withdrawToDest(), credentials::Ids({credIdx, credIdx}), Ter{temMALFORMED}); + env.close(); + + // Valid credential not authorized by dest hits authorizedDepositPreauth error path + char const credType2[] = "fghij"; + env(credentials::create(depositor, credIssuer, credType2)); + env(credentials::accept(depositor, credIssuer, credType2)); + env.close(); + auto const credIdx2 = + credentials::ledgerEntry(env, depositor, credIssuer, credType2)[jss::result][jss::index] + .asString(); + env(withdrawToDest(), credentials::Ids({credIdx2}), Ter{tecNO_PERMISSION}); + env.close(); + + // Advance time past expiration: credentials yield tecEXPIRED and are deleted + env.close(150s); + BEAST_EXPECT(env.le(credKeylet)); + env(withdrawToDest(), credentials::Ids({credIdx}), Ter{tecEXPIRED}); + env.close(); + BEAST_EXPECT(!env.le(credKeylet)); + } + public: void run() override @@ -578,6 +686,8 @@ public: testDomainLossAfterAcquisition(); testDomainCheckBuyerSideOffer(); testWithDomainChecXRP(); + testWithdrawCredentialDepositPreauth(all_ - fixCleanup3_4_0); + testWithdrawCredentialDepositPreauth(all_); } }; diff --git a/src/tests/libxrpl/protocol_autogen/transactions/LoanBrokerCoverWithdrawTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/LoanBrokerCoverWithdrawTests.cpp index 5b0a8c9146..043ab0a252 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/LoanBrokerCoverWithdrawTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/LoanBrokerCoverWithdrawTests.cpp @@ -33,6 +33,7 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderSettersRoundTrip) auto const amountValue = canonical_AMOUNT(); auto const destinationValue = canonical_ACCOUNT(); auto const destinationTagValue = canonical_UINT32(); + auto const credentialIDsValue = canonical_VECTOR256(); LoanBrokerCoverWithdrawBuilder builder{ accountValue, @@ -45,6 +46,7 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderSettersRoundTrip) // Set optional fields builder.setDestination(destinationValue); builder.setDestinationTag(destinationTagValue); + builder.setCredentialIDs(credentialIDsValue); auto tx = builder.build(publicKey, secretKey); @@ -90,6 +92,14 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderSettersRoundTrip) EXPECT_TRUE(tx.hasDestinationTag()); } + { + auto const& expected = credentialIDsValue; + auto const actualOpt = tx.getCredentialIDs(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present"; + expectEqualField(expected, *actualOpt, "sfCredentialIDs"); + EXPECT_TRUE(tx.hasCredentialIDs()); + } + } // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, @@ -110,6 +120,7 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderFromStTxRoundTrip) auto const amountValue = canonical_AMOUNT(); auto const destinationValue = canonical_ACCOUNT(); auto const destinationTagValue = canonical_UINT32(); + auto const credentialIDsValue = canonical_VECTOR256(); // Build an initial transaction LoanBrokerCoverWithdrawBuilder initialBuilder{ @@ -122,6 +133,7 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderFromStTxRoundTrip) initialBuilder.setDestination(destinationValue); initialBuilder.setDestinationTag(destinationTagValue); + initialBuilder.setCredentialIDs(credentialIDsValue); auto initialTx = initialBuilder.build(publicKey, secretKey); @@ -166,6 +178,13 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderFromStTxRoundTrip) expectEqualField(expected, *actualOpt, "sfDestinationTag"); } + { + auto const& expected = credentialIDsValue; + auto const actualOpt = rebuiltTx.getCredentialIDs(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present"; + expectEqualField(expected, *actualOpt, "sfCredentialIDs"); + } + } // 3) Verify wrapper throws when constructed from wrong transaction type. @@ -229,6 +248,8 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(tx.getDestination().has_value()); EXPECT_FALSE(tx.hasDestinationTag()); EXPECT_FALSE(tx.getDestinationTag().has_value()); + EXPECT_FALSE(tx.hasCredentialIDs()); + EXPECT_FALSE(tx.getCredentialIDs().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/transactions/VaultWithdrawTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/VaultWithdrawTests.cpp index 4067a6551d..518957d47b 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/VaultWithdrawTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/VaultWithdrawTests.cpp @@ -33,6 +33,7 @@ TEST(TransactionsVaultWithdrawTests, BuilderSettersRoundTrip) auto const amountValue = canonical_AMOUNT(); auto const destinationValue = canonical_ACCOUNT(); auto const destinationTagValue = canonical_UINT32(); + auto const credentialIDsValue = canonical_VECTOR256(); VaultWithdrawBuilder builder{ accountValue, @@ -45,6 +46,7 @@ TEST(TransactionsVaultWithdrawTests, BuilderSettersRoundTrip) // Set optional fields builder.setDestination(destinationValue); builder.setDestinationTag(destinationTagValue); + builder.setCredentialIDs(credentialIDsValue); auto tx = builder.build(publicKey, secretKey); @@ -90,6 +92,14 @@ TEST(TransactionsVaultWithdrawTests, BuilderSettersRoundTrip) EXPECT_TRUE(tx.hasDestinationTag()); } + { + auto const& expected = credentialIDsValue; + auto const actualOpt = tx.getCredentialIDs(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present"; + expectEqualField(expected, *actualOpt, "sfCredentialIDs"); + EXPECT_TRUE(tx.hasCredentialIDs()); + } + } // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, @@ -110,6 +120,7 @@ TEST(TransactionsVaultWithdrawTests, BuilderFromStTxRoundTrip) auto const amountValue = canonical_AMOUNT(); auto const destinationValue = canonical_ACCOUNT(); auto const destinationTagValue = canonical_UINT32(); + auto const credentialIDsValue = canonical_VECTOR256(); // Build an initial transaction VaultWithdrawBuilder initialBuilder{ @@ -122,6 +133,7 @@ TEST(TransactionsVaultWithdrawTests, BuilderFromStTxRoundTrip) initialBuilder.setDestination(destinationValue); initialBuilder.setDestinationTag(destinationTagValue); + initialBuilder.setCredentialIDs(credentialIDsValue); auto initialTx = initialBuilder.build(publicKey, secretKey); @@ -166,6 +178,13 @@ TEST(TransactionsVaultWithdrawTests, BuilderFromStTxRoundTrip) expectEqualField(expected, *actualOpt, "sfDestinationTag"); } + { + auto const& expected = credentialIDsValue; + auto const actualOpt = rebuiltTx.getCredentialIDs(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present"; + expectEqualField(expected, *actualOpt, "sfCredentialIDs"); + } + } // 3) Verify wrapper throws when constructed from wrong transaction type. @@ -229,6 +248,8 @@ TEST(TransactionsVaultWithdrawTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(tx.getDestination().has_value()); EXPECT_FALSE(tx.hasDestinationTag()); EXPECT_FALSE(tx.getDestinationTag().has_value()); + EXPECT_FALSE(tx.hasCredentialIDs()); + EXPECT_FALSE(tx.getCredentialIDs().has_value()); } }