Start vault implementation

This commit is contained in:
John Freeman
2024-10-17 10:09:17 -05:00
committed by Bronek Kozicki
parent 1a032f04e3
commit ff8c6491d7
65 changed files with 2128 additions and 231 deletions

View File

@@ -364,6 +364,8 @@ public:
*/
Value&
operator[](const StaticString& key);
Value const&
operator[](const StaticString& key) const;
/// Return the member named key if it exist, defaultValue otherwise.
Value

View File

@@ -48,14 +48,6 @@ class STObject;
class STAmount;
class Rules;
/** Calculate AMM account ID.
*/
AccountID
ammAccountID(
std::uint16_t prefix,
uint256 const& parentHash,
uint256 const& ammID);
/** Calculate Liquidity Provider Token (LPT) Currency.
*/
Currency

View File

@@ -114,6 +114,14 @@ public:
equalTokens(Asset const& lhs, Asset const& rhs);
};
inline Json::Value
to_json(Asset const& asset)
{
Json::Value jv;
asset.setJson(jv);
return jv;
}
template <ValidIssueType TIss>
constexpr bool
Asset::holds() const
@@ -219,9 +227,6 @@ validJSONAsset(Json::Value const& jv);
Asset
assetFromJson(Json::Value const& jv);
Json::Value
to_json(Asset const& asset);
} // namespace ripple
#endif // RIPPLE_PROTOCOL_ASSET_H_INCLUDED

View File

@@ -80,7 +80,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 83;
static constexpr std::size_t numFeatures = 84;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated

View File

@@ -330,6 +330,15 @@ mptoken(uint256 const& mptokenKey)
Keylet
mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept;
Keylet
vault(AccountID const& owner, std::uint32_t seq) noexcept;
inline Keylet
vault(uint256 const& vaultKey)
{
return {ltVAULT, vaultKey};
}
} // namespace keylet
// Everything below is deprecated and should be removed in favor of keylets:

View File

@@ -189,6 +189,9 @@ enum LedgerSpecificFlags {
// ltCREDENTIAL
lsfAccepted = 0x00010000,
// ltVAULT
lsfVaultPrivate = 0x00010000,
};
//------------------------------------------------------------------------------

View File

@@ -42,8 +42,11 @@ public:
AccountID const&
getIssuer() const;
MPTID const&
getMptID() const;
constexpr MPTID const&
getMptID() const
{
return mptID_;
}
std::string
getText() const;

View File

@@ -111,6 +111,9 @@ std::size_t constexpr maxMPTokenMetadataLength = 1024;
/** The maximum amount of MPTokenIssuance */
std::uint64_t constexpr maxMPTokenAmount = 0x7FFF'FFFF'FFFF'FFFFull;
/** The maximum length of MPTokenMetadata */
std::size_t constexpr maxVaultDataLength = 256;
/** A ledger index. */
using LedgerIndex = std::uint32_t;

View File

@@ -268,7 +268,7 @@ public:
std::string
getText() const override;
Json::Value getJson(JsonOptions) const override;
Json::Value getJson(JsonOptions = JsonOptions::none) const override;
void
add(Serializer& s) const override;

View File

@@ -90,6 +90,14 @@ struct JsonOptions
}
};
template <typename T>
requires requires(T const& t) { t.getJson(JsonOptions::none); }
Json::Value
to_json(T const& t)
{
return t.getJson(JsonOptions::none);
}
namespace detail {
class STVar;
}
@@ -155,7 +163,7 @@ public:
virtual std::string
getText() const;
virtual Json::Value getJson(JsonOptions /*options*/) const;
virtual Json::Value getJson(JsonOptions = JsonOptions::none) const;
virtual void
add(Serializer& s) const;

View File

@@ -45,6 +45,15 @@ public:
explicit STIssue(SField const& name);
STIssue&
operator=(STIssue const& rhs) = default;
STIssue&
operator=(Asset const& rhs)
{
asset_ = rhs;
return *this;
}
template <ValidIssueType TIss>
TIss const&
get() const;

View File

@@ -63,6 +63,13 @@ public:
void
setValue(Number const& v);
STNumber&
operator=(Number const& rhs)
{
setValue(rhs);
return *this;
}
bool
isEquivalent(STBase const& t) const override;
bool

View File

@@ -152,8 +152,7 @@ public:
getText() const override;
// TODO(tom): options should be an enum.
Json::Value
getJson(JsonOptions options) const override;
Json::Value getJson(JsonOptions = JsonOptions::none) const override;
void
addWithoutSigningFields(Serializer& s) const;
@@ -482,9 +481,19 @@ private:
template <class T>
class STObject::Proxy
{
protected:
public:
using value_type = typename T::value_type;
value_type
value() const;
value_type
operator*() const;
T const*
operator->() const;
protected:
STObject* st_;
SOEStyle style_;
TypedField<T> const* f_;
@@ -493,9 +502,6 @@ protected:
Proxy(STObject* st, TypedField<T> const* f);
value_type
value() const;
T const*
find() const;
@@ -510,7 +516,7 @@ template <typename U>
concept IsArithmetic = std::is_arithmetic_v<U> || std::is_same_v<U, STAmount>;
template <class T>
class STObject::ValueProxy : private Proxy<T>
class STObject::ValueProxy : public Proxy<T>
{
private:
using value_type = typename T::value_type;
@@ -536,6 +542,20 @@ public:
operator value_type() const;
template <typename U>
friend bool
operator==(U const& lhs, STObject::ValueProxy<T> const& rhs)
{
return rhs.value() == lhs;
}
template <typename U>
friend bool
operator!=(U const& lhs, STObject::ValueProxy<T> const& rhs)
{
return !(lhs == rhs);
}
private:
friend class STObject;
@@ -543,7 +563,7 @@ private:
};
template <class T>
class STObject::OptionalProxy : private Proxy<T>
class STObject::OptionalProxy : public Proxy<T>
{
private:
using value_type = typename T::value_type;
@@ -563,15 +583,6 @@ public:
explicit
operator bool() const noexcept;
/** Return the contained value
Throws:
STObject::FieldErr if !engaged()
*/
value_type
operator*() const;
operator optional_type() const;
/** Explicit conversion to std::optional */
@@ -715,6 +726,20 @@ STObject::Proxy<T>::value() const -> value_type
return value_type{};
}
template <class T>
auto
STObject::Proxy<T>::operator*() const -> value_type
{
return this->value();
}
template <class T>
T const*
STObject::Proxy<T>::operator->() const
{
return this->find();
}
template <class T>
inline T const*
STObject::Proxy<T>::find() const
@@ -790,13 +815,6 @@ STObject::OptionalProxy<T>::operator bool() const noexcept
return engaged();
}
template <class T>
auto
STObject::OptionalProxy<T>::operator*() const -> value_type
{
return this->value();
}
template <class T>
STObject::OptionalProxy<T>::operator typename STObject::OptionalProxy<
T>::optional_type() const

View File

@@ -101,6 +101,10 @@ public:
SeqProxy
getSeqProxy() const;
/** Returns the first non-zero value of (Sequence, TicketSequence). */
std::uint32_t
getSequence() const;
boost::container::flat_set<AccountID>
getMentionedAccounts() const;

View File

@@ -141,6 +141,7 @@ enum TEMcodes : TERUnderlyingType {
temARRAY_TOO_LARGE,
temBAD_TRANSFER_FEE,
temSTRING_TOO_LARGE,
};
//------------------------------------------------------------------------------
@@ -344,6 +345,7 @@ enum TECcodes : TERUnderlyingType {
tecARRAY_TOO_LARGE = 191,
tecLOCKED = 192,
tecBAD_CREDENTIALS = 193,
tecWRONG_ASSET = 194,
};
//------------------------------------------------------------------------------

View File

@@ -214,6 +214,12 @@ constexpr std::uint32_t tfAMMClawbackMask = ~(tfUniversal | tfClawTwoAssets);
// BridgeModify flags:
constexpr std::uint32_t tfClearAccountCreateAmount = 0x00010000;
constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreateAmount);
// VaultCreate flags:
constexpr std::uint32_t const tfVaultPrivate = 0x00010000;
static_assert(tfVaultPrivate == lsfVaultPrivate);
constexpr std::uint32_t const tfVaultShareNonTransferable = 0x00020000;
constexpr std::uint32_t const tfVaultCreateMask = ~(tfUniversal | tfVaultPrivate | tfVaultShareNonTransferable);
// clang-format on
} // namespace ripple

View File

