diff --git a/include/xrpl/ledger/helpers/VaultHelpers.h b/include/xrpl/ledger/helpers/VaultHelpers.h index 116064a1a3..d064a1ed16 100644 --- a/include/xrpl/ledger/helpers/VaultHelpers.h +++ b/include/xrpl/ledger/helpers/VaultHelpers.h @@ -171,6 +171,18 @@ 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); + /** * Resolves a Vault's LEVersion, the single point every accounting touch * point should call to determine which recognition model (accrual vs. diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 72cd60ab2c..3047e6d62d 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -199,7 +199,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) \ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index f7a6e3ce2d..bc4b28cebe 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -195,7 +195,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(VaultDeposit, \ @@ -243,7 +249,6 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TF_FLAG(tfSponsorshipCreate, 0x00020000) \ TF_FLAG(tfSponsorshipReassign, 0x00040000), \ MASK_ADJ(0)) - // clang-format on // Create all the flag values. diff --git a/include/xrpl/tx/transactors/vault/VaultSet.h b/include/xrpl/tx/transactors/vault/VaultSet.h index 835ed69610..0d7664eb3b 100644 --- a/include/xrpl/tx/transactors/vault/VaultSet.h +++ b/include/xrpl/tx/transactors/vault/VaultSet.h @@ -20,6 +20,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static bool checkExtraFeatures(PreflightContext const& ctx); diff --git a/merged-prs.md b/merged-prs.md index 06081d9e27..b6c2eedafe 100644 --- a/merged-prs.md +++ b/merged-prs.md @@ -2,7 +2,9 @@ PRs merged into the `ripple/lending-protocol-fv` branch. -| PR | Title | Author | Branch | Merged | -| --------------------------------------------------- | ---------------------------------- | ---------- | --------------------------------------------------------- | ---------- | -| [#6383](https://github.com/XRPLF/rippled/pull/6383) | feat: Add tfVaultDonate feature | @Tapanito | `tapanito/vault-donation` | 2026-09-02 | -| [#7820](https://github.com/XRPLF/rippled/pull/7820) | feat: Split LoanSet and LoanAccept | @a1q123456 | `a1q123456/split-loan-set-and-loan-accept-implementation` | 2026-09-02 | +| PR | Title | Author | Branch | Merged | +| --------------------------------------------------- | ----------------------------------------------- | ---------- | --------------------------------------------------------- | ---------- | +| [#6383](https://github.com/XRPLF/rippled/pull/6383) | feat: Add tfVaultDonate feature | @Tapanito | `tapanito/vault-donation` | 2026-09-02 | +| [#7820](https://github.com/XRPLF/rippled/pull/7820) | feat: Split LoanSet and LoanAccept | @a1q123456 | `a1q123456/split-loan-set-and-loan-accept-implementation` | 2026-09-02 | +| [#6528](https://github.com/XRPLF/rippled/pull/6528) | feat: Make VaultID conditional on LoanBrokerSet | @Tapanito | `tapanito/loan-broker-set` | 2026-09-02 | +| [#6361](https://github.com/XRPLF/rippled/pull/6361) | Adds functionality to block vault deposits | @Tapanito | `tapanito/vault-block-deposit` | 2026-09-02 | diff --git a/src/libxrpl/ledger/helpers/VaultHelpers.cpp b/src/libxrpl/ledger/helpers/VaultHelpers.cpp index add42f0cbc..3b6cc9d25f 100644 --- a/src/libxrpl/ledger/helpers/VaultHelpers.cpp +++ b/src/libxrpl/ledger/helpers/VaultHelpers.cpp @@ -225,6 +225,20 @@ 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; +} + [[nodiscard]] VaultVersion getVaultVersion(SLE::const_ref vault) { diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index 7ade4ed5ab..8d690c0e3d 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -55,7 +55,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 @@ -212,6 +215,7 @@ VaultCreate::doApply() std::uint32_t mptFlags = 0; if (!tx.isFlag(tfVaultShareNonTransferable)) mptFlags |= (lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer); + if (tx.isFlag(tfVaultPrivate)) mptFlags |= lsfMPTRequireAuth; @@ -249,7 +253,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.value(); vault->at(sfOwner) = accountID_; vault->at(sfAccount) = pseudoId; @@ -294,7 +304,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( applyViewContext, diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp index db99b83f98..8e0c94f899 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp @@ -152,8 +152,8 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } - auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID)); - if (!sleIssuance) + auto const sleShareIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID)); + if (!sleShareIssuance) { // LCOV_EXCL_START JLOG(ctx.j.error()) << "VaultDeposit: missing issuance of vault shares."; @@ -170,14 +170,14 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) } // Cannot donate to a vault with no shares - if (sleIssuance->at(sfOutstandingAmount) == 0) + if (sleShareIssuance->at(sfOutstandingAmount) == 0) { JLOG(ctx.j.debug()) << "VaultDeposit: empty vault cannot receive donations."; return tecNO_PERMISSION; } } - if (sleIssuance->isFlag(lsfMPTLocked)) + if (sleShareIssuance->isFlag(lsfMPTLocked)) { // LCOV_EXCL_START JLOG(ctx.j.error()) << "VaultDeposit: issuance of vault shares is locked."; @@ -185,6 +185,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; + } + } + if (fix330Enabled) { if (auto const ret = checkDepositFreeze(ctx.view, account, vaultAccount, vaultAsset)) @@ -205,7 +223,8 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) // credential is tolerated here because doApply deletes it. if (vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner)) { - if (auto const err = checkVaultDomain(ctx.view, sleIssuance, account, SuppressExpired::Yes); + if (auto const err = + checkVaultDomain(ctx.view, sleShareIssuance, account, SuppressExpired::Yes); !isTesSuccess(err)) return err; } diff --git a/src/libxrpl/tx/transactors/vault/VaultSet.cpp b/src/libxrpl/tx/transactors/vault/VaultSet.cpp index c71e84858a..d2b8d92f9d 100644 --- a/src/libxrpl/tx/transactors/vault/VaultSet.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultSet.cpp @@ -12,9 +12,12 @@ #include #include #include +#include #include #include +#include + 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. diff --git a/src/test/app/vault/VaultBlockDeposit_test.cpp b/src/test/app/vault/VaultBlockDeposit_test.cpp new file mode 100644 index 0000000000..929b575537 --- /dev/null +++ b/src/test/app/vault/VaultBlockDeposit_test.cpp @@ -0,0 +1,253 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +class VaultBlockDeposit_test : public VaultTestBase +{ + 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 + testPrivateVaultBlockDoesNotClearPrivate() + { + using namespace test::jtx; + + Env env{*this}; + Account const issuer{"issuer"}; + Account const owner{"owner"}; + env.fund(XRP(1000), issuer, owner); + env.close(); + + PrettyAsset const asset = issuer["IOU"]; + env.trust(asset(1000), owner); + env(pay(issuer, owner, asset(500))); + env.close(); + + Vault const vault{env}; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .flags = tfVaultPrivate | tfVaultOwnerCanBlockDeposit}); + env(tx); + env.close(); + + { + testcase("blocking a private vault does not change lsfVaultPrivate flag"); + auto setTx = + vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}); + env(setTx, 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 setTx = + vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}); + env(setTx, Ter(tesSUCCESS)); + auto const sleVault = env.le(keylet); + if (!BEAST_EXPECT(sleVault)) + return; + BEAST_EXPECT(!sleVault->isFlag(lsfVaultDepositBlocked)); + BEAST_EXPECT(sleVault->isFlag(lsfVaultPrivate)); + } + } + +public: + void + run() override + { + testVaultDepositBlockGeneral(); + testPrivateVaultBlockDoesNotClearPrivate(); + } +}; + +BEAST_DEFINE_TESTSUITE(VaultBlockDeposit, app, xrpl); + +} // namespace xrpl diff --git a/src/test/app/vault/VaultValidation_test.cpp b/src/test/app/vault/VaultValidation_test.cpp index 005126b3ad..96dea3d1c0 100644 --- a/src/test/app/vault/VaultValidation_test.cpp +++ b/src/test/app/vault/VaultValidation_test.cpp @@ -169,6 +169,14 @@ private: 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; diff --git a/src/test/jtx/impl/vault.cpp b/src/test/jtx/impl/vault.cpp index 1513369554..e7bb02a921 100644 --- a/src/test/jtx/impl/vault.cpp +++ b/src/test/jtx/impl/vault.cpp @@ -72,6 +72,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; } diff --git a/src/test/jtx/vault.h b/src/test/jtx/vault.h index 80bf10449d..2a8749fd85 100644 --- a/src/test/jtx/vault.h +++ b/src/test/jtx/vault.h @@ -80,6 +80,7 @@ struct Vault { Account owner; uint256 id; + std::optional flags = std::nullopt; }; static json::Value