fix: Check credential for LoanBrokerCoverWithdraw and VaultWithdraw (#7107)

Co-authored-by: Peter Chen <ychen@ripple.com>
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
This commit is contained in:
Shawn Xie
2026-08-20 15:33:46 +00:00
committed by GitHub
parent 959a186a0f
commit e3ba569187
12 changed files with 445 additions and 13 deletions

View File

@@ -24,6 +24,7 @@
#include <optional>
#include <set>
#include <utility>
#include <vector>
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<std::vector<uint256>> 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<std::vector<uint256>> 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).
*/

View File

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

View File

@@ -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<SF_VECTOR256::type::value_type>
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<typename SF_VECTOR256::type::value_type> const& value)
{
object_[sfCredentialIDs] = value;
return *this;
}
/**
* @brief Build and return the LoanBrokerCoverWithdraw wrapper.
* @param publicKey The public key for signing.

View File

@@ -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<SF_VECTOR256::type::value_type>
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<typename SF_VECTOR256::type::value_type> const& value)
{
object_[sfCredentialIDs] = value;
return *this;
}
/**
* @brief Build and return the VaultWithdraw wrapper.
* @param publicKey The public key for signing.

View File

@@ -20,6 +20,9 @@ public:
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -35,6 +35,7 @@
#include <cstdint>
#include <optional>
#include <set>
#include <vector>
namespace xrpl {
@@ -467,7 +468,8 @@ canWithdraw(
AccountID const& to,
SLE::const_ref toSle,
STAmount const& amount,
bool hasDestinationTag)
bool hasDestinationTag,
std::optional<std::vector<uint256>> 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<std::vector<uint256>> 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

View File

@@ -5,6 +5,7 @@
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/Feature.h>
@@ -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;

View File

@@ -6,6 +6,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -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&)

View File

@@ -6,6 +6,8 @@
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h>
#include <test/jtx/credentials.h>
#include <test/jtx/deposit.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/mpt.h>
@@ -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);

View File

@@ -4,6 +4,7 @@
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/credentials.h>
#include <test/jtx/deposit.h>
#include <test/jtx/flags.h>
#include <test/jtx/offer.h>
#include <test/jtx/pay.h>
@@ -18,6 +19,7 @@
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
@@ -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_);
}
};

View File

@@ -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());
}
}

View File

@@ -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());
}
}