mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-12 13:16:53 +00:00
Compare commits
63 Commits
develop
...
tapanito/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fee67c8f87 | ||
|
|
357f1a1a29 | ||
|
|
c9f6a606be | ||
|
|
a4f5f28edd | ||
|
|
863995691c | ||
|
|
24db40e56c | ||
|
|
da4513d096 | ||
|
|
2e2fddefe9 | ||
|
|
db997ecad9 | ||
|
|
f2495dccf7 | ||
|
|
68e4fbdf2b | ||
|
|
bb0a09ae21 | ||
|
|
d94232007f | ||
|
|
8899d346af | ||
|
|
df8bfbe5af | ||
|
|
347d1a19ef | ||
|
|
5fe99dc2ae | ||
|
|
b970c66a37 | ||
|
|
7e5a4c9d04 | ||
|
|
d65fab27a1 | ||
|
|
b5d25c5ab1 | ||
|
|
7222150095 | ||
|
|
68d7555033 | ||
|
|
a67da5c2ed | ||
|
|
d2f23b2f5b | ||
|
|
4067e5025f | ||
|
|
cc9dbe2243 | ||
|
|
ebfa659593 | ||
|
|
3deb0de8d7 | ||
|
|
ed4330a7d6 | ||
|
|
feba605998 | ||
|
|
b322097529 | ||
|
|
e159d27373 | ||
|
|
f034ca0844 | ||
|
|
ba53026006 | ||
|
|
34773080df | ||
|
|
2b716eb4c7 | ||
|
|
872347224d | ||
|
|
b08451c118 | ||
|
|
3c3bd75991 | ||
|
|
81e69b9317 | ||
|
|
8732e84e54 | ||
|
|
7459fe454d | ||
|
|
884530e415 | ||
|
|
d972071979 | ||
|
|
cd1e8ebbc3 | ||
|
|
3cfb5fe56d | ||
|
|
106bf48725 | ||
|
|
71b9f982bb | ||
|
|
74c968d4e3 | ||
|
|
f57b715936 | ||
|
|
82b0d57aac | ||
|
|
087a9c1cf3 | ||
|
|
1010866ba0 | ||
|
|
a2198146a8 | ||
|
|
c808c46049 | ||
|
|
167147281c | ||
|
|
ba60306610 | ||
|
|
6674500896 | ||
|
|
c5d7ebe93d | ||
|
|
d0b5ca9dab | ||
|
|
5e51893e9b | ||
|
|
3422c11d02 |
@@ -99,4 +99,15 @@ sharesToAssetsWithdraw(
|
||||
[[nodiscard]] bool
|
||||
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref issuance);
|
||||
|
||||
/** Determine if a vault is insolvent. A vault is considered insolvent when
|
||||
the total assets in the vault are zero, and outstanding shares are non-zero.
|
||||
|
||||
@param vault The vault SLE.
|
||||
@param shareIssuance The MPTokenIssuance SLE for the vault's shares.
|
||||
|
||||
@return True if the vault is insolvent, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isVaultInsolvent(SLE::const_ref vault, SLE::const_ref shareIssuance);
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -198,7 +198,9 @@ enum LedgerEntryType : std::uint16_t {
|
||||
LSF_FLAG(lsfAccepted, 0x00010000)) \
|
||||
\
|
||||
LEDGER_OBJECT(Vault, \
|
||||
LSF_FLAG(lsfVaultPrivate, 0x00010000)) \
|
||||
LSF_FLAG(lsfVaultPrivate, 0x00010000) \
|
||||
LSF_FLAG(lsfVaultDepositBlocked, 0x00020000) /* True, vault deposit is blocked */ \
|
||||
LSF_FLAG(lsfVaultOwnerCanBlockDeposit, 0x00040000)) /* True, vault owner can block deposit */ \
|
||||
\
|
||||
LEDGER_OBJECT(Loan, \
|
||||
LSF_FLAG(lsfLoanDefault, 0x00010000) \
|
||||
|
||||
@@ -185,7 +185,13 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
|
||||
\
|
||||
TRANSACTION(VaultCreate, \
|
||||
TF_FLAG(tfVaultPrivate, lsfVaultPrivate) \
|
||||
TF_FLAG(tfVaultShareNonTransferable, 0x00020000), \
|
||||
TF_FLAG(tfVaultShareNonTransferable, 0x00020000) \
|
||||
TF_FLAG(tfVaultOwnerCanBlockDeposit, lsfVaultOwnerCanBlockDeposit), \
|
||||
MASK_ADJ(0)) \
|
||||
\
|
||||
TRANSACTION(VaultSet, \
|
||||
TF_FLAG(tfVaultDepositBlock, 0x00010000) \
|
||||
TF_FLAG(tfVaultDepositUnblock, 0x00020000), \
|
||||
MASK_ADJ(0)) \
|
||||
\
|
||||
TRANSACTION(Batch, \
|
||||
@@ -215,7 +221,6 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
|
||||
TF_FLAG(tfLoanImpair, 0x00020000) \
|
||||
TF_FLAG(tfLoanUnimpair, 0x00040000), \
|
||||
MASK_ADJ(0))
|
||||
|
||||
// clang-format on
|
||||
|
||||
// Create all the flag values.
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// Add new amendments to the top of this list.
|
||||
// Keep it sorted in reverse chronological order.
|
||||
|
||||
XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_1_3, Supported::Yes, VoteBehavior::DefaultYes)
|
||||
|
||||
@@ -887,6 +887,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete,
|
||||
MustDeleteAcct | DestroyMptIssuance | MustModifyVault,
|
||||
({
|
||||
{sfVaultID, SoeRequired},
|
||||
{sfMemoData, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This transaction trades assets for shares with a vault. */
|
||||
|
||||
@@ -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<SF_VL::type::value_type>
|
||||
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<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfMemoData] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the VaultDelete wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
|
||||
@@ -13,6 +13,9 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
static std::uint32_t
|
||||
getFlagsMask(PreflightContext const& ctx);
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
|
||||
@@ -137,4 +137,18 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref
|
||||
return sleToken->getFieldU64(sfMPTAmount) == outstanding;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isVaultInsolvent(SLE::const_ref vault, SLE::const_ref shareIssuance)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::isVaultInsolvent : Vault sle");
|
||||
XRPL_ASSERT(
|
||||
shareIssuance && shareIssuance->getType() == ltMPTOKEN_ISSUANCE,
|
||||
"xrpl::isVaultInsolvent : MPTokenIssuance sle");
|
||||
|
||||
auto const assetsTotal = vault->at(sfAssetsTotal);
|
||||
auto const sharesOutstanding = shareIssuance->at(sfOutstandingAmount);
|
||||
|
||||
return assetsTotal == 0 && sharesOutstanding > 0;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -48,7 +48,10 @@ VaultCreate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
std::uint32_t
|
||||
VaultCreate::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
return tfVaultCreateMask;
|
||||
if (ctx.rules.enabled(featureLendingProtocolV1_1))
|
||||
return tfVaultCreateMask;
|
||||
|
||||
return tfVaultCreateMask | tfVaultOwnerCanBlockDeposit;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
@@ -178,6 +181,7 @@ VaultCreate::doApply()
|
||||
std::uint32_t mptFlags = 0;
|
||||
if (!tx.isFlag(tfVaultShareNonTransferable))
|
||||
mptFlags |= (lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer);
|
||||
|
||||
if (tx.isFlag(tfVaultPrivate))
|
||||
mptFlags |= lsfMPTRequireAuth;
|
||||
|
||||
@@ -216,7 +220,13 @@ VaultCreate::doApply()
|
||||
auto const& mptIssuanceID = *maybeShare;
|
||||
|
||||
vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset});
|
||||
vault->at(sfFlags) = tx.getFlags() & tfVaultPrivate;
|
||||
if (tx.isFlag(tfVaultPrivate))
|
||||
vault->setFlag(lsfVaultPrivate);
|
||||
|
||||
if (view().rules().enabled(featureLendingProtocolV1_1) &&
|
||||
tx.isFlag(tfVaultOwnerCanBlockDeposit))
|
||||
vault->setFlag(lsfVaultOwnerCanBlockDeposit);
|
||||
|
||||
vault->at(sfSequence) = sequence;
|
||||
vault->at(sfOwner) = accountID_;
|
||||
vault->at(sfAccount) = pseudoId;
|
||||
@@ -249,7 +259,7 @@ VaultCreate::doApply()
|
||||
return err;
|
||||
|
||||
// If the vault is private, set the authorized flag for the vault owner
|
||||
if (tx.isFlag(tfVaultPrivate))
|
||||
if (vault->isFlag(lsfVaultPrivate))
|
||||
{
|
||||
if (auto const err = authorizeMPToken(
|
||||
view(), preFeeBalance_, mptIssuanceID, pseudoId, ctx_.journal, {}, accountID_);
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
@@ -28,6 +30,14 @@ VaultDelete::preflight(PreflightContext const& ctx)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfMemoData) && !ctx.rules.enabled(featureLendingProtocolV1_1))
|
||||
return temDISABLED;
|
||||
|
||||
// The sfMemoData field is an optional field used to record the deletion reason.
|
||||
if (auto const data = ctx.tx[~sfMemoData];
|
||||
data && !validDataLength(data, kMaxDataPayloadLength))
|
||||
return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,8 +90,8 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
auto const sleShareIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleShareIssuance)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(ctx.j.error()) << "VaultDeposit: missing issuance of vault shares.";
|
||||
@@ -99,7 +99,7 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
if (sleIssuance->isFlag(lsfMPTLocked))
|
||||
if (sleShareIssuance->isFlag(lsfMPTLocked))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(ctx.j.error()) << "VaultDeposit: issuance of vault shares is locked.";
|
||||
@@ -107,6 +107,24 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
if (ctx.view.rules().enabled(featureLendingProtocolV1_1))
|
||||
{
|
||||
// Perform these checks early to avoid unnecessary processing
|
||||
|
||||
// The Vault is insolvent, deposits are not allowed
|
||||
if (isVaultInsolvent(vault, sleShareIssuance))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultDeposit: Vault is insolvent, deposits are not allowed";
|
||||
return tecLOCKED;
|
||||
}
|
||||
|
||||
if (vault->isFlag(lsfVaultDepositBlocked))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultDeposit: Vault deposits are blocked";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot deposit inside Vault an Asset frozen for the depositor
|
||||
if (isFrozen(ctx.view, account, vaultAsset))
|
||||
return vaultAsset.holds<Issue>() ? tecFROZEN : tecLOCKED;
|
||||
@@ -117,7 +135,7 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
if (vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner))
|
||||
{
|
||||
auto const maybeDomainID = sleIssuance->at(~sfDomainID);
|
||||
auto const maybeDomainID = sleShareIssuance->at(~sfDomainID);
|
||||
// Since this is a private vault and the account is not its owner, we
|
||||
// perform authorization check based on DomainID read from sleIssuance.
|
||||
// Had the vault shares been a regular MPToken, we would allow
|
||||
|
||||
@@ -12,9 +12,12 @@
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
bool
|
||||
@@ -23,6 +26,29 @@ VaultSet::checkExtraFeatures(PreflightContext const& ctx)
|
||||
return !ctx.tx.isFieldPresent(sfDomainID) || ctx.rules.enabled(featurePermissionedDomains);
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
VaultSet::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
if (ctx.rules.enabled(featureLendingProtocolV1_1))
|
||||
return tfVaultSetMask;
|
||||
|
||||
// Add tfVaultDepositBlock and tfVaultDepositUnblock flags to indicate they are disabled
|
||||
return tfVaultSetMask | tfVaultDepositBlock | tfVaultDepositUnblock;
|
||||
}
|
||||
|
||||
static bool
|
||||
isValidVaultUpdate(PreflightContext const& ctx)
|
||||
{
|
||||
auto const atLeastOneFieldPresent = ctx.tx.isFieldPresent(sfDomainID) ||
|
||||
ctx.tx.isFieldPresent(sfAssetsMaximum) || ctx.tx.isFieldPresent(sfData);
|
||||
|
||||
// Mask of valid, non-universal flags: any bit set here means the
|
||||
// transaction is requesting a meaningful flag change.
|
||||
auto const expectedFlags = ~(VaultSet::getFlagsMask(ctx) | tfUniversal);
|
||||
|
||||
return atLeastOneFieldPresent || ((ctx.tx.getFlags() & expectedFlags) != 0u);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
VaultSet::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -50,13 +76,19 @@ VaultSet::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.tx.isFieldPresent(sfDomainID) && !ctx.tx.isFieldPresent(sfAssetsMaximum) &&
|
||||
!ctx.tx.isFieldPresent(sfData))
|
||||
if (!isValidVaultUpdate(ctx))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultSet: nothing is being updated.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFlag(tfVaultDepositBlock) && ctx.tx.isFlag(tfVaultDepositUnblock))
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "VaultSet: cannot set tfVaultDepositBlock and tfVaultDepositUnblock simultaneously.";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -110,6 +142,29 @@ VaultSet::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.view.rules().enabled(featureLendingProtocolV1_1))
|
||||
{
|
||||
// The Vault is not configured to support deposit blocking
|
||||
if (!vault->isFlag(lsfVaultOwnerCanBlockDeposit) &&
|
||||
(ctx.tx.isFlag(tfVaultDepositBlock) || ctx.tx.isFlag(tfVaultDepositUnblock)))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultSet: vault does not support blocking deposits";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (vault->isFlag(lsfVaultDepositBlocked) && ctx.tx.isFlag(tfVaultDepositBlock))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultSet: vault deposit is already blocked";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (!vault->isFlag(lsfVaultDepositBlocked) && ctx.tx.isFlag(tfVaultDepositUnblock))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultSet: vault deposit is already unblocked";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -167,6 +222,15 @@ VaultSet::doApply()
|
||||
view().update(sleIssuance);
|
||||
}
|
||||
|
||||
if (view().rules().enabled(featureLendingProtocolV1_1))
|
||||
{
|
||||
if (tx.isFlag(tfVaultDepositBlock))
|
||||
vault->setFlag(lsfVaultDepositBlocked);
|
||||
|
||||
if (tx.isFlag(tfVaultDepositUnblock))
|
||||
vault->clearFlag(lsfVaultDepositBlocked);
|
||||
}
|
||||
|
||||
// Note, we must update Vault object even if only DomainID is being updated
|
||||
// in Issuance object. Otherwise it's really difficult for Vault invariants
|
||||
// to verify the operation.
|
||||
|
||||
@@ -707,6 +707,14 @@ class Vault_test : public beast::unit_test::Suite
|
||||
tx[sfFlags] = tfClearDeepFreeze;
|
||||
env(tx, Ter{temINVALID_FLAG});
|
||||
|
||||
{
|
||||
env.disableFeature(featureLendingProtocolV1_1);
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
tx[sfFlags] = tfVaultOwnerCanBlockDeposit;
|
||||
env(tx, Ter(temINVALID_FLAG));
|
||||
env.enableFeature(featureLendingProtocolV1_1);
|
||||
}
|
||||
|
||||
{
|
||||
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
||||
tx[sfFlags] = tfClearDeepFreeze;
|
||||
@@ -988,7 +996,7 @@ class Vault_test : public beast::unit_test::Suite
|
||||
{
|
||||
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
||||
tx[sfAssetsMaximum] = kNegativeAmount(asset).number();
|
||||
env(tx, Ter{temMALFORMED});
|
||||
env(tx, Ter(temMALFORMED));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1013,13 +1021,114 @@ class Vault_test : public beast::unit_test::Suite
|
||||
|
||||
testCase(
|
||||
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
|
||||
testcase("invalid set immutable flag");
|
||||
testcase("invalid withdraw amount");
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
|
||||
{
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = owner, .id = keylet.key, .amount = kNegativeAmount(asset)});
|
||||
env(tx, Ter(temBAD_AMOUNT));
|
||||
}
|
||||
|
||||
{
|
||||
auto tx =
|
||||
vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(0)});
|
||||
env(tx, Ter(temBAD_AMOUNT));
|
||||
}
|
||||
});
|
||||
|
||||
testCase(
|
||||
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
|
||||
testcase("set nothing updated");
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
|
||||
{
|
||||
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
||||
tx[sfFlags] = tfVaultPrivate;
|
||||
env(tx, Ter(temMALFORMED));
|
||||
}
|
||||
});
|
||||
|
||||
testCase(
|
||||
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
|
||||
testcase("create with invalid metadata");
|
||||
|
||||
auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
|
||||
{
|
||||
auto tx = tx1;
|
||||
tx[sfMPTokenMetadata] = "";
|
||||
env(tx, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
{
|
||||
auto tx = tx1;
|
||||
// This metadata is for the share token.
|
||||
// A hexadecimal string of 1025 bytes.
|
||||
tx[sfMPTokenMetadata] = std::string(2050, 'B');
|
||||
env(tx, Ter(temMALFORMED));
|
||||
}
|
||||
});
|
||||
|
||||
testCase(
|
||||
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
|
||||
testcase("set negative maximum");
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
|
||||
{
|
||||
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
||||
tx[sfAssetsMaximum] = kNegativeAmount(asset).number();
|
||||
env(tx, Ter(temMALFORMED));
|
||||
}
|
||||
});
|
||||
|
||||
testCase(
|
||||
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
|
||||
testcase("invalid deposit amount");
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
|
||||
{
|
||||
auto tx = vault.deposit(
|
||||
{.depositor = owner, .id = keylet.key, .amount = kNegativeAmount(asset)});
|
||||
env(tx, Ter(temBAD_AMOUNT));
|
||||
}
|
||||
|
||||
{
|
||||
auto tx =
|
||||
vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(0)});
|
||||
env(tx, Ter(temBAD_AMOUNT));
|
||||
}
|
||||
});
|
||||
|
||||
testCase(
|
||||
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
|
||||
testcase("set flags fail without featureLendingProtocolV1_1");
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
|
||||
{
|
||||
env.disableFeature(featureLendingProtocolV1_1);
|
||||
env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}),
|
||||
Ter(temINVALID_FLAG));
|
||||
env(vault.set(
|
||||
{.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}),
|
||||
Ter(temINVALID_FLAG));
|
||||
env.enableFeature(featureLendingProtocolV1_1);
|
||||
}
|
||||
});
|
||||
|
||||
testCase(
|
||||
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
|
||||
testcase("invalid set flag combination");
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
|
||||
{
|
||||
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
||||
tx[sfFlags] = tfVaultDepositBlock | tfVaultDepositUnblock;
|
||||
env(tx, Ter(temINVALID_FLAG));
|
||||
}
|
||||
});
|
||||
@@ -2276,6 +2385,106 @@ class Vault_test : public beast::unit_test::Suite
|
||||
env(vault.del({.owner = owner, .id = keylet.key}));
|
||||
});
|
||||
|
||||
testCase([&, this](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester const& mptt) {
|
||||
testcase("MPT insolvent vault blocks deposits");
|
||||
|
||||
auto const depositAmount = asset(20);
|
||||
|
||||
auto const [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// First deposit assets to later show that withdrawals are not blocked
|
||||
{
|
||||
auto const tx = vault.deposit(
|
||||
{.depositor = depositor, .id = vaultKeylet.key, .amount = depositAmount});
|
||||
env(tx, Ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto const& brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner));
|
||||
auto const& loanKeylet = keylet::loan(brokerKeylet.key, 1);
|
||||
|
||||
// Create a LoanBroker and a Loan, to drain the vault
|
||||
{
|
||||
using namespace loanBroker;
|
||||
using namespace loan;
|
||||
|
||||
env(set(owner, vaultKeylet.key));
|
||||
env.close();
|
||||
|
||||
// Create a simple Loan for the full amount of Vault assets
|
||||
env(set(depositor, brokerKeylet.key, depositAmount.value()),
|
||||
loan::kInterestRate(TenthBips32(0)),
|
||||
kPaymentInterval(120),
|
||||
kPaymentTotal(1),
|
||||
Sig(sfCounterpartySignature, owner),
|
||||
Fee(env.current()->fees().base * 2),
|
||||
Ter(tesSUCCESS));
|
||||
env.close(std::chrono::seconds{120 + 60});
|
||||
|
||||
env(manage(owner, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const sleVault = env.le(vaultKeylet);
|
||||
if (!BEAST_EXPECT(sleVault))
|
||||
return;
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptIssuance(sleVault->at(sfShareMPTID)));
|
||||
if (!BEAST_EXPECT(sleIssuance))
|
||||
return;
|
||||
|
||||
auto const shareBalance = sleIssuance->at(sfOutstandingAmount);
|
||||
auto const expectedShares = Number{
|
||||
depositAmount.number().mantissa(),
|
||||
depositAmount.number().exponent() + sleVault->at(sfScale)};
|
||||
|
||||
// verify that the vault is insolvent
|
||||
if (!BEAST_EXPECT(
|
||||
sleVault->at(sfAssetsTotal) == 0 && sleVault->at(sfAssetsAvailable) == 0 &&
|
||||
shareBalance == expectedShares))
|
||||
return;
|
||||
}
|
||||
|
||||
// The vault is insolvent, deposit must fail
|
||||
{
|
||||
auto const tx = vault.deposit(
|
||||
{.depositor = depositor, .id = vaultKeylet.key, .amount = asset(20)});
|
||||
env(tx, Ter(tecLOCKED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Clean up the vault to delete it
|
||||
{
|
||||
auto const sleVault = env.le(vaultKeylet);
|
||||
if (!BEAST_EXPECT(sleVault))
|
||||
return;
|
||||
|
||||
Asset const share = sleVault->at(sfShareMPTID);
|
||||
env(vault.clawback(
|
||||
{.issuer = owner,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
.amount = share(0)}),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
env(loan::del(owner, loanKeylet.key), Ter(tesSUCCESS));
|
||||
env(loanBroker::del(owner, brokerKeylet.key), Ter(tesSUCCESS));
|
||||
env(vault.del({.owner = owner, .id = vaultKeylet.key}));
|
||||
env.close();
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
testcase("MPT non-transferable: pre-fixCleanup3_2_0 withdraw blocked");
|
||||
|
||||
@@ -3330,10 +3539,111 @@ class Vault_test : public beast::unit_test::Suite
|
||||
env(vault.del({.owner = owner, .id = keylet.key}));
|
||||
env.close();
|
||||
});
|
||||
|
||||
testCase([&, this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const& issuer,
|
||||
Account const&,
|
||||
auto vaultAccount,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU insolvent vault blocks deposits");
|
||||
|
||||
auto const depositAmount = asset(20);
|
||||
|
||||
auto const [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// First deposit assets to later show that withdrawals are not blocked
|
||||
{
|
||||
auto const tx = vault.deposit(
|
||||
{.depositor = issuer, .id = vaultKeylet.key, .amount = depositAmount});
|
||||
env(tx, Ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner));
|
||||
auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
|
||||
|
||||
// Create a LoanBroker and a Loan, to drain the vault
|
||||
{
|
||||
using namespace loanBroker;
|
||||
using namespace loan;
|
||||
|
||||
env(set(owner, vaultKeylet.key), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// Create a simple Loan for the full amount of Vault assets
|
||||
env(set(issuer, brokerKeylet.key, depositAmount.value()),
|
||||
loan::kInterestRate(TenthBips32(0)),
|
||||
kPaymentInterval(120),
|
||||
kPaymentTotal(1),
|
||||
Sig(sfCounterpartySignature, owner),
|
||||
Fee(env.current()->fees().base * 2),
|
||||
Ter(tesSUCCESS));
|
||||
env.close(std::chrono::seconds{120 + 60});
|
||||
|
||||
env(manage(owner, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const sleVault = env.le(vaultKeylet);
|
||||
if (!BEAST_EXPECT(sleVault))
|
||||
return;
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptIssuance(sleVault->at(sfShareMPTID)));
|
||||
if (!BEAST_EXPECT(sleIssuance))
|
||||
return;
|
||||
|
||||
auto const shareBalance = sleIssuance->at(sfOutstandingAmount);
|
||||
auto const expectedShares = Number{
|
||||
depositAmount.number().mantissa(),
|
||||
depositAmount.number().exponent() + sleVault->at(sfScale)};
|
||||
|
||||
// verify that the vault is insolvent
|
||||
if (!BEAST_EXPECT(
|
||||
sleVault->at(sfAssetsTotal) == 0 && sleVault->at(sfAssetsAvailable) == 0 &&
|
||||
shareBalance == expectedShares))
|
||||
return;
|
||||
}
|
||||
|
||||
// The vault is insolvent, deposit must fail
|
||||
{
|
||||
auto const tx = vault.deposit(
|
||||
{.depositor = issuer, .id = vaultKeylet.key, .amount = asset(20)});
|
||||
env(tx, Ter(tecLOCKED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Clean up the vault to delete it
|
||||
{
|
||||
auto const sleVault = env.le(vaultKeylet);
|
||||
if (!BEAST_EXPECT(sleVault))
|
||||
return;
|
||||
|
||||
Asset const share = sleVault->at(sfShareMPTID);
|
||||
env(vault.clawback(
|
||||
{.issuer = owner,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = issuer,
|
||||
.amount = share(0)}),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
env(loan::del(owner, loanKeylet.key), Ter(tesSUCCESS));
|
||||
env(loanBroker::del(owner, brokerKeylet.key), Ter(tesSUCCESS));
|
||||
env(vault.del({.owner = owner, .id = vaultKeylet.key}));
|
||||
env.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testWithDomainCheck()
|
||||
testPrivateVault()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
@@ -3364,7 +3674,10 @@ class Vault_test : public beast::unit_test::Suite
|
||||
env(pay(issuer, charlie, asset(5)));
|
||||
env.close();
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
|
||||
auto [tx, keylet] = vault.create(
|
||||
{.owner = owner,
|
||||
.asset = asset,
|
||||
.flags = tfVaultPrivate | tfVaultOwnerCanBlockDeposit});
|
||||
env(tx);
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(keylet));
|
||||
@@ -3389,6 +3702,28 @@ class Vault_test : public beast::unit_test::Suite
|
||||
env(tx, Ter{tecOBJECT_NOT_FOUND});
|
||||
}
|
||||
|
||||
{
|
||||
testcase("blocking a private vault does not change lsfVaultPrivate flag");
|
||||
auto tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock});
|
||||
env(tx, Ter(tesSUCCESS));
|
||||
auto const sleVault = env.le(keylet);
|
||||
if (!BEAST_EXPECT(sleVault))
|
||||
return;
|
||||
BEAST_EXPECT(sleVault->isFlag(lsfVaultDepositBlocked));
|
||||
BEAST_EXPECT(sleVault->isFlag(lsfVaultPrivate));
|
||||
}
|
||||
|
||||
{
|
||||
testcase("unblocking a private vault does not change lsfVaultPrivate flag");
|
||||
auto tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock});
|
||||
env(tx, Ter(tesSUCCESS));
|
||||
auto const sleVault = env.le(keylet);
|
||||
if (!BEAST_EXPECT(sleVault))
|
||||
return;
|
||||
BEAST_EXPECT(!sleVault->isFlag(lsfVaultDepositBlocked));
|
||||
BEAST_EXPECT(sleVault->isFlag(lsfVaultPrivate));
|
||||
}
|
||||
|
||||
{
|
||||
testcase("private vault set domainId");
|
||||
|
||||
@@ -8027,6 +8362,230 @@ class Vault_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testVaultDepositBlockGeneral()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this};
|
||||
Account const owner{"owner"};
|
||||
Account const other{"other"};
|
||||
|
||||
env.fund(XRP(100'000'000), owner, other);
|
||||
Vault vault{env};
|
||||
PrettyAsset const asset = xrpIssue();
|
||||
std::string const prefix = "VaultDepositBlock: ";
|
||||
|
||||
auto const blockVault = [&](TER expectedTer, Keylet const& keylet) {
|
||||
env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}),
|
||||
Ter(expectedTer));
|
||||
};
|
||||
|
||||
auto const unblockVault = [&](TER expectedTer, Keylet const& keylet) {
|
||||
env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}),
|
||||
Ter(expectedTer));
|
||||
};
|
||||
|
||||
// Blocking Vault with the amendment disabled fails
|
||||
{
|
||||
testcase(prefix + "block/unblock fails when amendment is disabled");
|
||||
|
||||
env.disableFeature(featureLendingProtocolV1_1);
|
||||
auto const [tx, keylet] = vault.create(
|
||||
{.owner = owner, .asset = asset, .flags = tfVaultOwnerCanBlockDeposit});
|
||||
env(tx, Ter(temINVALID_FLAG));
|
||||
env.close();
|
||||
|
||||
blockVault(temINVALID_FLAG, keylet);
|
||||
unblockVault(temINVALID_FLAG, keylet);
|
||||
|
||||
env.enableFeature(featureLendingProtocolV1_1);
|
||||
}
|
||||
|
||||
// Block Vault deposits fails if the vault is not configured to allow blocking deposits
|
||||
{
|
||||
testcase(prefix + "block/unblock fails when vault is not configured");
|
||||
auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
blockVault(tecNO_PERMISSION, keylet);
|
||||
unblockVault(tecNO_PERMISSION, keylet);
|
||||
|
||||
env(vault.del({.owner = owner, .id = keylet.key}), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto const [tx, keylet] =
|
||||
vault.create({.owner = owner, .asset = asset, .flags = tfVaultOwnerCanBlockDeposit});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
{
|
||||
testcase(prefix + "block/unblock succeeds");
|
||||
// deposit assets to show that blocking deposit does not block withdrawals
|
||||
env(vault.deposit({
|
||||
.depositor = owner,
|
||||
.id = keylet.key,
|
||||
.amount = XRP(10'000),
|
||||
}),
|
||||
Ter(tesSUCCESS));
|
||||
env(vault.deposit({
|
||||
.depositor = other,
|
||||
.id = keylet.key,
|
||||
.amount = XRP(10'000),
|
||||
}),
|
||||
Ter(tesSUCCESS));
|
||||
|
||||
blockVault(tesSUCCESS, keylet);
|
||||
|
||||
// Owner is blocked from depositing to the vault
|
||||
env(vault.deposit({
|
||||
.depositor = owner,
|
||||
.id = keylet.key,
|
||||
.amount = XRP(10'000),
|
||||
}),
|
||||
Ter(tecNO_PERMISSION));
|
||||
|
||||
// Other accounts are also blocked from depositing to the vault
|
||||
env(vault.deposit({
|
||||
.depositor = other,
|
||||
.id = keylet.key,
|
||||
.amount = XRP(10'000),
|
||||
}),
|
||||
Ter(tecNO_PERMISSION));
|
||||
|
||||
// Block vault withdrawal works as normal
|
||||
env(vault.withdraw({
|
||||
.depositor = owner,
|
||||
.id = keylet.key,
|
||||
.amount = XRP(10'000),
|
||||
}),
|
||||
Ter(tesSUCCESS));
|
||||
|
||||
env(vault.withdraw({
|
||||
.depositor = other,
|
||||
.id = keylet.key,
|
||||
.amount = XRP(10'000),
|
||||
}),
|
||||
Ter(tesSUCCESS));
|
||||
|
||||
unblockVault(tesSUCCESS, keylet);
|
||||
|
||||
env(vault.deposit({
|
||||
.depositor = owner,
|
||||
.id = keylet.key,
|
||||
.amount = XRP(10'000),
|
||||
}),
|
||||
Ter(tesSUCCESS));
|
||||
|
||||
env(vault.deposit({
|
||||
.depositor = other,
|
||||
.id = keylet.key,
|
||||
.amount = XRP(10'000),
|
||||
}),
|
||||
Ter(tesSUCCESS));
|
||||
|
||||
// Withdraw to keep the vault empty
|
||||
env(vault.withdraw({
|
||||
.depositor = owner,
|
||||
.id = keylet.key,
|
||||
.amount = XRP(10'000),
|
||||
}),
|
||||
Ter(tesSUCCESS));
|
||||
|
||||
env(vault.withdraw({
|
||||
.depositor = other,
|
||||
.id = keylet.key,
|
||||
.amount = XRP(10'000),
|
||||
}),
|
||||
Ter(tesSUCCESS));
|
||||
}
|
||||
|
||||
{
|
||||
testcase(prefix + "block/unblock fails when caller is not owner");
|
||||
|
||||
env(vault.set({.owner = other, .id = keylet.key, .flags = tfVaultDepositBlock}),
|
||||
Ter(tecNO_PERMISSION));
|
||||
|
||||
blockVault(tesSUCCESS, keylet);
|
||||
|
||||
env(vault.set({.owner = other, .id = keylet.key, .flags = tfVaultDepositUnblock}),
|
||||
Ter(tecNO_PERMISSION));
|
||||
|
||||
unblockVault(tesSUCCESS, keylet);
|
||||
}
|
||||
|
||||
{
|
||||
testcase(prefix + "unblock fails when vault is already unblocked");
|
||||
unblockVault(tecNO_PERMISSION, keylet);
|
||||
}
|
||||
|
||||
{
|
||||
testcase(prefix + "block fails when vault is already blocked");
|
||||
blockVault(tesSUCCESS, keylet);
|
||||
blockVault(tecNO_PERMISSION, keylet);
|
||||
unblockVault(tesSUCCESS, keylet);
|
||||
}
|
||||
|
||||
env(vault.del({.owner = owner, .id = keylet.key}));
|
||||
}
|
||||
|
||||
void
|
||||
testVaultDeleteData()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
Env env{*this};
|
||||
Account const owner{"owner"};
|
||||
env.fund(XRP(1'000'000), owner);
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
|
||||
auto const keylet = keylet::vault(owner.id(), 1);
|
||||
auto delTx = vault.del({.owner = owner, .id = keylet.key});
|
||||
|
||||
// Test VaultDelete with featureLendingProtocolV1_1 disabled
|
||||
// Transaction fails if the data field is provided
|
||||
{
|
||||
testcase("VaultDelete data featureLendingProtocolV1_1 disabled");
|
||||
env.disableFeature(featureLendingProtocolV1_1);
|
||||
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
|
||||
env(delTx, Ter(temDISABLED));
|
||||
env.close();
|
||||
env.enableFeature(featureLendingProtocolV1_1);
|
||||
}
|
||||
|
||||
// Transaction fails if the data field is too large
|
||||
{
|
||||
testcase("VaultDelete data featureLendingProtocolV1_1 enabled data too large");
|
||||
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength + 1, 'A'));
|
||||
env(delTx, Ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Transaction fails if the data field is set, but is empty
|
||||
{
|
||||
testcase("VaultDelete data featureLendingProtocolV1_1 enabled data empty");
|
||||
delTx[sfMemoData] = strHex(std::string(0, 'A'));
|
||||
env(delTx, Ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
testcase("VaultDelete data featureLendingProtocolV1_1 enabled data valid");
|
||||
PrettyAsset const xrpAsset = xrpIssue();
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
|
||||
env(tx, Ter(tesSUCCESS));
|
||||
env.close();
|
||||
// Recreate the transaction as the vault keylet changed
|
||||
auto delTx = vault.del({.owner = owner, .id = keylet.key});
|
||||
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
|
||||
env(delTx, Ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -8045,7 +8604,7 @@ public:
|
||||
testCreateFailMPT();
|
||||
testWithMPT();
|
||||
testWithIOU();
|
||||
testWithDomainCheck();
|
||||
testPrivateVault();
|
||||
testWithDomainChecXRP();
|
||||
testNonTransferableShares();
|
||||
testFailedPseudoAccount();
|
||||
@@ -8055,6 +8614,8 @@ public:
|
||||
testVaultClawbackAssets();
|
||||
testVaultEscrowedMPT();
|
||||
testAssetsMaximum();
|
||||
testVaultDepositBlockGeneral();
|
||||
testVaultDeleteData();
|
||||
testBug6LimitBypassWithShares();
|
||||
testRemoveEmptyHoldingLockedAmount();
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ Vault::set(SetArgs const& args)
|
||||
jv[jss::TransactionType] = jss::VaultSet;
|
||||
jv[jss::Account] = args.owner.human();
|
||||
jv[sfVaultID] = to_string(args.id);
|
||||
if (args.flags)
|
||||
jv[jss::Flags] = *args.flags;
|
||||
return jv;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ struct Vault
|
||||
{
|
||||
Account owner;
|
||||
uint256 id;
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
};
|
||||
|
||||
static json::Value
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user