@@ -29,6 +29,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(SingleAssetVault, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Credentials, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(AMMClawback, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (AMMv1_2, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -379,6 +379,37 @@ LEDGER_ENTRY(ltAMM, 0x0079, AMM, ({
{sfPreviousTxnLgrSeq, soeOPTIONAL},
}))
/** A ledger object representing an individual MPToken asset type, but not
* any balances of that asset itself.
\sa keylet::mptIssuance
*/
LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, ({
{sfIssuer, soeREQUIRED},
{sfSequence, soeREQUIRED},
{sfTransferFee, soeDEFAULT},
{sfOwnerNode, soeREQUIRED},
{sfAssetScale, soeDEFAULT},
{sfMaximumAmount, soeOPTIONAL},
{sfOutstandingAmount, soeREQUIRED},
{sfMPTokenMetadata, soeOPTIONAL},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
/** A ledger object representing an individual MPToken balance.
\sa keylet::mptoken
*/
LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, ({
{sfAccount, soeREQUIRED},
{sfMPTokenIssuanceID, soeREQUIRED},
{sfMPTAmount, soeDEFAULT},
{sfOwnerNode, soeREQUIRED},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
/** A ledger object which tracks Oracle
\sa keylet::oracle
*/
@@ -394,34 +425,6 @@ LEDGER_ENTRY(ltORACLE, 0x0080, Oracle, ({
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
/** A ledger object which tracks MPTokenIssuance
\sa keylet::mptIssuance
*/
LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, ({
{sfIssuer, soeREQUIRED},
{sfSequence, soeREQUIRED},
{sfTransferFee, soeDEFAULT},
{sfOwnerNode, soeREQUIRED},
{sfAssetScale, soeDEFAULT},
{sfMaximumAmount, soeOPTIONAL},
{sfOutstandingAmount, soeREQUIRED},
{sfMPTokenMetadata, soeOPTIONAL},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
/** A ledger object which tracks MPToken
\sa keylet::mptoken
*/
LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, ({
{sfAccount, soeREQUIRED},
{sfMPTokenIssuanceID, soeREQUIRED},
{sfMPTAmount, soeDEFAULT},
{sfOwnerNode, soeREQUIRED},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
/** A ledger object which tracks Credential
\sa keylet::credential
*/
@@ -436,3 +439,26 @@ LEDGER_ENTRY(ltCREDENTIAL, 0x0081, Credential, ({
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
/** A ledger object representing a single asset vault.
\sa keylet::mptoken
*/
LEDGER_ENTRY(ltVAULT, 0x0082, Vault, ({
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfSequence, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
{sfOwner, soeREQUIRED},
{sfAccount, soeREQUIRED},
{sfData, soeDEFAULT},
{sfAsset, soeREQUIRED},
{sfAssetTotal, soeDEFAULT},
{sfAssetAvailable, soeDEFAULT},
{sfAssetMaximum, soeDEFAULT},
{sfLossUnrealized, soeDEFAULT},
{sfMPTokenIssuanceID, soeREQUIRED}, // sfShare
// no ShareTotal ever (use MPTIssuance.sfOutstandingAmount)
// no WithdrawalPolicy yet
// no PermissionedDomainID yet
}))

View File

@@ -190,9 +190,14 @@ TYPED_SFIELD(sfHookStateKey, UINT256, 30)
TYPED_SFIELD(sfHookHash, UINT256, 31)
TYPED_SFIELD(sfHookNamespace, UINT256, 32)
TYPED_SFIELD(sfHookSetTxnID, UINT256, 33)
TYPED_SFIELD(sfVaultID, UINT256, 34)
// number (common)
TYPED_SFIELD(sfNumber, NUMBER, 1)
TYPED_SFIELD(sfAssetAvailable, NUMBER, 2)
TYPED_SFIELD(sfAssetMaximum, NUMBER, 3)
TYPED_SFIELD(sfAssetTotal, NUMBER, 4)
TYPED_SFIELD(sfLossUnrealized, NUMBER, 5)
// currency amount (common)
TYPED_SFIELD(sfAmount, AMOUNT, 1)

View File

@@ -447,6 +447,49 @@ TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, ({
{sfCredentialType, soeREQUIRED},
}))
/** This transaction creates a single asset vault. */
TRANSACTION(ttVAULT_CREATE, 61, VaultCreate, ({
{sfAsset, soeREQUIRED},
{sfAssetMaximum, soeOPTIONAL},
{sfMPTokenMetadata, soeOPTIONAL},
// no PermissionedDomainID yet
// no WithdrawalPolicy yet
{sfData, soeOPTIONAL},
}))
/** This transaction updates a single asset vault. */
TRANSACTION(ttVAULT_SET, 62, VaultSet, ({
{sfVaultID, soeREQUIRED},
{sfAssetMaximum, soeOPTIONAL},
// no PermissionedDomainID yet
// no WithdrawalPolicy yet
{sfData, soeOPTIONAL},
}))
/** This transaction deletes a single asset vault. */
TRANSACTION(ttVAULT_DELETE, 63, VaultDelete, ({
{sfVaultID, soeREQUIRED},
}))
/** This transaction trades assets for shares with a vault. */
TRANSACTION(ttVAULT_DEPOSIT, 64, VaultDeposit, ({
{sfVaultID, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
}))
/** This transaction trades shares for assets with a vault. */
TRANSACTION(ttVAULT_WITHDRAW, 65, VaultWithdraw, ({
{sfVaultID, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
{sfDestination, soeOPTIONAL},
}))
/** This transaction trades shares for assets with a vault. */
TRANSACTION(ttVAULT_CLAWBACK, 66, VaultClawback, ({
{sfVaultID, soeREQUIRED},
{sfHolder, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
}))
/** This system-generated transaction type is used to update the status of the various amendments.

View File

@@ -113,14 +113,17 @@ JSS(Subject); // in: Credential transactions
JSS(TakerGets); // field.
JSS(TakerPays); // field.
JSS(Ticket); // ledger type.
JSS(TxnSignature); // field.
JSS(TradingFee); // in/out: AMM trading fee
JSS(TransactionType); // in: TransactionSign.
JSS(TransferRate); // in: TransferRate.
JSS(TxnSignature); // field.
JSS(URI); // field.
JSS(Vault); // ledger type.
JSS(VaultID); // field.
JSS(VoteSlots); // out: AMM Vote
JSS(XChainOwnedClaimID); // ledger type.
JSS(XChainOwnedCreateAccountClaimID); // ledger type.
//
JSS(aborted); // out: InboundLedger
JSS(accepted); // out: LedgerToJson, OwnerInfo, SubmitTransaction
JSS(account); // in/out: many
@@ -749,6 +752,8 @@ JSS(NegativeUNL); // out: ValidatorList; ledger type
#undef JSS
// clang-format on
} // namespace jss
} // namespace ripple

View File

@@ -887,6 +887,12 @@ Value::operator[](const StaticString& key)
return resolveReference(key, true);
}
Value const&
Value::operator[](const StaticString& key) const
{
return (*this)[key.c_str()];
}
Value&
Value::append(const Value& value)
{

View File

@@ -26,18 +26,6 @@
namespace ripple {
AccountID
ammAccountID(
std::uint16_t prefix,
uint256 const& parentHash,
uint256 const& ammID)
{
ripesha_hasher rsh;
auto const hash = sha512Half(prefix, parentHash, ammID);
rsh(hash.data(), hash.size());
return AccountID{static_cast<ripesha_hasher::result_type>(rsh)};
}
Currency
ammLPTCurrency(Currency const& cur1, Currency const& cur2)
{

View File

@@ -70,11 +70,4 @@ assetFromJson(Json::Value const& v)
return mptIssueFromJson(v);
}
Json::Value
to_json(Asset const& asset)
{
return std::visit(
[&](auto const& issue) { return to_json(issue); }, asset.value());
}
} // namespace ripple

View File

@@ -78,6 +78,7 @@ enum class LedgerNameSpace : std::uint16_t {
MPTOKEN_ISSUANCE = '~',
MPTOKEN = 't',
CREDENTIAL = 'D',
VAULT = 'V',
// No longer used or supported. Left here to reserve the space
// to avoid accidental reuse.
@@ -527,6 +528,12 @@ credential(
indexHash(LedgerNameSpace::CREDENTIAL, subject, issuer, credType)};
}
Keylet
vault(AccountID const& owner, std::uint32_t seq) noexcept
{
return vault(indexHash(LedgerNameSpace::VAULT, owner, seq));
}
} // namespace keylet
} // namespace ripple

View File

@@ -35,7 +35,7 @@ Keylet::check(STLedgerEntry const& sle) const
if (type == ltCHILD)
return sle.getType() != ltDIR_NODE;
return sle.getType() == type;
return sle.getType() == type && sle.key() == key;
}
} // namespace ripple

View File

@@ -39,12 +39,6 @@ MPTIssue::getIssuer() const
return *account;
}
MPTID const&
MPTIssue::getMptID() const
{
return mptID_;
}
std::string
MPTIssue::getText() const
{

View File

@@ -197,6 +197,12 @@ STTx::getSeqProxy() const
return SeqProxy{SeqProxy::ticket, *ticketSeq};
}
std::uint32_t
STTx::getSequence() const
{
return getSeqProxy().value();
}
void
STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
{

View File

@@ -117,6 +117,7 @@ transResults()
MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."),
MAKE_ERROR(tecLOCKED, "Fund is locked."),
MAKE_ERROR(tecBAD_CREDENTIALS, "Bad credentials."),
MAKE_ERROR(tecWRONG_ASSET, "Wrong asset given."),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),

View File

@@ -3535,7 +3535,7 @@ private:
if (!BEAST_EXPECT(checkArraySize(affected, 4u)))
return;
auto ff =
affected[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
affected[2u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
BEAST_EXPECT(
ff[sfHighLimit.fieldName] ==
bob["USD"](100).value().getJson(JsonOptions::none));
@@ -3809,10 +3809,10 @@ private:
auto ff =
affected[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
BEAST_EXPECT(
ff[sfHighLimit.fieldName] ==
ff[sfLowLimit.fieldName] ==
G1["USD"](0).value().getJson(JsonOptions::none));
BEAST_EXPECT(!(ff[jss::Flags].asUInt() & lsfLowFreeze));
BEAST_EXPECT(ff[jss::Flags].asUInt() & lsfHighFreeze);
BEAST_EXPECT(ff[jss::Flags].asUInt() & lsfLowFreeze);
BEAST_EXPECT(!(ff[jss::Flags].asUInt() & lsfHighFreeze));
env.close();
// test: Can make a payment via the new offer

View File

@@ -22,6 +22,7 @@
#include <test/jtx/xchain_bridge.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/jss.h>
#include <format>
namespace ripple {
namespace test {

228
src/test/app/Vault_test.cpp Normal file
View File

@@ -0,0 +1,228 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx/Account.h>
#include <test/jtx/fee.h>
#include <test/jtx/mpt.h>
#include <test/jtx/utility.h>
#include <test/jtx/vault.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STNumber.h>
namespace ripple {
class Vault_test : public beast::unit_test::suite
{
TEST_CASE(CreateUpdateDelete)
{
using namespace test::jtx;
Env env{*this};
Account issuer{"issuer"};
Account owner{"owner"};
env.fund(XRP(1000), issuer, owner);
env.close();
auto vault = env.vault();
SUBCASE("IOU")
{
Asset asset = issuer["IOU"];
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
SUBCASE("nothing to delete")
{
tx = vault.del({.owner = issuer, .id = keylet.key});
env(tx, ter(tecOBJECT_NOT_FOUND));
env.close();
}
SUBCASE("create")
{
env(tx);
{
auto meta = env.meta()->getJson();
// JLOG(env.journal.error()) << meta;
auto n = 0;
for (auto const& affected : meta[sfAffectedNodes])
{
if (!affected[sfCreatedNode])
continue;
++n;
}
}
BEAST_EXPECT(env.le(keylet));
tx = vault.del({.owner = issuer, .id = keylet.key});
env(tx, ter(tecNO_PERMISSION));
env.close();
tx = vault.del({.owner = owner, .id = keylet.key});
env(tx);
{
auto n = 0;
auto meta = env.meta()->getJson();
for (auto const& affected : meta[sfAffectedNodes])
{
if (!affected[sfDeletedNode])
continue;
// JLOG(env.journal.error())
// << affected[sfDeletedNode][sfLedgerEntryType];
++n;
}
}
BEAST_EXPECT(!env.le(keylet));
// TODO: Assert all the entries created earlier are deleted.
}
// The vault owner is the transaction submitter.
// If that account is missing,
// then `preclaim` throws an exception.
SUBCASE("insufficient fee")
{
env(tx, fee(env.current()->fees().base), ter(telINSUF_FEE_P));
env.close();
}
SUBCASE("insufficient reserve")
{
// It is possible to construct a complicated mathematical
// expression for this amount, but it is sadly not easy.
env(pay(owner, issuer, XRP(775)));
env.close();
env(tx, ter(tecINSUFFICIENT_RESERVE));
env.close();
}
SUBCASE("global freeze")
{
env(fset(issuer, asfGlobalFreeze));
env.close();
env(tx, ter(tecFROZEN));
env.close();
}
SUBCASE("data too large")
{
tx[sfData] = blob257;
env(tx, ter(temSTRING_TOO_LARGE));
env.close();
}
SUBCASE("metadata too large")
{
// This metadata is for the share token.
tx[sfMPTokenMetadata] = blob1025;
env(tx, ter(temSTRING_TOO_LARGE));
env.close();
}
}
SUBCASE("MPT")
{
MPTTester mptt{env, issuer, {.fund = false}};
mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
Asset asset = mptt.issuanceID();
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
SUBCASE("create")
{
env(tx);
env.close();
SUBCASE("update")
{
auto tx = vault.set({.owner = owner, .id = keylet.key});
SUBCASE("happy path")
{
tx[sfData] = "ABCD";
tx[sfAssetMaximum] = 123;
env(tx);
env.close();
}
SUBCASE("not the owner")
{
tx[sfAccount] = issuer.human();
env(tx, ter(tecNO_PERMISSION));
env.close();
}
SUBCASE("data too large")
{
tx[sfData] = blob257;
env(tx, ter(temSTRING_TOO_LARGE));
env.close();
}
SUBCASE("shrinking assets")
{
// TODO: VaultSet (update) fail: AssetMaximum <
// AssetTotal
}
SUBCASE("immutable flags")
{
tx[sfFlags] = tfVaultPrivate;
env(tx, ter(temINVALID_FLAG));
env.close();
}
}
}
SUBCASE("global lock")
{
mptt.set({.account = issuer, .flags = tfMPTLock});
env(tx, ter(tecLOCKED));
}
}
SUBCASE("MPT cannot transfer")
{
MPTTester mptt{env, issuer, {.fund = false}};
mptt.create();
Asset asset = mptt.issuanceID();
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx, ter(tecLOCKED));
}
// TODO: VaultSet (update) succeed
// TODO: VaultSet (update) fail: wrong owner
// TODO: VaultSet (update) fail: Data too large
// TODO: VaultSet (update) fail: tfPrivate flag
// TODO: VaultSet (update) fail: tfShareNonTransferable flag
// TODO: Payment to VaultSet.PA fail
// TODO: VaultSet (update) fail: missing vault
BEAST_EXPECT(true);
}
public:
void
run() override
{
EXECUTE(CreateUpdateDelete);
}
};
BEAST_DEFINE_TESTSUITE_PRIO(Vault, tx, ripple, 1);
} // namespace ripple

View File

@@ -44,6 +44,11 @@ public:
IOUAmount const zz(beast::zero);
BEAST_EXPECT(z == zz);
// https://github.com/XRPLF/rippled/issues/5170
IOUAmount const zzz{};
BEAST_EXPECT(zzz == beast::zero);
// BEAST_EXPECT(zzz == zz);
}
void

View File

@@ -60,6 +60,7 @@
#include <test/jtx/sendmax.h>
#include <test/jtx/seq.h>
#include <test/jtx/sig.h>
#include <test/jtx/subcases.h>
#include <test/jtx/tag.h>
#include <test/jtx/tags.h>
#include <test/jtx/ter.h>

View File

@@ -28,6 +28,7 @@
#include <test/jtx/envconfig.h>
#include <test/jtx/require.h>
#include <test/jtx/tags.h>
#include <test/jtx/vault.h>
#include <test/unit_test/SuiteJournal.h>
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/ledger/OpenLedger.h>
@@ -452,6 +453,12 @@ public:
std::uint32_t
ownerCount(Account const& account) const;
Vault
vault()
{
return Vault{*this};
}
/** Return an account root.
@return empty if the account does not exist.
*/
@@ -785,6 +792,9 @@ Env::rpc(std::string const& cmd, Args&&... args)
std::forward<Args>(args)...);
}
extern std::string blob257;
extern std::string blob1025;
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -586,7 +586,10 @@ Env::disableFeature(uint256 const feature)
app().config().features.erase(feature);
}
} // namespace jtx
// The strings are hexadecimal. 1 byte = 2 hexadecimal characters.
std::string blob257(514, 'A');
std::string blob1025(2050, 'B');
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -0,0 +1,79 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx/subcases.h>
#include <stdexcept>
namespace subcases {
Subcase::Subcase(Context& context, char const* name)
: context_(context), name_(name)
{
}
Subcase::operator bool() const
{
auto& _ = context_;
++_.level;
if (_.level >= MAXIMUM_SUBCASE_DEPTH)
throw std::logic_error("maximum subcase depth exceeded");
if (_.entered < _.level && _.skip[_.level] == _.skipped)
{
_.entered = _.level;
_.names[_.level] = name_;
_.skipped = 0;
return true;
}
++_.skipped;
return false;
}
Subcase::~Subcase()
{
auto& _ = context_;
if (_.level == _.entered && _.skipped == 0)
{
// We are destroying the leaf subcase that executed on this pass.
// We call `suite::testcase()` here, after the subcase is finished,
// because only now do we know which subcase was the leaf,
// and we only want to print one name line for each subcase.
_.suite.testcase(_.name());
}
if (_.skipped == 0)
{
++_.skip[_.level];
_.skip[_.level + 1] = 0;
}
--_.level;
}
void
execute(beast::unit_test::suite* suite, char const* name, Supercase supercase)
{
Context context{*suite};
context.names[0] = name;
do
{
context.lap();
supercase(context);
} while (context.skipped != 0);
}
} // namespace subcases

View File

@@ -0,0 +1,69 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx/Env.h>
#include <test/jtx/vault.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/jss.h>
#include <optional>
namespace ripple {
namespace test {
namespace jtx {
std::tuple<Json::Value, Keylet>
Vault::create(CreateArgs const& args)
{
auto keylet = keylet::vault(args.owner.id(), env.seq(args.owner));
Json::Value jv;
jv[jss::TransactionType] = jss::VaultCreate;
jv[jss::Account] = args.owner.human();
jv[jss::Asset] = to_json(args.asset);
jv[jss::Fee] = STAmount(env.current()->fees().increment).getJson();
if (args.flags)
jv[jss::Flags] = *args.flags;
return {jv, keylet};
}
Json::Value
Vault::set(SetArgs const& args)
{
Json::Value jv;
jv[jss::TransactionType] = jss::VaultSet;
jv[jss::Account] = args.owner.human();
jv[jss::VaultID] = to_string(args.id);
return jv;
}
Json::Value
Vault::del(DeleteArgs const& args)
{
Json::Value jv;
jv[jss::TransactionType] = jss::VaultDelete;
jv[jss::Account] = args.owner.human();
jv[jss::VaultID] = to_string(args.id);
return jv;
}
} // namespace jtx
} // namespace test
} // namespace ripple

131
src/test/jtx/subcases.h Normal file
View File

@@ -0,0 +1,131 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TEST_JTX_SUBCASES_H_INCLUDED
#define RIPPLE_TEST_JTX_SUBCASES_H_INCLUDED
#include <xrpl/beast/unit_test/suite.h>
#include <cstdint>
#include <functional>
namespace subcases {
constexpr std::size_t MAXIMUM_SUBCASE_DEPTH = 10;
/**
* This short library implements a pattern found in doctest and Catch:
*
* TEST_CASE(testName) {
* // setup
* SUBCASE("one") {
* // actions and assertions
* }
* SUBCASE("two") {
* // actions and assertions
* }
* SUBCASE("three") {
* // actions and assertions
* }
* // assertions before teardown
* }
*
* EXECUTE(testName);
*
* In short:
*
* - Top-level test cases are declared with `TEST_CASE(name)`.
* The name must be a legal identifier.
* It will become the name of a function.
* - Subcases are declared with `SUBCASE("description")`.
* Descriptions do not need to be unique.
* - Test cases are executed with `EXECUTE(name)`,
* where `name` is the one that was passed to `TEST_CASE`.
* When executing a test case, it will loop,
* executing exactly one leaf subcase in each pass,
* until all subcases have executed.
* The top-level test case is considered a subcase too.
*
* This lets test authors easily share common setup among multiple subcases.
* Subcases can be nested up to `MAXIMUM_SUBCASE_DEPTH`.
*/
struct Context
{
beast::unit_test::suite& suite;
// The number of subcases to skip at each level to reach the next subcase.
std::uint8_t skip[MAXIMUM_SUBCASE_DEPTH] = {0};
// The subcase names at each level.
char const* names[MAXIMUM_SUBCASE_DEPTH] = {""};
// The current level.
std::uint8_t level = 0;
// The maximum depth at which we entered a subcase.
std::uint8_t entered = 0;
// The number of subcases we skipped on this or deeper levels
// since entering a subcase.
std::uint8_t skipped = 0;
std::string
name() const
{
std::string n;
for (auto i = 0; i <= level; ++i)
{
if (i != 0)
{
n += " > ";
}
n += names[i];
}
return n;
}
void
lap()
{
level = 0;
entered = 0;
skipped = 0;
}
};
struct Subcase
{
Context& context_;
char const* name_;
Subcase(Context& context, char const* name);
~Subcase();
/** Return true if we should enter this subcase. */
operator bool() const;
};
using Supercase = std::function<void(Context&)>;
void
execute(beast::unit_test::suite* suite, char const* name, Supercase supercase);
} // namespace subcases
#define TEST_CASE(name) void name(subcases::Context& _09876)
#define SUBCASE(name) if (subcases::Subcase _54321{_09876, name})
#define SKIP(name) if (false)
#define EXECUTE(name) \
subcases::execute(this, #name, [&](auto& ctx) { name(ctx); })
#endif

77
src/test/jtx/vault.h Normal file
View File

@@ -0,0 +1,77 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TEST_JTX_VAULT_H_INCLUDED
#define RIPPLE_TEST_JTX_VAULT_H_INCLUDED
#include <test/jtx/Account.h>
#include <xrpl/json/json_value.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Keylet.h>
#include <optional>
#include <tuple>
namespace ripple {
namespace test {
namespace jtx {
class Env;
struct Vault
{
Env& env;
struct CreateArgs
{
Account owner;
Asset asset;
std::optional<std::uint32_t> flags{};
};
/** Return a VaultCreate transaction and the Vault's expected keylet. */
std::tuple<Json::Value, Keylet>
create(CreateArgs const& args);
struct SetArgs
{
Account owner;
uint256 id;
};
Json::Value
set(SetArgs const& args);
struct DeleteArgs
{
Account owner;
uint256 id;
};
Json::Value
del(DeleteArgs const& args);
};
} // namespace jtx
} // namespace test
} // namespace ripple
#endif

View File

@@ -115,12 +115,14 @@ class Invariants_test : public beast::unit_test::suite
sink.messages().str().starts_with("Invariant failed:") ||
sink.messages().str().starts_with(
"Transaction caused an exception"));
// uncomment if you want to log the invariant failure message
// log << " --> " << sink.messages().str() << std::endl;
for (auto const& m : expect_logs)
{
BEAST_EXPECT(
sink.messages().str().find(m) != std::string::npos);
if (sink.messages().str().find(m) == std::string::npos)
{
// uncomment if you want to log the invariant failure
// message log << " --> " << m << std::endl;
fail();
}
}
}
}
@@ -606,7 +608,7 @@ class Invariants_test : public beast::unit_test::suite
testcase << "valid new account root";
doInvariantCheck(
{{"account root created by a non-Payment"}},
{{"account root created illegally"}},
[](Account const&, Account const&, ApplyContext& ac) {
// Insert a new account root created by a non-payment into
// the view.

View File

@@ -336,22 +336,22 @@ DirectIPaymentStep::quality(ReadView const& sb, QualityDirection qDir) const
if (!sle)
return QUALITY_ONE;
auto const& field = [this, qDir]() -> SF_UINT32 const& {
auto const& field = *[this, qDir]() {
if (qDir == QualityDirection::in)
{
// compute dst quality in
if (this->dst_ < this->src_)
return sfLowQualityIn;
return &sfLowQualityIn;
else
return sfHighQualityIn;
return &sfHighQualityIn;
}
else
{
// compute src quality out
if (this->src_ < this->dst_)
return sfLowQualityOut;
return &sfLowQualityOut;
else
return sfHighQualityOut;
return &sfHighQualityOut;
}
}();

View File

@@ -220,64 +220,39 @@ applyCreate(
auto const ammKeylet = keylet::amm(amount.issue(), amount2.issue());
// Mitigate same account exists possibility
auto const ammAccount = [&]() -> Expected<AccountID, TER> {
std::uint16_t constexpr maxAccountAttempts = 256;
for (auto p = 0; p < maxAccountAttempts; ++p)
{
auto const ammAccount =
ammAccountID(p, sb.info().parentHash, ammKeylet.key);
if (!sb.read(keylet::account(ammAccount)))
return ammAccount;
}
return Unexpected(tecDUPLICATE);
}();
auto const maybeAccount = createPseudoAccount(sb, ammKeylet.key);
// AMM account already exists (should not happen)
if (!ammAccount)
if (!maybeAccount)
{
JLOG(j_.error()) << "AMM Instance: AMM already exists.";
return {ammAccount.error(), false};
return {maybeAccount.error(), false};
}
auto& account = *maybeAccount;
auto const accountId = (*account)[sfAccount];
// LP Token already exists. (should not happen)
auto const lptIss = ammLPTIssue(
amount.issue().currency, amount2.issue().currency, *ammAccount);
if (sb.read(keylet::line(*ammAccount, lptIss)))
amount.issue().currency, amount2.issue().currency, accountId);
if (sb.read(keylet::line(accountId, lptIss)))
{
JLOG(j_.error()) << "AMM Instance: LP Token already exists.";
return {tecDUPLICATE, false};
}
// Create AMM Root Account.
auto sleAMMRoot = std::make_shared<SLE>(keylet::account(*ammAccount));
sleAMMRoot->setAccountID(sfAccount, *ammAccount);
sleAMMRoot->setFieldAmount(sfBalance, STAmount{});
std::uint32_t const seqno{
ctx_.view().rules().enabled(featureDeletableAccounts)
? ctx_.view().seq()
: 1};
sleAMMRoot->setFieldU32(sfSequence, seqno);
// Ignore reserves requirement, disable the master key, allow default
// rippling (AMM LPToken can be used in payments and offer crossing but
// not as a token in another AMM), and enable deposit authorization to
// prevent payments into AMM.
// Note, that the trustlines created by AMM have 0 credit limit.
// This prevents shifting the balance between accounts via AMM,
// or sending unsolicited LPTokens. This is a desired behavior.
// A user can only receive LPTokens through affirmative action -
// either an AMMDeposit, TrustSet, crossing an offer, etc.
sleAMMRoot->setFieldU32(
sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
// Link the root account and AMM object
sleAMMRoot->setFieldH256(sfAMMID, ammKeylet.key);
sb.insert(sleAMMRoot);
account->setFieldH256(sfAMMID, ammKeylet.key);
// Calculate initial LPT balance.
auto const lpTokens = ammLPTokens(amount, amount2, lptIss);
// Create ltAMM
auto ammSle = std::make_shared<SLE>(ammKeylet);
ammSle->setAccountID(sfAccount, *ammAccount);
ammSle->setAccountID(sfAccount, accountId);
ammSle->setFieldAmount(sfLPTokenBalance, lpTokens);
auto const& [issue1, issue2] = std::minmax(amount.issue(), amount2.issue());
ammSle->setFieldIssue(sfAsset, STIssue{sfAsset, issue1});
@@ -287,14 +262,7 @@ applyCreate(
ctx_.view(), ammSle, account_, lptIss, ctx_.tx[sfTradingFee]);
// Add owner directory to link the root account and AMM object.
if (auto const page = sb.dirInsert(
keylet::ownerDir(*ammAccount),
ammSle->key(),
describeOwnerDir(*ammAccount)))
{
ammSle->setFieldU64(sfOwnerNode, *page);
}
else
if (auto ter = dirLink(sb, accountId, ammSle); ter)
{
JLOG(j_.debug()) << "AMM Instance: failed to insert owner dir";
return {tecDIR_FULL, false};
@@ -302,7 +270,7 @@ applyCreate(
sb.insert(ammSle);
// Send LPT to LP.
auto res = accountSend(sb, *ammAccount, account_, lpTokens, ctx_.journal);
auto res = accountSend(sb, accountId, account_, lpTokens, ctx_.journal);
if (res != tesSUCCESS)
{
JLOG(j_.debug()) << "AMM Instance: failed to send LPT " << lpTokens;
@@ -313,7 +281,7 @@ applyCreate(
if (auto const res = accountSend(
sb,
account_,
*ammAccount,
accountId,
amount,
ctx_.journal,
WaiveTransferFee::Yes))
@@ -322,7 +290,7 @@ applyCreate(
if (!isXRP(amount))
{
if (SLE::pointer sleRippleState =
sb.peek(keylet::line(*ammAccount, amount.issue()));
sb.peek(keylet::line(accountId, amount.issue()));
!sleRippleState)
return tecINTERNAL;
else
@@ -351,7 +319,7 @@ applyCreate(
return {res, false};
}
JLOG(j_.debug()) << "AMM Instance: success " << *ammAccount << " "
JLOG(j_.debug()) << "AMM Instance: success " << accountId << " "
<< ammKeylet.key << " " << lpTokens << " " << amount << " "
<< amount2;
auto addOrderBook =

View File

@@ -327,7 +327,8 @@ AccountRootsNotDeleted::finalize(
// A successful AccountDelete or AMMDelete MUST delete exactly
// one account root.
if ((tx.getTxnType() == ttACCOUNT_DELETE ||
tx.getTxnType() == ttAMM_DELETE) &&
tx.getTxnType() == ttAMM_DELETE ||
tx.getTxnType() == ttVAULT_DELETE) &&
result == tesSUCCESS)
{
if (accountsDeleted_ == 1)
@@ -485,6 +486,7 @@ LedgerEntryTypesMatch::visitEntry(
case ltMPTOKEN_ISSUANCE:
case ltMPTOKEN:
case ltCREDENTIAL:
case ltVAULT:
break;
default:
invalidTypeAdded_ = true;
@@ -586,6 +588,7 @@ ValidNewAccountRoot::finalize(
// From this point on we know exactly one account was created.
if ((tx.getTxnType() == ttPAYMENT || tx.getTxnType() == ttAMM_CREATE ||
tx.getTxnType() == ttVAULT_CREATE ||
tx.getTxnType() == ttXCHAIN_ADD_CLAIM_ATTESTATION ||
tx.getTxnType() == ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION) &&
result == tesSUCCESS)
@@ -602,9 +605,7 @@ ValidNewAccountRoot::finalize(
return true;
}
JLOG(j.fatal()) << "Invariant failed: account root created "
"by a non-Payment, by an unsuccessful transaction, "
"or by AMM";
JLOG(j.fatal()) << "Invariant failed: account root created illegally";
return false;
}
@@ -992,28 +993,30 @@ ValidMPTIssuance::finalize(
{
if (result == tesSUCCESS)
{
if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_CREATE)
if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_CREATE ||
tx.getTxnType() == ttVAULT_CREATE)
{
if (mptIssuancesCreated_ == 0)
{
JLOG(j.fatal()) << "Invariant failed: MPT issuance creation "
JLOG(j.fatal()) << "Invariant failed: transaction "
"succeeded without creating a MPT issuance";
}
else if (mptIssuancesDeleted_ != 0)
{
JLOG(j.fatal()) << "Invariant failed: MPT issuance creation "
JLOG(j.fatal()) << "Invariant failed: transaction "
"succeeded while removing MPT issuances";
}
else if (mptIssuancesCreated_ > 1)
{
JLOG(j.fatal()) << "Invariant failed: MPT issuance creation "
JLOG(j.fatal()) << "Invariant failed: transaction "
"succeeded but created multiple issuances";
}
return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0;
}
if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_DESTROY)
if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_DESTROY ||
tx.getTxnType() == ttVAULT_DELETE)
{
if (mptIssuancesDeleted_ == 0)
{

View File

@@ -141,7 +141,7 @@ MPTokenAuthorize::authorize(
beast::Journal journal,
MPTAuthorizeArgs const& args)
{
auto const sleAcct = view.peek(keylet::account(args.account));
auto const sleAcct = view.peek(keylet::account(args.accountID));
if (!sleAcct)
return tecINTERNAL;
@@ -150,19 +150,22 @@ MPTokenAuthorize::authorize(
// `holderID` is NOT used
if (!args.holderID)
{
auto const mptokenKey =
keylet::mptoken(args.mptIssuanceID, args.accountID);
auto sleMpt = view.peek(mptokenKey);
// When a holder wants to unauthorize/delete a MPT, the ledger must
// - delete mptokenKey from owner directory
// - delete the MPToken
if (args.flags & tfMPTUnauthorize)
{
auto const mptokenKey =
keylet::mptoken(args.mptIssuanceID, args.account);
auto const sleMpt = view.peek(mptokenKey);
if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0)
return tecINTERNAL;
if (!sleMpt)
return tecOBJECT_NOT_FOUND;
if ((*sleMpt)[sfMPTAmount] != 0)
return tecHAS_OBLIGATIONS;
if (!view.dirRemove(
keylet::ownerDir(args.account),
keylet::ownerDir(args.accountID),
(*sleMpt)[sfOwnerNode],
sleMpt->key(),
false))
@@ -191,27 +194,19 @@ MPTokenAuthorize::authorize(
if (args.priorBalance < reserveCreate)
return tecINSUFFICIENT_RESERVE;
auto const mptokenKey =
keylet::mptoken(args.mptIssuanceID, args.account);
if (sleMpt)
return tecDUPLICATE;
sleMpt = std::make_shared<SLE>(mptokenKey);
auto const ownerNode = view.dirInsert(
keylet::ownerDir(args.account),
mptokenKey,
describeOwnerDir(args.account));
if (!ownerNode)
return tecDIR_FULL;
auto mptoken = std::make_shared<SLE>(mptokenKey);
(*mptoken)[sfAccount] = args.account;
(*mptoken)[sfMPTokenIssuanceID] = args.mptIssuanceID;
(*mptoken)[sfFlags] = 0;
(*mptoken)[sfOwnerNode] = *ownerNode;
view.insert(mptoken);
// Update owner count.
if (auto ter = dirLink(view, args.accountID, sleMpt))
return ter;
adjustOwnerCount(view, sleAcct, 1, journal);
(*sleMpt)[sfAccount] = args.accountID;
(*sleMpt)[sfMPTokenIssuanceID] = args.mptIssuanceID;
(*sleMpt)[sfFlags] = 0;
view.insert(sleMpt);
return tesSUCCESS;
}
@@ -223,7 +218,7 @@ MPTokenAuthorize::authorize(
// If the account that submitted this tx is the issuer of the MPT
// Note: `account_` is issuer's account
// `holderID` is holder's account
if (args.account != (*sleMptIssuance)[sfIssuer])
if (args.accountID != (*sleMptIssuance)[sfIssuer])
return tecINTERNAL;
auto const sleMpt =
@@ -259,7 +254,7 @@ MPTokenAuthorize::doApply()
ctx_.journal,
{.priorBalance = mPriorBalance,
.mptIssuanceID = tx[sfMPTokenIssuanceID],
.account = account_,
.accountID = account_,
.flags = tx.getFlags(),
.holderID = tx[~sfHolder]});
}

View File

@@ -26,11 +26,11 @@ namespace ripple {
struct MPTAuthorizeArgs
{
XRPAmount const& priorBalance;
XRPAmount const& priorBalance{};
uint192 const& mptIssuanceID;
AccountID const& account;
std::uint32_t flags;
std::optional<AccountID> holderID;
AccountID const& accountID;
std::uint32_t flags{};
std::optional<AccountID> holderID{};
};
class MPTokenAuthorize : public Transactor

View File

@@ -67,7 +67,7 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
return preflight2(ctx);
}
TER
Expected<MPTID, TER>
MPTokenIssuanceCreate::create(
ApplyView& view,
beast::Journal journal,
@@ -75,14 +75,10 @@ MPTokenIssuanceCreate::create(
{
auto const acct = view.peek(keylet::account(args.account));
if (!acct)
return tecINTERNAL;
return Unexpected(tecINTERNAL);
if (args.priorBalance <
view.fees().accountReserve((*acct)[sfOwnerCount] + 1))
return tecINSUFFICIENT_RESERVE;
auto const mptIssuanceKeylet =
keylet::mptIssuance(args.sequence, args.account);
auto mptId = makeMptID(args.sequence, args.account);
auto const mptIssuanceKeylet = keylet::mptIssuance(mptId);
// create the MPTokenIssuance
{
@@ -92,7 +88,7 @@ MPTokenIssuanceCreate::create(
describeOwnerDir(args.account));
if (!ownerNode)
return tecDIR_FULL;
return Unexpected(tecDIR_FULL);
auto mptIssuance = std::make_shared<SLE>(mptIssuanceKeylet);
(*mptIssuance)[sfFlags] = args.flags & ~tfUniversal;
@@ -119,24 +115,29 @@ MPTokenIssuanceCreate::create(
// Update owner count.
adjustOwnerCount(view, acct, 1, journal);
return tesSUCCESS;
return mptId;
}
TER
MPTokenIssuanceCreate::doApply()
{
auto const& tx = ctx_.tx;
return create(
ctx_.view(),
ctx_.journal,
{.priorBalance = mPriorBalance,
.account = account_,
auto const acct = view().peek(keylet::account(account_));
if (mPriorBalance < view().fees().accountReserve((*acct)[sfOwnerCount] + 1))
return tecINSUFFICIENT_RESERVE;
auto result = create(
view(),
j_,
{.account = account_,
.sequence = tx.getSeqProxy().value(),
.flags = tx.getFlags(),
.maxAmount = tx[~sfMaximumAmount],
.assetScale = tx[~sfAssetScale],
.transferFee = tx[~sfTransferFee],
.metadata = tx[~sfMPTokenMetadata]});
return result ? tesSUCCESS : result.error();
}
} // namespace ripple

View File

@@ -21,19 +21,20 @@
#define RIPPLE_TX_MPTOKENISSUANCECREATE_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
#include <xrpl/basics/Expected.h>
#include <xrpl/protocol/UintTypes.h>
namespace ripple {
struct MPTCreateArgs
{
XRPAmount const& priorBalance;
AccountID const& account;
std::uint32_t sequence;
std::uint32_t flags;
std::optional<std::uint64_t> maxAmount;
std::optional<std::uint8_t> assetScale;
std::optional<std::uint16_t> transferFee;
std::optional<Slice> const& metadata;
std::uint32_t flags = 0;
std::optional<std::uint64_t> maxAmount{};
std::optional<std::uint8_t> assetScale{};
std::optional<std::uint16_t> transferFee{};
std::optional<Slice> const& metadata{};
};
class MPTokenIssuanceCreate : public Transactor
@@ -51,7 +52,7 @@ public:
TER
doApply() override;
static TER
static Expected<MPTID, TER>
create(ApplyView& view, beast::Journal journal, MPTCreateArgs const& args);
};

View File

@@ -63,22 +63,39 @@ MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx)
}
TER
MPTokenIssuanceDestroy::doApply()
MPTokenIssuanceDestroy::destroy(
ApplyView& view,
beast::Journal journal,
MPTDestroyArgs const& args)
{
auto const mpt =
view().peek(keylet::mptIssuance(ctx_.tx[sfMPTokenIssuanceID]));
if (account_ != mpt->getAccountID(sfIssuer))
return tecINTERNAL;
auto const mpt = view.peek(keylet::mptIssuance(args.issuanceID));
if (!mpt)
return tecOBJECT_NOT_FOUND;
if (!view().dirRemove(
keylet::ownerDir(account_), (*mpt)[sfOwnerNode], mpt->key(), false))
if ((*mpt)[sfIssuer] != args.account)
return tecNO_PERMISSION;
auto const& issuer = args.account;
if ((*mpt)[~sfOutstandingAmount] != 0)
return tecHAS_OBLIGATIONS;
if (!view.dirRemove(
keylet::ownerDir(issuer), (*mpt)[sfOwnerNode], mpt->key(), false))
return tefBAD_LEDGER;
view().erase(mpt);
adjustOwnerCount(view(), view().peek(keylet::account(account_)), -1, j_);
view.erase(mpt);
adjustOwnerCount(view, view.peek(keylet::account(issuer)), -1, journal);
return tesSUCCESS;
}
TER
MPTokenIssuanceDestroy::doApply()
{
return destroy(
view(),
j_,
{.account = ctx_.tx[sfAccount],
.issuanceID = ctx_.tx[sfMPTokenIssuanceID]});
}
} // namespace ripple

View File

@@ -24,6 +24,12 @@
namespace ripple {
struct MPTDestroyArgs
{
AccountID const& account;
MPTID issuanceID;
};
class MPTokenIssuanceDestroy : public Transactor
{
public:
@@ -39,6 +45,12 @@ public:
static TER
preclaim(PreclaimContext const& ctx);
static TER
destroy(
ApplyView& view,
beast::Journal journal,
MPTDestroyArgs const& args);
TER
doApply() override;
};

View File

@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2022 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/VaultClawback.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
VaultClawback::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureSingleAssetVault))
return temDISABLED;
if (auto const ter = preflight1(ctx))
return ter;
if (ctx.tx.getFlags() & tfUniversalMask)
return temINVALID_FLAG;
return preflight2(ctx);
}
TER
VaultClawback::preclaim(PreclaimContext const& ctx)
{
auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID]));
if (!vault)
return tecOBJECT_NOT_FOUND;
return tesSUCCESS;
}
TER
VaultClawback::doApply()
{
auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID]));
if (!vault)
return tecOBJECT_NOT_FOUND;
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,48 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_VAULTCLAWBACK_H_INCLUDED
#define RIPPLE_TX_VAULTCLAWBACK_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class VaultClawback : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit VaultClawback(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -0,0 +1,151 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2023 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/MPTokenIssuanceCreate.h>
#include <xrpld/app/tx/detail/VaultCreate.h>
#include <xrpld/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
VaultCreate::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureSingleAssetVault))
return temDISABLED;
if (auto const ter = preflight1(ctx))
return ter;
if (ctx.tx.getFlags() & tfVaultCreateMask)
return temINVALID_FLAG;
if (auto const data = ctx.tx[~sfData])
{
if (data->length() > maxVaultDataLength)
return temSTRING_TOO_LARGE;
}
// This block is copied from `MPTokenIssuanceCreate::preflight`.
if (auto const metadata = ctx.tx[~sfMPTokenMetadata])
{
if (metadata->length() == 0 ||
metadata->length() > maxMPTokenMetadataLength)
return temSTRING_TOO_LARGE;
}
return preflight2(ctx);
}
XRPAmount
VaultCreate::calculateBaseFee(ReadView const& view, STTx const& tx)
{
// One reserve increment is typically much greater than one base fee.
return view.fees().increment;
}
TER
VaultCreate::preclaim(PreclaimContext const& ctx)
{
auto asset = ctx.tx[sfAsset];
if (asset.holds<MPTIssue>())
{
auto mptID = asset.get<MPTIssue>().getMptID();
auto issuance = ctx.view.read(keylet::mptIssuance(mptID));
if (issuance->getFlags() & lsfMPTLocked)
return tecLOCKED;
if ((issuance->getFlags() & lsfMPTCanTransfer) == 0)
return tecLOCKED;
}
return tesSUCCESS;
}
TER
VaultCreate::doApply()
{
// All return codes in `doApply` must be `tec`, `ter`, or `tes`.
// As we move checks into `preflight` and `preclaim`,
// we can consider downgrading them to `tef` or `tem`.
auto const& tx = ctx_.tx;
auto const& ownerId = account_;
auto sequence = tx.getSequence();
auto owner = view().peek(keylet::account(ownerId));
auto vault = std::make_shared<SLE>(keylet::vault(ownerId, sequence));
if (auto ter = dirLink(view(), ownerId, vault))
return ter;
// Should the next 3 lines be folded into `dirLink`?
adjustOwnerCount(view(), owner, 1, j_);
auto ownerCount = owner->at(sfOwnerCount);
if (mPriorBalance < view().fees().accountReserve(ownerCount))
return tecINSUFFICIENT_RESERVE;
auto maybePseudo = createPseudoAccount(view(), vault->key());
if (!maybePseudo)
return maybePseudo.error();
auto& pseudo = *maybePseudo;
auto pseudoId = pseudo->at(sfAccount);
if (auto ter =
addEmptyHolding(view(), pseudoId, mPriorBalance, tx[sfAsset], j_))
return ter;
auto txFlags = tx.getFlags();
std::uint32_t mptFlags = 0;
if (!(txFlags & tfVaultShareNonTransferable))
mptFlags |= (lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer);
if (txFlags & tfVaultPrivate)
mptFlags |= lsfMPTRequireAuth;
auto maybeShare = MPTokenIssuanceCreate::create(
view(),
j_,
{
.account = pseudoId,
.sequence = 1,
.flags = mptFlags,
.metadata = tx[~sfMPTokenMetadata],
});
if (!maybeShare)
return maybeShare.error();
auto& share = *maybeShare;
vault->at(sfFlags) = txFlags & tfVaultPrivate;
vault->at(sfSequence) = sequence;
vault->at(sfOwner) = ownerId;
vault->at(sfAccount) = pseudoId;
vault->at(sfAsset) = tx[sfAsset];
// Leave default values for AssetTotal and AssetAvailable, both zero.
if (auto value = tx[~sfAssetMaximum])
vault->at(sfAssetMaximum) = *value;
vault->at(sfMPTokenIssuanceID) = share;
if (auto value = tx[~sfData])
vault->at(sfData) = *value;
// No `LossUnrealized`.
view().insert(vault);
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,51 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_VAULTCREATE_H_INCLUDED
#define RIPPLE_TX_VAULTCREATE_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class VaultCreate : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit VaultCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -0,0 +1,105 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2022 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/VaultDelete.h>
#include <xrpld/app/tx/detail/MPTokenIssuanceDestroy.h>
#include <xrpld/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
VaultDelete::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureSingleAssetVault))
return temDISABLED;
if (auto const ter = preflight1(ctx))
return ter;
if (ctx.tx.getFlags() & tfUniversalMask)
return temINVALID_FLAG;
return preflight2(ctx);
}
TER
VaultDelete::preclaim(PreclaimContext const& ctx)
{
auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID]));
if (!vault)
return tecOBJECT_NOT_FOUND;
if (vault->at(sfOwner) != ctx.tx[sfAccount])
return tecNO_PERMISSION;
if (vault->at(sfAssetAvailable) != 0)
return tecHAS_OBLIGATIONS;
return tesSUCCESS;
}
TER
VaultDelete::doApply()
{
auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID]));
if (!vault)
return tecOBJECT_NOT_FOUND;
// Destroy the asset holding.
if (auto ter = removeEmptyHolding(
view(), vault->at(sfAccount), vault->at(sfAsset), j_))
return ter;
// Destroy the share issuance.
if (auto ter = MPTokenIssuanceDestroy::destroy(
view(),
j_,
{.account = vault->at(sfAccount),
.issuanceID = vault->at(sfMPTokenIssuanceID)}))
return ter;
// The psuedo-account's directory should have been deleted already.
auto const& pseudoID = vault->at(sfAccount);
if (view().peek(keylet::ownerDir(pseudoID)))
return tecHAS_OBLIGATIONS;
// Destroy the pseudo-account.
view().erase(view().peek(keylet::account(pseudoID)));
// Remove the vault from its owner's directory.
auto const ownerID = vault->at(sfOwner);
if (!view().dirRemove(
keylet::ownerDir(ownerID),
vault->at(sfOwnerNode),
vault->key(),
false))
return tefBAD_LEDGER;
auto const owner = view().peek(keylet::account(ownerID));
if (!owner)
return tefBAD_LEDGER;
adjustOwnerCount(view(), owner, -1, j_);
// Destroy the vault.
view().erase(vault);
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,48 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_VAULTDELETE_H_INCLUDED
#define RIPPLE_TX_VAULTDELETE_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class VaultDelete : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit VaultDelete(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -0,0 +1,79 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2022 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/VaultDeposit.h>
#include <xrpld/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
VaultDeposit::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureSingleAssetVault))
return temDISABLED;
if (auto const ter = preflight1(ctx))
return ter;
if (ctx.tx.getFlags() & tfUniversalMask)
return temINVALID_FLAG;
return preflight2(ctx);
}
TER
VaultDeposit::preclaim(PreclaimContext const& ctx)
{
auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID]));
if (!vault)
return tecOBJECT_NOT_FOUND;
return tesSUCCESS;
}
TER
VaultDeposit::doApply()
{
auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID]));
if (!vault)
return tecOBJECT_NOT_FOUND;
auto amount = ctx_.tx[sfAmount];
Asset const& asset = vault->at(sfAsset);
if (amount.asset() != asset)
return tecWRONG_ASSET;
if (accountHolds(view(), account_, asset,
FreezeHandling::fhZERO_IF_FROZEN,
AuthHandling::ahZERO_IF_UNAUTHORIZED, j_) < amount)
{
return tecINSUFFICIENT_FUNDS;
}
// STAmount assetTotalNew{sfAssetTotal};
// assetTotalNew = vault->at(sfAssetTotal) + amount;
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,48 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_VAULTDEPOSIT_H_INCLUDED
#define RIPPLE_TX_VAULTDEPOSIT_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class VaultDeposit : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit VaultDeposit(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -0,0 +1,83 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2023 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/VaultSet.h>
#include <xrpld/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
VaultSet::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureSingleAssetVault))
return temDISABLED;
if (auto const ter = preflight1(ctx))
return ter;
if (ctx.tx.getFlags() & tfUniversalMask)
return temINVALID_FLAG;
if (auto const data = ctx.tx[~sfData])
{
if (data->length() > maxVaultDataLength)
return temSTRING_TOO_LARGE;
}
return preflight2(ctx);
}
TER
VaultSet::preclaim(PreclaimContext const& ctx)
{
return tesSUCCESS;
}
TER
VaultSet::doApply()
{
// All return codes in `doApply` must be `tec`, `ter`, or `tes`.
// As we move checks into `preflight` and `preclaim`,
// we can consider downgrading them to `tef` or `tem`.
auto const& tx = ctx_.tx;
auto const& owner = account_;
// Update existing object.
auto vault = view().peek({ltVAULT, tx[sfVaultID]});
if (!vault)
return tecOBJECT_NOT_FOUND;
// Assert that submitter is the Owner.
if (owner != vault->at(sfOwner))
return tecNO_PERMISSION;
// Update mutable flags and fields if given.
if (tx.isFieldPresent(sfData))
vault->at(sfData) = tx[sfData];
if (tx.isFieldPresent(sfAssetMaximum))
vault->at(sfAssetMaximum) = tx[sfAssetMaximum];
view().update(vault);
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,48 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_VAULTSET_H_INCLUDED
#define RIPPLE_TX_VAULTSET_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class VaultSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit VaultSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2022 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/VaultWithdraw.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
VaultWithdraw::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureSingleAssetVault))
return temDISABLED;
if (auto const ter = preflight1(ctx))
return ter;
if (ctx.tx.getFlags() & tfUniversalMask)
return temINVALID_FLAG;
return preflight2(ctx);
}
TER
VaultWithdraw::preclaim(PreclaimContext const& ctx)
{
auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID]));
if (!vault)
return tecOBJECT_NOT_FOUND;
return tesSUCCESS;
}
TER
VaultWithdraw::doApply()
{
auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID]));
if (!vault)
return tecOBJECT_NOT_FOUND;
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,48 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_VAULTWITHDRAW_H_INCLUDED
#define RIPPLE_TX_VAULTWITHDRAW_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class VaultWithdraw : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit VaultWithdraw(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -57,6 +57,12 @@
#include <xrpld/app/tx/detail/SetRegularKey.h>
#include <xrpld/app/tx/detail/SetSignerList.h>
#include <xrpld/app/tx/detail/SetTrust.h>
#include <xrpld/app/tx/detail/VaultClawback.h>
#include <xrpld/app/tx/detail/VaultCreate.h>
#include <xrpld/app/tx/detail/VaultDelete.h>
#include <xrpld/app/tx/detail/VaultDeposit.h>
#include <xrpld/app/tx/detail/VaultSet.h>
#include <xrpld/app/tx/detail/VaultWithdraw.h>
#include <xrpld/app/tx/detail/XChainBridge.h>
#include <xrpl/protocol/TxFormats.h>

View File

@@ -39,8 +39,6 @@
#include <memory>
#include <utility>
#include <vector>
namespace ripple {
enum class WaiveTransferFee : bool { No = false, Yes };
@@ -182,6 +180,15 @@ accountHolds(
AuthHandling zeroIfUnauthorized,
beast::Journal j);
[[nodiscard]] STAmount
accountHolds(
ReadView const& view,
AccountID const& account,
Asset const& asset,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j);
// Returns the amount an account can spend of the currency type saDefault, or
// returns saDefault if this account is the issuer of the currency in
// question. Should be used in favor of accountHolds when questioning how much
@@ -420,6 +427,12 @@ dirNext(
[[nodiscard]] std::function<void(SLE::ref)>
describeOwnerDir(AccountID const& account);
[[nodiscard]] TER
dirLink(ApplyView& view, AccountID const& owner, std::shared_ptr<SLE>& object);
[[nodiscard]] Expected<std::shared_ptr<SLE>, TER>
createPseudoAccount(ApplyView& view, uint256 const& pseudoOwnerKey);
// VFALCO NOTE Both STAmount parameters should just
// be "Amount", a unit-less number.
//
@@ -454,6 +467,30 @@ trustDelete(
AccountID const& uHighAccountID,
beast::Journal j);
/** Create the structures necessary for an account to hold an asset.
*
* If the asset is:
* - XRP: Do nothing.
* - IOU: Check that the asset is not globally frozen,
* and create a trust line (with limit 0).
* - MPT: Check that the asset is not globally locked,
* and create an MPToken.
*/
[[nodiscard]] TER
addEmptyHolding(
ApplyView& view,
AccountID const& account,
XRPAmount priorBalance,
Asset const& asset,
beast::Journal journal);
[[nodiscard]] TER
removeEmptyHolding(
ApplyView& view,
AccountID const& account,
Asset const& asset,
beast::Journal journal);
/** Delete an offer.
Requirements:
@@ -581,6 +618,33 @@ deleteAMMTrustLine(
std::optional<AccountID> const& ammAccountID,
beast::Journal j);
// From the perspective of a vault,
// return the number of shares to give the depositor
// when they deposit a fixed amount of assets.
[[nodiscard]] Expected<Number, TER>
assetsToSharesDeposit(
ReadView const& view,
std::shared_ptr<SLE> const& vault,
STAmount const& assets);
// From the perspective of a vault,
// return the number of shares to demand from the depositor
// when they ask to withdraw a fixed amount of assets.
[[nodiscard]] Expected<Number, TER>
assetsToSharesWithdraw(
ReadView const& view,
std::shared_ptr<SLE> const& vault,
STAmount const& assets);
// From the perspective of a vault,
// return the number of assets to give the depositor
// when they redeem a fixed amount of shares.
[[nodiscard]] Expected<Number, TER>
sharesToAssetsWithdraw(
ReadView const& view,
std::shared_ptr<SLE> const& vault,
STAmount const& shares);
} // namespace ripple
#endif

View File

@@ -18,6 +18,8 @@
//==============================================================================
#include <xrpld/ledger/ReadView.h>
// TODO: Move the helper out of the `app` module.
#include <xrpld/app/tx/detail/MPTokenAuthorize.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/chrono.h>
@@ -26,8 +28,12 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/st.h>
#include <optional>
#include <type_traits>
#include <variant>
namespace ripple {
@@ -362,6 +368,24 @@ accountHolds(
return amount;
}
[[nodiscard]] STAmount
accountHolds(
ReadView const& view,
AccountID const& account,
Asset const& asset,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j)
{
return std::visit([&] (auto const& value) {
if constexpr (std::is_same_v<std::remove_cvref_t<decltype(value)>, Issue>)
{
return accountHolds(view, account, value, zeroIfFrozen, j);
}
return accountHolds(view, account, value, zeroIfFrozen, zeroIfUnauthorized, j);
}, asset.value());
}
STAmount
accountFunds(
ReadView const& view,
@@ -852,6 +876,52 @@ describeOwnerDir(AccountID const& account)
};
}
TER
dirLink(ApplyView& view, AccountID const& owner, std::shared_ptr<SLE>& object)
{
auto const page = view.dirInsert(
keylet::ownerDir(owner), object->key(), describeOwnerDir(owner));
if (!page)
return tecDIR_FULL;
object->setFieldU64(sfOwnerNode, *page);
return tesSUCCESS;
}
Expected<std::shared_ptr<SLE>, TER>
createPseudoAccount(ApplyView& view, uint256 const& pseudoOwnerKey)
{
AccountID accountId;
for (auto i = 0;; ++i)
{
if (i >= 256)
return Unexpected(tecDUPLICATE);
ripesha_hasher rsh;
auto const hash = sha512Half(i, view.info().parentHash, pseudoOwnerKey);
rsh(hash.data(), hash.size());
accountId = static_cast<ripesha_hasher::result_type>(rsh);
if (!view.read(keylet::account(accountId)))
break;
}
// Create pseudo-account.
auto account = std::make_shared<SLE>(keylet::account(accountId));
account->setAccountID(sfAccount, accountId);
account->setFieldAmount(sfBalance, STAmount{});
std::uint32_t const seqno{
view.rules().enabled(featureDeletableAccounts) ? view.seq() : 1};
account->setFieldU32(sfSequence, seqno);
// Ignore reserves requirement, disable the master key, allow default
// rippling, and enable deposit authorization to prevent payments into
// pseudo-account.
account->setFieldU32(
sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
// Link the pseudo-account with its owner object.
// account->setFieldH256(sfPseudoOwner, pseudoOwnerKey);
view.insert(account);
return account;
}
TER
trustCreate(
ApplyView& view,
@@ -1006,6 +1076,119 @@ trustDelete(
return tesSUCCESS;
}
[[nodiscard]] TER
addEmptyHolding(
ApplyView& view,
AccountID const& accountID,
XRPAmount priorBalance,
Asset const& asset,
beast::Journal journal)
{
if (asset.holds<Issue>())
{
auto const& issue = asset.get<Issue>();
// Every account can hold XRP.
if (issue.native())
return tesSUCCESS;
auto const& issuerId = issue.getIssuer();
auto const& currency = issue.currency;
if (isGlobalFrozen(view, issuerId))
return tecFROZEN;
auto const& srcId = issuerId;
auto const& dstId = accountID;
auto const high = srcId > dstId;
auto const index = keylet::line(srcId, dstId, currency);
auto const sle = view.peek(keylet::account(accountID));
return trustCreate(
view,
high,
srcId,
dstId,
index.key,
sle,
/*auth=*/false,
/*noRipple=*/true,
/*freeze=*/false,
/*balance=*/STAmount{Issue{currency, noAccount()}},
/*limit=*/STAmount{Issue{currency, dstId}},
/*qualityIn=*/0,
/*qualityOut=*/0,
journal);
}
if (asset.holds<MPTIssue>())
{
auto const& mptIssue = asset.get<MPTIssue>();
auto const& mptID = mptIssue.getMptID();
auto const mpt = view.peek(keylet::mptIssuance(mptID));
if (mpt->getFlags() & lsfMPTLocked)
return tecLOCKED;
return MPTokenAuthorize::authorize(
view,
journal,
{.priorBalance = priorBalance,
.mptIssuanceID = mptID,
.accountID = accountID});
}
// Should be unreachable.
return tecINTERNAL;
}
[[nodiscard]] TER
removeEmptyHolding(
ApplyView& view,
AccountID const& accountID,
Asset const& asset,
beast::Journal journal)
{
if (asset.holds<Issue>())
{
auto const& issue = asset.get<Issue>();
if (issue.native())
{
auto const sle = view.read(keylet::account(accountID));
if (!sle)
return tecINTERNAL;
auto const balance = sle->getFieldAmount(sfBalance);
if (balance.xrp() != 0)
return tecHAS_OBLIGATIONS;
return tesSUCCESS;
}
// `asset` is an IOU.
auto const line = view.peek(keylet::line(accountID, issue));
if (!line)
return tecOBJECT_NOT_FOUND;
if (line->at(sfBalance)->iou() != beast::zero)
return tecHAS_OBLIGATIONS;
return trustDelete(
view,
line,
line->at(sfLowLimit)->getIssuer(),
line->at(sfHighLimit)->getIssuer(),
journal);
}
if (asset.holds<MPTIssue>())
{
auto const& mptIssue = asset.get<MPTIssue>();
auto const& mptID = mptIssue.getMptID();
// `MPTokenAuthorize::authorize` asserts that the balance is 0.
return MPTokenAuthorize::authorize(
view,
journal,
{.mptIssuanceID = mptID,
.accountID = accountID,
.flags = tfMPTUnauthorize});
}
// Should be unreachable.
return tecINTERNAL;
}
TER
offerDelete(ApplyView& view, std::shared_ptr<SLE> const& sle, beast::Journal j)
{
@@ -2039,4 +2222,66 @@ rippleCredit(
saAmount.asset().value());
}
Number
getShareTotal(ReadView const& view, std::shared_ptr<SLE> const& vault)
{
auto issuance =
view.read(keylet::mptIssuance(vault->at(sfMPTokenIssuanceID)));
return issuance->at(sfOutstandingAmount);
}
[[nodiscard]] Expected<Number, TER>
assetsToSharesDeposit(
ReadView const& view,
std::shared_ptr<SLE> const& vault,
STAmount const& assets)
{
assert(assets.asset() == vault->at(sfAsset));
Number assetTotal = *vault->at(sfAssetTotal);
if (assetTotal == 0)
return assets;
Number shareTotal = getShareTotal(view, vault);
auto shares = shareTotal * (assets / assetTotal);
return shares;
}
[[nodiscard]] Expected<Number, TER>
assetsToSharesWithdraw(
ReadView const& view,
std::shared_ptr<SLE> const& vault,
STAmount const& assets)
{
assert(assets.asset() == vault->at(sfAsset));
Number assetTotal = vault->at(sfAssetTotal);
assetTotal -= vault->at(sfLossUnrealized);
// TODO: What error here?
if (assets > assetTotal)
return Unexpected{tecINTERNAL};
if (assetTotal == 0)
return 0;
Number shareTotal = getShareTotal(view, vault);
auto shares = shareTotal * (assets / assetTotal);
// TODO: Limit by withdrawal policy?
return shares;
}
[[nodiscard]] Expected<Number, TER>
sharesToAssetsWithdraw(
ReadView const& view,
std::shared_ptr<SLE> const& vault,
STAmount const& shares)
{
assert(shares.asset() == vault->at(sfMPTokenIssuanceID));
Number assetTotal = vault->at(sfAssetTotal);
assetTotal -= vault->at(sfLossUnrealized);
if (assetTotal == 0)
return 0;
Number shareTotal = getShareTotal(view, vault);
if (shares > shareTotal)
return Unexpected{tecINTERNAL};
auto assets = assetTotal * (shares / shareTotal);
// TODO: Limit by withdrawal policy?
return assets;
}
} // namespace ripple