diff --git a/Builds/levelization/results/loops.txt b/Builds/levelization/results/loops.txt index f703a3a9d5d..fd5d6ffa04b 100644 --- a/Builds/levelization/results/loops.txt +++ b/Builds/levelization/results/loops.txt @@ -5,7 +5,7 @@ Loop: test.jtx test.unit_test test.unit_test == test.jtx Loop: xrpl.basics xrpl.json - xrpl.json ~= xrpl.basics + xrpl.json == xrpl.basics Loop: xrpld.app xrpld.core xrpld.app > xrpld.core diff --git a/include/xrpl/basics/MPTAmount.h b/include/xrpl/basics/MPTAmount.h new file mode 100644 index 00000000000..0aaa3a2853f --- /dev/null +++ b/include/xrpl/basics/MPTAmount.h @@ -0,0 +1,185 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 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_BASICS_INTEGRALAMOUNT_H_INCLUDED +#define RIPPLE_BASICS_INTEGRALAMOUNT_H_INCLUDED + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace ripple { + +class MPTAmount : private boost::totally_ordered, + private boost::additive, + private boost::equality_comparable, + private boost::additive +{ +public: + using value_type = std::int64_t; + +protected: + value_type value_; + +public: + MPTAmount() = default; + constexpr MPTAmount(MPTAmount const& other) = default; + constexpr MPTAmount& + operator=(MPTAmount const& other) = default; + + constexpr explicit MPTAmount(value_type value); + + constexpr MPTAmount& operator=(beast::Zero); + + MPTAmount& + operator+=(MPTAmount const& other); + + MPTAmount& + operator-=(MPTAmount const& other); + + MPTAmount + operator-() const; + + bool + operator==(MPTAmount const& other) const; + + bool + operator==(value_type other) const; + + bool + operator<(MPTAmount const& other) const; + + /** Returns true if the amount is not zero */ + explicit constexpr operator bool() const noexcept; + + /** Return the sign of the amount */ + constexpr int + signum() const noexcept; + + Json::Value + jsonClipped() const; + + /** Returns the underlying value. Code SHOULD NOT call this + function unless the type has been abstracted away, + e.g. in a templated function. + */ + constexpr value_type + value() const; + + friend std::istream& + operator>>(std::istream& s, MPTAmount& val); + + static MPTAmount + minPositiveAmount(); +}; + +constexpr MPTAmount::MPTAmount(value_type value) : value_(value) +{ +} + +constexpr MPTAmount& MPTAmount::operator=(beast::Zero) +{ + value_ = 0; + return *this; +} + +/** Returns true if the amount is not zero */ +constexpr MPTAmount::operator bool() const noexcept +{ + return value_ != 0; +} + +/** Return the sign of the amount */ +constexpr int +MPTAmount::signum() const noexcept +{ + return (value_ < 0) ? -1 : (value_ ? 1 : 0); +} + +/** Returns the underlying value. Code SHOULD NOT call this + function unless the type has been abstracted away, + e.g. in a templated function. +*/ +constexpr MPTAmount::value_type +MPTAmount::value() const +{ + return value_; +} + +inline std::istream& +operator>>(std::istream& s, MPTAmount& val) +{ + s >> val.value_; + return s; +} + +// Output MPTAmount as just the value. +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const MPTAmount& q) +{ + return os << q.value(); +} + +inline std::string +to_string(MPTAmount const& amount) +{ + return std::to_string(amount.value()); +} + +inline MPTAmount +mulRatio( + MPTAmount const& amt, + std::uint32_t num, + std::uint32_t den, + bool roundUp) +{ + using namespace boost::multiprecision; + + if (!den) + Throw("division by zero"); + + int128_t const amt128(amt.value()); + auto const neg = amt.value() < 0; + auto const m = amt128 * num; + auto r = m / den; + if (m % den) + { + if (!neg && roundUp) + r += 1; + if (neg && !roundUp) + r -= 1; + } + if (r > std::numeric_limits::max()) + Throw("XRP mulRatio overflow"); + return MPTAmount(r.convert_to()); +} + +} // namespace ripple + +#endif // RIPPLE_BASICS_INTEGRALAMOUNT_H_INCLUDED diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 30ce3f73173..89c15f7d1b8 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -20,6 +20,7 @@ #ifndef RIPPLE_BASICS_NUMBER_H_INCLUDED #define RIPPLE_BASICS_NUMBER_H_INCLUDED +#include #include #include #include @@ -52,6 +53,7 @@ class Number explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept; Number(XRPAmount const& x); + Number(MPTAmount const& x); constexpr rep mantissa() const noexcept; @@ -89,6 +91,7 @@ class Number lowest() noexcept; explicit operator XRPAmount() const; // round to nearest, even on tie + explicit operator MPTAmount() const; // round to nearest, even on tie explicit operator rep() const; // round to nearest, even on tie friend constexpr bool @@ -210,6 +213,10 @@ inline Number::Number(XRPAmount const& x) : Number{x.drops()} { } +inline Number::Number(MPTAmount const& x) : Number{x.value()} +{ +} + inline constexpr Number::rep Number::mantissa() const noexcept { diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index 2b44d3072ee..0518ee37ea5 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -548,6 +548,7 @@ class base_uint using uint128 = base_uint<128>; using uint160 = base_uint<160>; using uint256 = base_uint<256>; +using uint192 = base_uint<192>; template [[nodiscard]] inline constexpr std::strong_ordering @@ -633,6 +634,7 @@ operator<<(std::ostream& out, base_uint const& u) #ifndef __INTELLISENSE__ static_assert(sizeof(uint128) == 128 / 8, "There should be no padding bytes"); static_assert(sizeof(uint160) == 160 / 8, "There should be no padding bytes"); +static_assert(sizeof(uint192) == 192 / 8, "There should be no padding bytes"); static_assert(sizeof(uint256) == 256 / 8, "There should be no padding bytes"); #endif diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index a00d6b85c1b..b0c8860fb85 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -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 = 79; +static constexpr std::size_t numFeatures = 80; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -372,6 +372,7 @@ extern uint256 const fixEnforceNFTokenTrustline; extern uint256 const fixInnerObjTemplate2; extern uint256 const featureInvariantsV1_1; extern uint256 const fixNFTokenPageLinks; +extern uint256 const featureMPTokensV1; } // namespace ripple diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index f179bbacfab..e9f4100008e 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -287,6 +287,30 @@ did(AccountID const& account) noexcept; Keylet oracle(AccountID const& account, std::uint32_t const& documentID) noexcept; +Keylet +mptIssuance(AccountID const& issuer, std::uint32_t seq) noexcept; + +Keylet +mptIssuance(MPTID const& mpt) noexcept; + +inline Keylet +mptIssuance(uint256 const& issuance) +{ + return {ltMPTOKEN_ISSUANCE, issuance}; +} + +Keylet +mptoken(MPTID const& issuanceID, AccountID const& holder) noexcept; + +inline Keylet +mptoken(uint256 const& mptokenKey) +{ + return {ltMPTOKEN, mptokenKey}; +} + +Keylet +mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept; + } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: @@ -327,6 +351,9 @@ std::array, 6> const directAccountKeylets{ {&keylet::nftpage_max, jss::NFTokenPage, true}, {&keylet::did, jss::DID, true}}}; +MPTID +getMptID(AccountID const& account, std::uint32_t sequence); + } // namespace ripple #endif diff --git a/include/xrpl/protocol/Issue.h b/include/xrpl/protocol/Issue.h index a18502f2138..5bf14e81c73 100644 --- a/include/xrpl/protocol/Issue.h +++ b/include/xrpl/protocol/Issue.h @@ -46,6 +46,9 @@ class Issue { } + AccountID const& + getIssuer() const; + std::string getText() const; }; diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 0ee6c992d8d..464fbd3e125 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -196,6 +196,19 @@ enum LedgerEntryType : std::uint16_t \sa keylet::oracle */ ltORACLE = 0x0080, + + /** A ledger object representing an individual MPToken asset type, but not + * any balances of that asset itself. + + \sa keylet::mptIssuance + */ + ltMPTOKEN_ISSUANCE = 0x007e, + + /** A ledger object representing an individual MPToken balance. + + \sa keylet::mptoken + */ + ltMPTOKEN = 0x007f, //--------------------------------------------------------------------------- /** A special type, matching any ledger entry type. @@ -308,6 +321,18 @@ enum LedgerSpecificFlags { // ltNFTOKEN_OFFER lsfSellNFToken = 0x00000001, + + // ltMPTOKEN_ISSUANCE + lsfMPTLocked = 0x00000001, // Also used in ltMPTOKEN + lsfMPTCanLock = 0x00000002, + lsfMPTRequireAuth = 0x00000004, + lsfMPTCanEscrow = 0x00000008, + lsfMPTCanTrade = 0x00000010, + lsfMPTCanTransfer = 0x00000020, + lsfMPTCanClawback = 0x00000040, + + // ltMPTOKEN + lsfMPTAuthorized = 0x00000002, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h new file mode 100644 index 00000000000..7bc34adcf53 --- /dev/null +++ b/include/xrpl/protocol/MPTIssue.h @@ -0,0 +1,74 @@ +//------------------------------------------------------------------------------ +/* + 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_PROTOCOL_MPTISSUE_H_INCLUDED +#define RIPPLE_PROTOCOL_MPTISSUE_H_INCLUDED + +#include +#include + +namespace ripple { + +class MPTIssue +{ +private: + MPTID mptID_; + +public: + MPTIssue() = default; + + MPTIssue(MPTID const& id); + + AccountID + getIssuer() const; + + MPTID const& + getMptID() const; + + friend constexpr bool + operator==(MPTIssue const& lhs, MPTIssue const& rhs); + + friend constexpr bool + operator!=(MPTIssue const& lhs, MPTIssue const& rhs); +}; + +constexpr bool +operator==(MPTIssue const& lhs, MPTIssue const& rhs) +{ + return lhs.mptID_ == rhs.mptID_; +} + +constexpr bool +operator!=(MPTIssue const& lhs, MPTIssue const& rhs) +{ + return !(lhs.mptID_ == rhs.mptID_); +} + +inline bool +isXRP(MPTID const&) +{ + return false; +} + +Json::Value +to_json(MPTIssue const& issue); + +} // namespace ripple + +#endif // RIPPLE_PROTOCOL_MPTISSUE_H_INCLUDED diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 8d8a71dfef8..627c8041be8 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -95,6 +95,12 @@ std::size_t constexpr maxDIDAttestationLength = 256; /** The maximum length of a domain */ std::size_t constexpr maxDomainLength = 256; +/** The maximum length of MPTokenMetadata */ +std::size_t constexpr maxMPTokenMetadataLength = 1024; + +/** The maximum amount of MPTokenIssuance */ +std::uint64_t constexpr maxMPTokenAmount = 0x7FFFFFFFFFFFFFFFull; + /** A ledger index. */ using LedgerIndex = std::uint32_t; diff --git a/include/xrpl/protocol/Rate.h b/include/xrpl/protocol/Rate.h index b065acb2316..fc9aa7f2c13 100644 --- a/include/xrpl/protocol/Rate.h +++ b/include/xrpl/protocol/Rate.h @@ -21,6 +21,7 @@ #define RIPPLE_PROTOCOL_RATE_H_INCLUDED #include +#include #include #include #include @@ -67,6 +68,9 @@ operator<<(std::ostream& os, Rate const& rate) STAmount multiply(STAmount const& amount, Rate const& rate); +STMPTAmount +multiply(STMPTAmount const& amount, Rate const& rate); + STAmount multiplyRound(STAmount const& amount, Rate const& rate, bool roundUp); diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 7f54201a4b8..9090416c514 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -42,6 +42,7 @@ Some fields have a different meaning for their // Forwards class STAccount; +class STEitherAmount; class STAmount; class STIssue; class STBlob; @@ -56,43 +57,49 @@ class STCurrency; #pragma push_macro("XMACRO") #undef XMACRO -#define XMACRO(STYPE) \ - /* special types */ \ - STYPE(STI_UNKNOWN, -2) \ - STYPE(STI_NOTPRESENT, 0) \ - STYPE(STI_UINT16, 1) \ - \ - /* types (common) */ \ - STYPE(STI_UINT32, 2) \ - STYPE(STI_UINT64, 3) \ - STYPE(STI_UINT128, 4) \ - STYPE(STI_UINT256, 5) \ - STYPE(STI_AMOUNT, 6) \ - STYPE(STI_VL, 7) \ - STYPE(STI_ACCOUNT, 8) \ - \ - /* 9-13 are reserved */ \ - STYPE(STI_OBJECT, 14) \ - STYPE(STI_ARRAY, 15) \ - \ - /* types (uncommon) */ \ - STYPE(STI_UINT8, 16) \ - STYPE(STI_UINT160, 17) \ - STYPE(STI_PATHSET, 18) \ - STYPE(STI_VECTOR256, 19) \ - STYPE(STI_UINT96, 20) \ - STYPE(STI_UINT192, 21) \ - STYPE(STI_UINT384, 22) \ - STYPE(STI_UINT512, 23) \ - STYPE(STI_ISSUE, 24) \ - STYPE(STI_XCHAIN_BRIDGE, 25) \ - STYPE(STI_CURRENCY, 26) \ - \ - /* high-level types */ \ - /* cannot be serialized inside other types */ \ - STYPE(STI_TRANSACTION, 10001) \ - STYPE(STI_LEDGERENTRY, 10002) \ - STYPE(STI_VALIDATION, 10003) \ +#define XMACRO(STYPE) \ + /* special types */ \ + STYPE(STI_UNKNOWN, -2) \ + STYPE(STI_NOTPRESENT, 0) \ + STYPE(STI_UINT16, 1) \ + \ + /* types (common) */ \ + STYPE(STI_UINT32, 2) \ + STYPE(STI_UINT64, 3) \ + STYPE(STI_UINT128, 4) \ + STYPE(STI_UINT256, 5) \ + /* Need two enumerators with the same value */ \ + /* so that SF_AMOUNT and SF_EITHER_AMOUNT */ \ + /* map to the same serialization id. */ \ + /* This is an artifact of */ \ + /* CONSTRUCT_TYPED_SFIELD */ \ + STYPE(STI_AMOUNT, 6) \ + STYPE(STI_EITHER_AMOUNT, 6) \ + STYPE(STI_VL, 7) \ + STYPE(STI_ACCOUNT, 8) \ + \ + /* 9-13 are reserved */ \ + STYPE(STI_OBJECT, 14) \ + STYPE(STI_ARRAY, 15) \ + \ + /* types (uncommon) */ \ + STYPE(STI_UINT8, 16) \ + STYPE(STI_UINT160, 17) \ + STYPE(STI_PATHSET, 18) \ + STYPE(STI_VECTOR256, 19) \ + STYPE(STI_UINT96, 20) \ + STYPE(STI_UINT192, 21) \ + STYPE(STI_UINT384, 22) \ + STYPE(STI_UINT512, 23) \ + STYPE(STI_ISSUE, 24) \ + STYPE(STI_XCHAIN_BRIDGE, 25) \ + STYPE(STI_CURRENCY, 26) \ + \ + /* high-level types */ \ + /* cannot be serialized inside other types */ \ + STYPE(STI_TRANSACTION, 10001) \ + STYPE(STI_LEDGERENTRY, 10002) \ + STYPE(STI_VALIDATION, 10003) \ STYPE(STI_METADATA, 10004) #pragma push_macro("TO_ENUM") @@ -322,6 +329,37 @@ struct OptionaledField } }; +/** A field representing a variant with a type known at compile time. + * First template parameter is the variant type, the second + * template parameter is one of its alternative types. A variant field + * enables STObject::operator[]() overload to return the specified + * alternative type. For instance, STEitherAmount is a variant of STAmount + * and STMPTAmount. Some Amount fields, like SFee, don't support MPT + * and are declared as TypedVariantField. + * Conversely, sfAmount field supports MPT and is declared as + * TypedVariantField. Then tx[sfFee] always returns STAmount, + * while tx[sfAmount] returns STEitherAmount and the caller has to get + * the specific type that STEitherAmount holds. + */ +template +struct TypedVariantField : TypedField +{ + template + explicit TypedVariantField( + SField::private_access_tag_t pat, + Args&&... args); +}; + +/** Indicate std::optional variant field semantics. */ +template +struct OptionaledVariantField : OptionaledField +{ + explicit OptionaledVariantField(TypedVariantField const& f_) + : OptionaledField(f_) + { + } +}; + template inline OptionaledField operator~(TypedField const& f) @@ -329,6 +367,13 @@ operator~(TypedField const& f) return OptionaledField(f); } +template +inline OptionaledVariantField +operator~(TypedVariantField const& f) +{ + return OptionaledVariantField(f); +} + //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ @@ -346,7 +391,8 @@ using SF_UINT384 = TypedField>; using SF_UINT512 = TypedField>; using SF_ACCOUNT = TypedField; -using SF_AMOUNT = TypedField; +using SF_AMOUNT = TypedVariantField; +using SF_EITHER_AMOUNT = TypedVariantField; using SF_ISSUE = TypedField; using SF_CURRENCY = TypedField; using SF_VL = TypedField; @@ -373,6 +419,7 @@ extern SF_UINT8 const sfScale; extern SF_UINT8 const sfTickSize; extern SF_UINT8 const sfUNLModifyDisabling; extern SF_UINT8 const sfHookResult; +extern SF_UINT8 const sfAssetScale; // 16-bit integers (common) extern SF_UINT16 const sfLedgerEntryType; @@ -467,6 +514,10 @@ extern SF_UINT64 const sfXChainClaimID; extern SF_UINT64 const sfXChainAccountCreateCount; extern SF_UINT64 const sfXChainAccountClaimCount; extern SF_UINT64 const sfAssetPrice; +extern SF_UINT64 const sfMaximumAmount; +extern SF_UINT64 const sfOutstandingAmount; +extern SF_UINT64 const sfLockedAmount; +extern SF_UINT64 const sfMPTAmount; // 128-bit extern SF_UINT128 const sfEmailHash; @@ -477,6 +528,9 @@ extern SF_UINT160 const sfTakerPaysIssuer; extern SF_UINT160 const sfTakerGetsCurrency; extern SF_UINT160 const sfTakerGetsIssuer; +// 192-bit (common) +extern SF_UINT192 const sfMPTokenIssuanceID; + // 256-bit (common) extern SF_UINT256 const sfLedgerHash; extern SF_UINT256 const sfParentHash; @@ -513,7 +567,7 @@ extern SF_UINT256 const sfHookNamespace; extern SF_UINT256 const sfHookSetTxnID; // currency amount (common) -extern SF_AMOUNT const sfAmount; +extern SF_EITHER_AMOUNT const sfAmount; extern SF_AMOUNT const sfBalance; extern SF_AMOUNT const sfLimitAmount; extern SF_AMOUNT const sfTakerPays; @@ -564,6 +618,7 @@ extern SF_VL const sfDIDDocument; extern SF_VL const sfData; extern SF_VL const sfAssetClass; extern SF_VL const sfProvider; +extern SF_VL const sfMPTokenMetadata; // variable length (uncommon) extern SF_VL const sfFulfillment; @@ -587,6 +642,7 @@ extern SF_ACCOUNT const sfUnauthorize; extern SF_ACCOUNT const sfRegularKey; extern SF_ACCOUNT const sfNFTokenMinter; extern SF_ACCOUNT const sfEmitCallback; +extern SF_ACCOUNT const sfMPTokenHolder; // account (uncommon) extern SF_ACCOUNT const sfHookAccount; diff --git a/include/xrpl/protocol/SOTemplate.h b/include/xrpl/protocol/SOTemplate.h index c0fcfb64358..f6c231d1310 100644 --- a/include/xrpl/protocol/SOTemplate.h +++ b/include/xrpl/protocol/SOTemplate.h @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -39,6 +40,9 @@ enum SOEStyle { // constructed with STObject::makeInnerObject() }; +/** Amount fields that can support MPT */ +enum SOETxMPTAmount { soeMPTNone, soeMPTSupported, soeMPTNotSupported }; + //------------------------------------------------------------------------------ /** An element in a SOTemplate. */ @@ -47,10 +51,11 @@ class SOElement // Use std::reference_wrapper so SOElement can be stored in a std::vector. std::reference_wrapper sField_; SOEStyle style_; + SOETxMPTAmount supportMpt_; -public: - SOElement(SField const& fieldName, SOEStyle style) - : sField_(fieldName), style_(style) +private: + void + init(SField const& fieldName) const { if (!sField_.get().isUseful()) { @@ -62,6 +67,28 @@ class SOElement } } +public: + SOElement(SField const& fieldName, SOEStyle style) + : sField_(fieldName), style_(style), supportMpt_(soeMPTNone) + { + init(fieldName); + } + SOElement( + TypedVariantField const& fieldName, + SOEStyle style) + : sField_(fieldName), style_(style), supportMpt_(soeMPTNotSupported) + { + init(fieldName); + } + SOElement( + TypedVariantField const& fieldName, + SOEStyle style, + SOETxMPTAmount supportMpt = soeMPTNotSupported) + : sField_(fieldName), style_(style), supportMpt_(supportMpt) + { + init(fieldName); + } + SField const& sField() const { @@ -73,6 +100,12 @@ class SOElement { return style_; } + + SOETxMPTAmount + supportMPT() const + { + return supportMpt_; + } }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 3eed0860f54..6cd5cc7a557 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -26,7 +26,6 @@ #include #include #include -#include #include #include #include @@ -43,7 +42,7 @@ namespace ripple { // Wire form: // High 8 bits are (offset+142), legal range is, 80 to 22 inclusive // Low 56 bits are value, legal range is 10^15 to (10^16 - 1) inclusive -class STAmount final : public STBase, public CountedObject +class STAmount final { public: using mantissa_type = std::uint64_t; @@ -76,23 +75,13 @@ class STAmount final : public STBase, public CountedObject static std::uint64_t const uRateOne; //-------------------------------------------------------------------------- - STAmount(SerialIter& sit, SField const& name); + STAmount(SerialIter& sit); struct unchecked { explicit unchecked() = default; }; - // Do not call canonicalize - STAmount( - SField const& name, - Issue const& issue, - mantissa_type mantissa, - exponent_type exponent, - bool native, - bool negative, - unchecked); - STAmount( Issue const& issue, mantissa_type mantissa, @@ -103,31 +92,14 @@ class STAmount final : public STBase, public CountedObject // Call canonicalize STAmount( - SField const& name, Issue const& issue, mantissa_type mantissa, exponent_type exponent, bool native, bool negative); - STAmount(SField const& name, std::int64_t mantissa); - - STAmount( - SField const& name, - std::uint64_t mantissa = 0, - bool negative = false); - - STAmount( - SField const& name, - Issue const& issue, - std::uint64_t mantissa = 0, - int exponent = 0, - bool negative = false); - explicit STAmount(std::uint64_t mantissa = 0, bool negative = false); - explicit STAmount(SField const& name, STAmount const& amt); - STAmount( Issue const& issue, std::uint64_t mantissa = 0, @@ -242,24 +214,21 @@ class STAmount final : public STBase, public CountedObject //-------------------------------------------------------------------------- SerializedTypeID - getSType() const override; + getSType() const; std::string - getFullText() const override; + getFullText() const; std::string - getText() const override; + getText() const; - Json::Value getJson(JsonOptions) const override; + Json::Value getJson(JsonOptions) const; void - add(Serializer& s) const override; + add(Serializer& s) const; bool - isEquivalent(const STBase& t) const override; - - bool - isDefault() const override; + isDefault() const; XRPAmount xrp() const; @@ -268,18 +237,13 @@ class STAmount final : public STBase, public CountedObject private: static std::unique_ptr - construct(SerialIter&, SField const& name); + construct(SerialIter&); void set(std::int64_t v); void canonicalize(); - STBase* - copy(std::size_t n, void* buf) const override; - STBase* - move(std::size_t n, void* buf) override; - STAmount& operator=(IOUAmount const& iou); @@ -302,12 +266,6 @@ amountFromQuality(std::uint64_t rate); STAmount amountFromString(Issue const& issue, std::string const& amount); -STAmount -amountFromJson(SField const& name, Json::Value const& v); - -bool -amountFromJsonNoThrow(STAmount& result, Json::Value const& jvSource); - // IOUAmount and XRPAmount define toSTAmount, defining this // trivial conversion here makes writing generic code easier inline STAmount const& @@ -489,6 +447,9 @@ operator>=(STAmount const& lhs, STAmount const& rhs) STAmount operator-(STAmount const& value); +std::ostream& +operator<<(std::ostream& out, const STAmount& t); + //------------------------------------------------------------------------------ // // Arithmetic @@ -585,18 +546,4 @@ class STAmountSO } // namespace ripple -//------------------------------------------------------------------------------ -namespace Json { -template <> -inline ripple::STAmount -getOrThrow(Json::Value const& v, ripple::SField const& field) -{ - using namespace ripple; - Json::StaticString const& key = field.getJsonName(); - if (!v.isMember(key)) - Throw(key); - Json::Value const& inner = v[key]; - return amountFromJson(field, inner); -} -} // namespace Json #endif diff --git a/include/xrpl/protocol/STBitString.h b/include/xrpl/protocol/STBitString.h index 7dc92303e72..f3a74f2fc54 100644 --- a/include/xrpl/protocol/STBitString.h +++ b/include/xrpl/protocol/STBitString.h @@ -84,6 +84,7 @@ class STBitString final : public STBase, public CountedObject> using STUInt128 = STBitString<128>; using STUInt160 = STBitString<160>; +using STUInt192 = STBitString<192>; using STUInt256 = STBitString<256>; template @@ -136,6 +137,13 @@ STUInt160::getSType() const return STI_UINT160; } +template <> +inline SerializedTypeID +STUInt192::getSType() const +{ + return STI_UINT192; +} + template <> inline SerializedTypeID STUInt256::getSType() const diff --git a/include/xrpl/protocol/STEitherAmount.h b/include/xrpl/protocol/STEitherAmount.h new file mode 100644 index 00000000000..d4b5756451f --- /dev/null +++ b/include/xrpl/protocol/STEitherAmount.h @@ -0,0 +1,264 @@ +//------------------------------------------------------------------------------ +/* + 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_PROTOCOL_STEITHERAMOUNT_H_INCLUDED +#define RIPPLE_PROTOCOL_STEITHERAMOUNT_H_INCLUDED + +#include +#include + +namespace ripple { + +template +concept ValidAmountType = + std::is_same_v || std::is_same_v; + +template +concept EitherAmountType = std::is_same_v || + std::is_same_v>; + +class STEitherAmount : public STBase, public CountedObject +{ +private: + std::variant amount_; + +public: + using value_type = STEitherAmount; + STEitherAmount() = default; + STEitherAmount(SerialIter& sit, SField const& name); + STEitherAmount(XRPAmount const& amount); + STEitherAmount(STAmount const& amount); + STEitherAmount(SField const& name, STAmount const& amount = STAmount{}); + STEitherAmount(SField const& name, STMPTAmount const& amount); + STEitherAmount(STMPTAmount const& amount); + + STEitherAmount& + operator=(STAmount const&); + STEitherAmount& + operator=(STMPTAmount const&); + STEitherAmount& + operator=(XRPAmount const&); + + SerializedTypeID + getSType() const override; + + std::string + getFullText() const override; + + std::string + getText() const override; + + Json::Value getJson(JsonOptions) const override; + + void + setJson(Json::Value&) const; + + void + add(Serializer& s) const override; + + bool + isEquivalent(const STBase& t) const override; + + bool + isDefault() const override; + + //------------------------------------------------------------------------------ + + bool + isMPT() const; + + bool + isIssue() const; + + STEitherAmount const& + value() const; + + std::variant const& + getValue() const; + + std::variant& + getValue(); + + AccountID + getIssuer() const; + + bool + negative() const; + + bool + native() const; + + STEitherAmount + zeroed() const; + + int + signum() const noexcept; + + template + T const& + get() const; + + template + T& + get(); + +private: + STBase* + copy(std::size_t n, void* buf) const override; + STBase* + move(std::size_t n, void* buf) override; +}; + +template +T const& +STEitherAmount::get() const +{ + if (std::holds_alternative(amount_)) + return std::get(amount_); + Throw("Invalid STEitherAmount conversion"); +} + +template +T& +STEitherAmount::get() +{ + if (std::holds_alternative(amount_)) + return std::get(amount_); + Throw("Invalid STEitherAmount conversion"); +} + +template +decltype(auto) +get(auto&& amount) +{ + using TAmnt = std::decay_t; + if constexpr (std::is_same_v) + { + if constexpr (std::is_lvalue_reference_v) + return amount.template get(); + else + return T{amount.template get()}; + } + else if constexpr (std::is_same_v>) + { + static std::optional t; + if (amount.has_value()) + return std::make_optional(amount->template get()); + return t; + } + else if constexpr (std::is_convertible_v) + { + if constexpr (std::is_lvalue_reference_v) + return amount.operator STEitherAmount().template get(); + else + return T{amount.operator STEitherAmount().template get()}; + } + else + { + bool const alwaysFalse = !std::is_same_v; + static_assert(alwaysFalse, "Invalid STEitherAmount conversion"); + } +} + +STEitherAmount +amountFromJson(SField const& name, Json::Value const& v); + +STAmount +amountFromJson(SF_AMOUNT const& name, Json::Value const& v); + +bool +amountFromJsonNoThrow(STEitherAmount& result, Json::Value const& jvSource); + +bool +amountFromJsonNoThrow(STAmount& result, Json::Value const& jvSource); + +inline bool +operator==(STEitherAmount const& lhs, STEitherAmount const& rhs) +{ + return std::visit( + [&](T1 const& a1, T2 const& a2) { + if constexpr (std::is_same_v) + return a1 == a2; + else + return false; + }, + lhs.getValue(), + rhs.getValue()); +} + +inline bool +operator!=(STEitherAmount const& lhs, STEitherAmount const& rhs) +{ + return !operator==(lhs, rhs); +} + +template +bool +isMPT(T const& amount) +{ + if constexpr (std::is_same_v) + return true; + else if constexpr (std::is_same_v) + return false; +} + +template +bool +isMPT(T const& amount) +{ + if constexpr (std::is_same_v) + return amount.isMPT(); + else + return amount && amount->isMPT(); +} + +template +bool +isIssue(T const& amount) +{ + return !isMPT(amount); +} + +inline bool +isXRP(STEitherAmount const& amount) +{ + if (amount.isIssue()) + return isXRP(get(amount)); + return false; +} + +} // namespace ripple + +//------------------------------------------------------------------------------ +namespace Json { +template <> +inline ripple::STAmount +getOrThrow(Json::Value const& v, ripple::SField const& field) +{ + using namespace ripple; + Json::StaticString const& key = field.getJsonName(); + if (!v.isMember(key)) + Throw(key); + Json::Value const& inner = v[key]; + return get(amountFromJson(field, inner)); +} + +} // namespace Json + +#endif // RIPPLE_PROTOCOL_STEITHERAMOUNT_H_INCLUDED diff --git a/include/xrpl/protocol/STMPTAmount.h b/include/xrpl/protocol/STMPTAmount.h new file mode 100644 index 00000000000..8e467b6453b --- /dev/null +++ b/include/xrpl/protocol/STMPTAmount.h @@ -0,0 +1,195 @@ +//------------------------------------------------------------------------------ +/* + 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_PROTOCOL_STMPTAMOUNT_H_INCLUDED +#define RIPPLE_PROTOCOL_STMPTAMOUNT_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { + +struct Rate; + +class STMPTAmount final : public MPTAmount +{ +private: + MPTIssue issue_; + +public: + static constexpr std::uint8_t cMPToken = 0x20; + static constexpr std::uint8_t cPositive = 0x40; + + STMPTAmount(SerialIter& sit); + STMPTAmount( + MPTIssue const& issue, + std::uint64_t value, + bool negative = false); + STMPTAmount(MPTIssue const& issue, std::int64_t value = 0); + explicit STMPTAmount(value_type value = 0); + + SerializedTypeID + getSType() const; + + std::string + getFullText() const; + + std::string + getText() const; + + Json::Value getJson(JsonOptions) const; + + void + add(Serializer& s) const; + + void + setJson(Json::Value& elem) const; + + bool + isDefault() const; + + AccountID + getIssuer() const; + + MPTIssue const& + issue() const; + + MPTID const& + getCurrency() const; + + void + clear(); + + void + clear(MPTIssue const& issue); + + STMPTAmount + zeroed() const; + + int + signum() const noexcept; + + STMPTAmount& + operator+=(STMPTAmount const& other); + + STMPTAmount& + operator-=(STMPTAmount const& other); + + STMPTAmount + operator-() const; + + STMPTAmount& operator=(beast::Zero); +}; + +inline STMPTAmount +operator+(STMPTAmount const& lhs, STMPTAmount const& rhs) +{ + if (lhs.issue() != rhs.issue()) + Throw("Can't add amounts that aren't comparable!"); + return {lhs.issue(), lhs.value() + rhs.value()}; +} + +inline STMPTAmount +operator-(STMPTAmount const& lhs, STMPTAmount const& rhs) +{ + return lhs + (-rhs); +} + +inline STMPTAmount& +STMPTAmount::operator+=(const ripple::STMPTAmount& other) +{ + *this = *this + other; + return *this; +} + +inline STMPTAmount& +STMPTAmount::operator-=(const ripple::STMPTAmount& other) +{ + *this = *this - other; + return *this; +} + +inline STMPTAmount +STMPTAmount::operator-() const +{ + return {issue_, -value_}; +} + +inline STMPTAmount& STMPTAmount::operator=(beast::Zero) +{ + clear(); + return *this; +} + +inline bool +operator==(STMPTAmount const& lhs, STMPTAmount const& rhs) +{ + return lhs.issue() == rhs.issue() && lhs.value() == rhs.value(); +} + +inline bool +operator<(STMPTAmount const& lhs, STMPTAmount const& rhs) +{ + if (lhs.issue() != rhs.issue()) + Throw( + "Can't compare amounts that are't comparable!"); + return lhs.value() < rhs.value(); +} + +inline bool +operator!=(STMPTAmount const& lhs, STMPTAmount const& rhs) +{ + return !(lhs == rhs); +} + +inline bool +operator>(STMPTAmount const& lhs, STMPTAmount const& rhs) +{ + return rhs < lhs; +} + +inline bool +operator<=(STMPTAmount const& lhs, STMPTAmount const& rhs) +{ + return !(rhs < lhs); +} + +inline bool +operator>=(STMPTAmount const& lhs, STMPTAmount const& rhs) +{ + return !(lhs < rhs); +} + +inline bool +isLegalNet(STMPTAmount const& value) +{ + return true; +} + +STMPTAmount +amountFromString(MPTIssue const& issue, std::string const& amount); + +STMPTAmount +multiply(STMPTAmount const& amount, Rate const& rate); + +} // namespace ripple + +#endif // RIPPLE_PROTOCOL_STMPTAMOUNT_H_INCLUDED diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index b3cef83de5f..052751a2bc6 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -26,10 +26,11 @@ #include #include #include +#include #include -#include #include #include +#include #include #include #include @@ -54,11 +55,11 @@ throwFieldNotFound(SField const& field) class STObject : public STBase, public CountedObject { // Proxy value for a STBase derived class - template + template class Proxy; - template + template class ValueProxy; - template + template class OptionalProxy; struct Transform @@ -226,6 +227,8 @@ class STObject : public STBase, public CountedObject uint160 getFieldH160(SField const& field) const; + uint192 + getFieldH192(SField const& field) const; uint256 getFieldH256(SField const& field) const; AccountID @@ -233,8 +236,11 @@ class STObject : public STBase, public CountedObject Blob getFieldVL(SField const& field) const; - STAmount const& + STEitherAmount const& getFieldAmount(SField const& field) const; + STAmount const& + getFieldAmount( + TypedVariantField const& field) const; STPathSet const& getFieldPathSet(SField const& field) const; const STVector256& @@ -255,6 +261,12 @@ class STObject : public STBase, public CountedObject typename T::value_type operator[](TypedField const& f) const; + /** Overload for amount field + */ + template + typename H::value_type + operator[](TypedVariantField const& f) const; + /** Get the value of a field as a std::optional @param An OptionaledField built from an SField value representing the @@ -267,6 +279,12 @@ class STObject : public STBase, public CountedObject std::optional> operator[](OptionaledField const& of) const; + /** Overload for a variant field + */ + template + std::optional> + operator[](OptionaledVariantField const& of) const; + /** Get a modifiable field value. @param A TypedField built from an SField value representing the desired object field. In typical use, the TypedField will be implicitly @@ -278,6 +296,12 @@ class STObject : public STBase, public CountedObject ValueProxy operator[](TypedField const& f); + /** Overload for a variant field + */ + template + ValueProxy + operator[](TypedVariantField const& f); + /** Return a modifiable field value as std::optional @param An OptionaledField built from an SField value representing the @@ -291,6 +315,12 @@ class STObject : public STBase, public CountedObject OptionalProxy operator[](OptionaledField const& of); + /** Overload for a variant field + */ + template + OptionalProxy + operator[](OptionaledVariantField const& of); + /** Get the value of a field. @param A TypedField built from an SField value representing the desired object field. In typical use, the TypedField will be implicitly @@ -302,6 +332,12 @@ class STObject : public STBase, public CountedObject typename T::value_type at(TypedField const& f) const; + /** Overload for a variant field + */ + template + typename H::value_type + at(TypedVariantField const& f) const; + /** Get the value of a field as std::optional @param An OptionaledField built from an SField value representing the @@ -314,6 +350,12 @@ class STObject : public STBase, public CountedObject std::optional> at(OptionaledField const& of) const; + /** Overload for a variant field + */ + template + std::optional> + at(OptionaledVariantField const& of) const; + /** Get a modifiable field value. @param A TypedField built from an SField value representing the desired object field. In typical use, the TypedField will be implicitly @@ -325,6 +367,12 @@ class STObject : public STBase, public CountedObject ValueProxy at(TypedField const& f); + /** Overload for a variant field + */ + template + ValueProxy + at(TypedVariantField const& f); + /** Return a modifiable field value as std::optional @param An OptionaledField built from an SField value representing the @@ -338,6 +386,12 @@ class STObject : public STBase, public CountedObject OptionalProxy at(OptionaledField const& of); + /** Overload for a variant field + */ + template + OptionalProxy + at(OptionaledVariantField const& of); + /** Set a field. if the field already exists, it is replaced. */ @@ -368,7 +422,7 @@ class STObject : public STBase, public CountedObject setAccountID(SField const& field, AccountID const&); void - setFieldAmount(SField const& field, STAmount const&); + setFieldAmount(SField const& field, STEitherAmount const&); void setFieldIssue(SField const& field, STIssue const&); void @@ -427,6 +481,14 @@ class STObject : public STBase, public CountedObject static std::vector getSortedFields(STObject const& objToSort, WhichFields whichFields); + template + typename T::value_type + atImpl(TypedField const& f) const; + + template + std::optional> + atImpl(OptionaledField const& of) const; + // Implementation for getting (most) fields that return by value. // // The remove_cv and remove_reference are necessitated by the STBitString @@ -473,11 +535,11 @@ class STObject : public STBase, public CountedObject //------------------------------------------------------------------------------ -template +template class STObject::Proxy { protected: - using value_type = typename T::value_type; + using value_type = typename H::value_type; STObject* st_; SOEStyle style_; @@ -498,11 +560,11 @@ class STObject::Proxy assign(U&& u); }; -template -class STObject::ValueProxy : private Proxy +template +class STObject::ValueProxy : private Proxy { private: - using value_type = typename T::value_type; + using value_type = typename H::value_type; public: ValueProxy(ValueProxy const&) = default; @@ -521,11 +583,11 @@ class STObject::ValueProxy : private Proxy ValueProxy(STObject* st, TypedField const* f); }; -template -class STObject::OptionalProxy : private Proxy +template +class STObject::OptionalProxy : private Proxy { private: - using value_type = typename T::value_type; + using value_type = typename H::value_type; using optional_type = std::optional::type>; @@ -657,8 +719,9 @@ class STObject::FieldErr : public std::runtime_error using std::runtime_error::runtime_error; }; -template -STObject::Proxy::Proxy(STObject* st, TypedField const* f) : st_(st), f_(f) +template +STObject::Proxy::Proxy(STObject* st, TypedField const* f) + : st_(st), f_(f) { if (st_->mType) { @@ -674,13 +737,18 @@ STObject::Proxy::Proxy(STObject* st, TypedField const* f) : st_(st), f_(f) } } -template +template auto -STObject::Proxy::value() const -> value_type +STObject::Proxy::value() const -> value_type { auto const t = find(); if (t) - return t->value(); + { + if constexpr (std::is_same_v) + return t->value(); + else + return get(t->value()); + } if (style_ == soeINVALID) { Throw("Value requested from invalid STObject."); @@ -693,17 +761,17 @@ STObject::Proxy::value() const -> value_type return value_type{}; } -template +template inline T const* -STObject::Proxy::find() const +STObject::Proxy::find() const { return dynamic_cast(st_->peekAtPField(*f_)); } -template +template template void -STObject::Proxy::assign(U&& u) +STObject::Proxy::assign(U&& u) { if (style_ == soeDEFAULT && u == value_type{}) { @@ -721,67 +789,68 @@ STObject::Proxy::assign(U&& u) //------------------------------------------------------------------------------ -template +template template -std::enable_if_t, STObject::ValueProxy&> -STObject::ValueProxy::operator=(U&& u) +std::enable_if_t, STObject::ValueProxy&> +STObject::ValueProxy::operator=(U&& u) { this->assign(std::forward(u)); return *this; } -template -STObject::ValueProxy::operator value_type() const +template +STObject::ValueProxy::operator value_type() const { return this->value(); } -template -STObject::ValueProxy::ValueProxy(STObject* st, TypedField const* f) - : Proxy(st, f) +template +STObject::ValueProxy::ValueProxy(STObject* st, TypedField const* f) + : Proxy(st, f) { } //------------------------------------------------------------------------------ -template -STObject::OptionalProxy::operator bool() const noexcept +template +STObject::OptionalProxy::operator bool() const noexcept { return engaged(); } -template +template auto -STObject::OptionalProxy::operator*() const -> value_type +STObject::OptionalProxy::operator*() const -> value_type { return this->value(); } -template -STObject::OptionalProxy::operator typename STObject::OptionalProxy< - T>::optional_type() const +template +STObject::OptionalProxy::operator typename STObject::OptionalProxy:: + optional_type() const { return optional_value(); } -template -typename STObject::OptionalProxy::optional_type -STObject::OptionalProxy::operator~() const +template +typename STObject::OptionalProxy::optional_type +STObject::OptionalProxy::operator~() const { return optional_value(); } -template +template auto -STObject::OptionalProxy::operator=(std::nullopt_t const&) -> OptionalProxy& +STObject::OptionalProxy::operator=(std::nullopt_t const&) + -> OptionalProxy& { disengage(); return *this; } -template +template auto -STObject::OptionalProxy::operator=(optional_type&& v) -> OptionalProxy& +STObject::OptionalProxy::operator=(optional_type&& v) -> OptionalProxy& { if (v) this->assign(std::move(*v)); @@ -790,9 +859,10 @@ STObject::OptionalProxy::operator=(optional_type&& v) -> OptionalProxy& return *this; } -template +template auto -STObject::OptionalProxy::operator=(optional_type const& v) -> OptionalProxy& +STObject::OptionalProxy::operator=(optional_type const& v) + -> OptionalProxy& { if (v) this->assign(*v); @@ -801,31 +871,33 @@ STObject::OptionalProxy::operator=(optional_type const& v) -> OptionalProxy& return *this; } -template +template template -std::enable_if_t, STObject::OptionalProxy&> -STObject::OptionalProxy::operator=(U&& u) +std::enable_if_t, STObject::OptionalProxy&> +STObject::OptionalProxy::operator=(U&& u) { this->assign(std::forward(u)); return *this; } -template -STObject::OptionalProxy::OptionalProxy(STObject* st, TypedField const* f) - : Proxy(st, f) +template +STObject::OptionalProxy::OptionalProxy( + STObject* st, + TypedField const* f) + : Proxy(st, f) { } -template +template bool -STObject::OptionalProxy::engaged() const noexcept +STObject::OptionalProxy::engaged() const noexcept { return this->style_ == soeDEFAULT || this->find() != nullptr; } -template +template void -STObject::OptionalProxy::disengage() +STObject::OptionalProxy::disengage() { if (this->style_ == soeREQUIRED || this->style_ == soeDEFAULT) Throw( @@ -836,18 +908,18 @@ STObject::OptionalProxy::disengage() this->st_->makeFieldAbsent(*this->f_); } -template +template auto -STObject::OptionalProxy::optional_value() const -> optional_type +STObject::OptionalProxy::optional_value() const -> optional_type { if (!engaged()) return std::nullopt; return this->value(); } -template -typename STObject::OptionalProxy::value_type -STObject::OptionalProxy::value_or(value_type val) const +template +typename STObject::OptionalProxy::value_type +STObject::OptionalProxy::value_or(value_type val) const { return engaged() ? this->value() : val; } @@ -959,6 +1031,13 @@ STObject::operator[](TypedField const& f) const return at(f); } +template +typename H::value_type +STObject::operator[](TypedVariantField const& f) const +{ + return at(f); +} + template std::optional> STObject::operator[](OptionaledField const& of) const @@ -966,6 +1045,13 @@ STObject::operator[](OptionaledField const& of) const return at(of); } +template +std::optional> +STObject::operator[](OptionaledVariantField const& of) const +{ + return at(of); +} + template inline auto STObject::operator[](TypedField const& f) -> ValueProxy @@ -973,6 +1059,13 @@ STObject::operator[](TypedField const& f) -> ValueProxy return at(f); } +template +inline auto +STObject::operator[](TypedVariantField const& f) -> ValueProxy +{ + return at(f); +} + template inline auto STObject::operator[](OptionaledField const& of) -> OptionalProxy @@ -980,9 +1073,17 @@ STObject::operator[](OptionaledField const& of) -> OptionalProxy return at(of); } +template +inline auto +STObject::operator[](OptionaledVariantField const& of) + -> OptionalProxy +{ + return at(of); +} + template typename T::value_type -STObject::at(TypedField const& f) const +STObject::atImpl(TypedField const& f) const { auto const b = peekAtPField(f); if (!b) @@ -1007,9 +1108,26 @@ STObject::at(TypedField const& f) const return dv; } +template +typename T::value_type +STObject::at(TypedField const& f) const +{ + return atImpl(f); +} + +template +typename H::value_type +STObject::at(TypedVariantField const& f) const +{ + if constexpr (std::is_same_v) + return atImpl(f); + else + return get(atImpl(f)); +} + template std::optional> -STObject::at(OptionaledField const& of) const +STObject::atImpl(OptionaledField const& of) const { auto const b = peekAtPField(*of.f); if (!b) @@ -1027,6 +1145,23 @@ STObject::at(OptionaledField const& of) const return u->value(); } +template +std::optional> +STObject::at(OptionaledField const& of) const +{ + return atImpl(of); +} + +template +std::optional> +STObject::at(OptionaledVariantField const& of) const +{ + if constexpr (std::is_same_v) + return atImpl(of); + else + return get(atImpl(of)); +} + template inline auto STObject::at(TypedField const& f) -> ValueProxy @@ -1034,6 +1169,13 @@ STObject::at(TypedField const& f) -> ValueProxy return ValueProxy(this, &f); } +template +inline auto +STObject::at(TypedVariantField const& f) -> ValueProxy +{ + return ValueProxy(this, &f); +} + template inline auto STObject::at(OptionaledField const& of) -> OptionalProxy @@ -1041,6 +1183,13 @@ STObject::at(OptionaledField const& of) -> OptionalProxy return OptionalProxy(this, of.f); } +template +inline auto +STObject::at(OptionaledVariantField const& of) -> OptionalProxy +{ + return OptionalProxy(this, of.f); +} + template void STObject::setFieldH160(SField const& field, base_uint<160, Tag> const& v) diff --git a/include/xrpl/protocol/Serializer.h b/include/xrpl/protocol/Serializer.h index b85e8eb013d..dac1cc6132b 100644 --- a/include/xrpl/protocol/Serializer.h +++ b/include/xrpl/protocol/Serializer.h @@ -344,6 +344,10 @@ class SerialIter return static_cast(remain_); } + // peek function, throw on error + unsigned char + peek8(); + // get functions throw on error unsigned char get8(); @@ -373,6 +377,12 @@ class SerialIter return getBitString<160>(); } + uint192 + get192() + { + return getBitString<192>(); + } + uint256 get256() { diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index aae3c7107bd..605e7f6073a 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -125,6 +125,7 @@ enum TEMcodes : TERUnderlyingType { temSEQ_AND_TICKET, temBAD_NFTOKEN_TRANSFER_FEE, + temBAD_MPTOKEN_TRANSFER_FEE, temBAD_AMM_TOKENS, @@ -138,7 +139,7 @@ enum TEMcodes : TERUnderlyingType { temEMPTY_DID, temARRAY_EMPTY, - temARRAY_TOO_LARGE, + temARRAY_TOO_LARGE }; //------------------------------------------------------------------------------ @@ -339,7 +340,11 @@ enum TECcodes : TERUnderlyingType { tecINVALID_UPDATE_TIME = 188, tecTOKEN_PAIR_NOT_FOUND = 189, tecARRAY_EMPTY = 190, - tecARRAY_TOO_LARGE = 191 + tecARRAY_TOO_LARGE = 191, + tecMPTOKEN_EXISTS = 192, + tecMPT_MAX_AMOUNT_EXCEEDED = 193, + tecMPT_LOCKED = 194, + tecMPT_NOT_SUPPORTED = 195, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index ba2b97562db..e364b9ba6e2 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -22,6 +22,8 @@ #include +#include + namespace ripple { /** Transaction flags. @@ -130,6 +132,23 @@ constexpr std::uint32_t const tfOnlyXRP = 0x00000002; constexpr std::uint32_t const tfTrustLine = 0x00000004; constexpr std::uint32_t const tfTransferable = 0x00000008; +// MPTokenIssuanceCreate flags: +// NOTE - there is intentionally no flag here for 0x01 because that +// corresponds to lsfMPTLocked, which this transaction cannot mutate. +constexpr std::uint32_t const tfMPTCanLock = lsfMPTCanLock; +constexpr std::uint32_t const tfMPTRequireAuth = lsfMPTRequireAuth; +constexpr std::uint32_t const tfMPTCanEscrow = lsfMPTCanEscrow; +constexpr std::uint32_t const tfMPTCanTrade = lsfMPTCanTrade; +constexpr std::uint32_t const tfMPTCanTransfer = lsfMPTCanTransfer; +constexpr std::uint32_t const tfMPTCanClawback = lsfMPTCanClawback; + +// MPTokenAuthorize flags: +constexpr std::uint32_t const tfMPTUnauthorize = 0x00000001; + +// MPTokenIssuanceSet flags: +constexpr std::uint32_t const tfMPTLock = 0x00000001; +constexpr std::uint32_t const tfMPTUnlock = 0x00000002; + // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between // accounts allowed a TrustLine to be added to the issuer of that token // without explicit permission from that issuer. This was enabled by @@ -185,6 +204,18 @@ constexpr std::uint32_t tfDepositMask = ~(tfUniversal | tfDepositSubTx); constexpr std::uint32_t tfClearAccountCreateAmount = 0x00010000; constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreateAmount); +// MPTokenIssuanceCreate flags: +constexpr std::uint32_t const tfMPTokenIssuanceCreateMask = + ~(tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback | tfUniversal); + +// MPTokenIssuanceDestroy flags: +constexpr std::uint32_t const tfMPTokenIssuanceDestroyMask = ~tfUniversal; + +// MPTokenAuthorize flags: +constexpr std::uint32_t const tfMPTokenAuthorizeMask = ~(tfMPTUnauthorize | tfUniversal); + +// MPTokenIssuanceSet flags: +constexpr std::uint32_t const tfMPTokenIssuanceSetMask = ~(tfMPTLock | tfMPTUnlock | tfUniversal); // clang-format on } // namespace ripple diff --git a/include/xrpl/protocol/TxFormats.h b/include/xrpl/protocol/TxFormats.h index a3f5cca108c..aa26a4641d0 100644 --- a/include/xrpl/protocol/TxFormats.h +++ b/include/xrpl/protocol/TxFormats.h @@ -199,6 +199,17 @@ enum TxType : std::uint16_t /** This transaction type fixes a problem in the ledger state */ ttLEDGER_STATE_FIX = 53, + /** This transaction creates a new MPTokenIssuance object. */ + ttMPTOKEN_ISSUANCE_CREATE = 54, + + /** This transaction destroys an existing MPTokenIssuance object. */ + ttMPTOKEN_ISSUANCE_DESTROY = 55, + + /** This transaction destroys an existing MPTokenIssuance object. */ + ttMPTOKEN_AUTHORIZE = 56, + + /** This transaction sets an existing MPTokenIssuance or MPToken object. */ + ttMPTOKEN_ISSUANCE_SET = 57, /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/include/xrpl/protocol/TxMeta.h b/include/xrpl/protocol/TxMeta.h index 7932a4c55a3..92114230da7 100644 --- a/include/xrpl/protocol/TxMeta.h +++ b/include/xrpl/protocol/TxMeta.h @@ -108,12 +108,12 @@ class TxMeta } void - setDeliveredAmount(STAmount const& delivered) + setDeliveredAmount(STEitherAmount const& delivered) { mDelivered = delivered; } - STAmount + STEitherAmount getDeliveredAmount() const { assert(hasDeliveredAmount()); @@ -132,7 +132,7 @@ class TxMeta std::uint32_t mIndex; int mResult; - std::optional mDelivered; + std::optional mDelivered; STArray mNodes; }; diff --git a/include/xrpl/protocol/UintTypes.h b/include/xrpl/protocol/UintTypes.h index a0a8069f669..cf34366262f 100644 --- a/include/xrpl/protocol/UintTypes.h +++ b/include/xrpl/protocol/UintTypes.h @@ -58,6 +58,13 @@ using Currency = base_uint<160, detail::CurrencyTag>; /** NodeID is a 160-bit hash representing one node. */ using NodeID = base_uint<160, detail::NodeIDTag>; +/** MPT is a 192-bit hash representing MPTID. + * Currently MPTID is the only 192-bit field. + * If other 192-bit fields with different semantics + * are added then MPTID must change to a unique tag. + */ +using MPTID = base_uint<192>; + /** XRP currency. */ Currency const& xrpCurrency(); @@ -66,6 +73,10 @@ xrpCurrency(); Currency const& noCurrency(); +/** A placeholder for empty MPTID. */ +MPTID const& +noMPT(); + /** We deliberately disallow the currency that looks like "XRP" because too many people were using it instead of the correct XRP currency. */ Currency const& diff --git a/include/xrpl/protocol/XChainAttestations.h b/include/xrpl/protocol/XChainAttestations.h index 721950ca9c1..ea715aa30ab 100644 --- a/include/xrpl/protocol/XChainAttestations.h +++ b/include/xrpl/protocol/XChainAttestations.h @@ -143,7 +143,7 @@ struct AttestationClaim : AttestationBase message( STXChainBridge const& bridge, AccountID const& sendingAccount, - STAmount const& sendingAmount, + STEitherAmount const& sendingAmount, AccountID const& rewardAccount, bool wasLockingChainSend, std::uint64_t claimID, @@ -226,8 +226,8 @@ struct AttestationCreateAccount : AttestationBase message( STXChainBridge const& bridge, AccountID const& sendingAccount, - STAmount const& sendingAmount, - STAmount const& rewardAmount, + STEitherAmount const& sendingAmount, + STEitherAmount const& rewardAmount, AccountID const& rewardAccount, bool wasLockingChainSend, std::uint64_t createCount, diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index e3eda80b44f..f317968eeb7 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -41,116 +41,123 @@ namespace jss { error: Common properties of RPC error responses. */ -JSS(AL_size); // out: GetCounts -JSS(AL_hit_rate); // out: GetCounts -JSS(Account); // in: TransactionSign; field. -JSS(AccountDelete); // transaction type. -JSS(AccountRoot); // ledger type. -JSS(AccountSet); // transaction type. -JSS(AMM); // ledger type -JSS(AMMBid); // transaction type -JSS(AMMID); // field -JSS(AMMCreate); // transaction type -JSS(AMMDeposit); // transaction type -JSS(AMMDelete); // transaction type -JSS(AMMVote); // transaction type -JSS(AMMWithdraw); // transaction type -JSS(Amendments); // ledger type. -JSS(Amount); // in: TransactionSign; field. -JSS(Amount2); // in/out: AMM IOU/XRP pool, deposit, withdraw amount -JSS(Asset); // in: AMM Asset1 -JSS(Asset2); // in: AMM Asset2 -JSS(AssetClass); // in: Oracle -JSS(AssetPrice); // in: Oracle -JSS(AuthAccount); // in: AMM Auction Slot -JSS(AuthAccounts); // in: AMM Auction Slot -JSS(BaseAsset); // in: Oracle -JSS(Bridge); // ledger type. -JSS(Check); // ledger type. -JSS(CheckCancel); // transaction type. -JSS(CheckCash); // transaction type. -JSS(CheckCreate); // transaction type. -JSS(Clawback); // transaction type. -JSS(ClearFlag); // field. -JSS(DID); // ledger type. -JSS(DIDDelete); // transaction type. -JSS(DIDSet); // transaction type. -JSS(DeliverMax); // out: alias to Amount -JSS(DeliverMin); // in: TransactionSign -JSS(DepositPreauth); // transaction and ledger type. -JSS(Destination); // in: TransactionSign; field. -JSS(DirectoryNode); // ledger type. -JSS(EnableAmendment); // transaction type. -JSS(EPrice); // in: AMM Deposit option -JSS(Escrow); // ledger type. -JSS(EscrowCancel); // transaction type. -JSS(EscrowCreate); // transaction type. -JSS(EscrowFinish); // transaction type. -JSS(Fee); // in/out: TransactionSign; field. -JSS(FeeSettings); // ledger type. -JSS(Flags); // in/out: TransactionSign; field. -JSS(Invalid); // -JSS(LastLedgerSequence); // in: TransactionSign; field -JSS(LastUpdateTime); // field. -JSS(LedgerHashes); // ledger type. -JSS(LimitAmount); // field. -JSS(BidMax); // in: AMM Bid -JSS(BidMin); // in: AMM Bid -JSS(NetworkID); // field. -JSS(NFTokenBurn); // transaction type. -JSS(NFTokenMint); // transaction type. -JSS(NFTokenOffer); // ledger type. -JSS(NFTokenAcceptOffer); // transaction type. -JSS(NFTokenCancelOffer); // transaction type. -JSS(NFTokenCreateOffer); // transaction type. -JSS(NFTokenPage); // ledger type. -JSS(LedgerStateFix); // transaction type. -JSS(LPTokenOut); // in: AMM Liquidity Provider deposit tokens -JSS(LPTokenIn); // in: AMM Liquidity Provider withdraw tokens -JSS(LPToken); // out: AMM Liquidity Provider tokens info -JSS(Offer); // ledger type. -JSS(OfferCancel); // transaction type. -JSS(OfferCreate); // transaction type. -JSS(OfferSequence); // field. -JSS(Oracle); // ledger type. -JSS(OracleDelete); // transaction type. -JSS(OracleDocumentID); // field -JSS(OracleSet); // transaction type. -JSS(Owner); // field -JSS(Paths); // in/out: TransactionSign -JSS(PayChannel); // ledger type. -JSS(Payment); // transaction type. -JSS(PaymentChannelClaim); // transaction type. -JSS(PaymentChannelCreate); // transaction type. -JSS(PaymentChannelFund); // transaction type. -JSS(PriceDataSeries); // field. -JSS(PriceData); // field. -JSS(Provider); // field. -JSS(QuoteAsset); // in: Oracle. -JSS(RippleState); // ledger type. -JSS(SLE_hit_rate); // out: GetCounts. -JSS(SetFee); // transaction type. -JSS(UNLModify); // transaction type. -JSS(Scale); // field. -JSS(SettleDelay); // in: TransactionSign -JSS(SendMax); // in: TransactionSign -JSS(Sequence); // in/out: TransactionSign; field. -JSS(SetFlag); // field. -JSS(SetRegularKey); // transaction type. -JSS(SignerList); // ledger type. -JSS(SignerListSet); // transaction type. -JSS(SigningPubKey); // field. -JSS(TakerGets); // field. -JSS(TakerPays); // field. -JSS(Ticket); // ledger type. -JSS(TicketCreate); // transaction type. -JSS(TxnSignature); // field. -JSS(TradingFee); // in/out: AMM trading fee -JSS(TransactionType); // in: TransactionSign. -JSS(TransferRate); // in: TransferRate. -JSS(TrustSet); // transaction type. -JSS(URI); // field. -JSS(VoteSlots); // out: AMM Vote +JSS(AL_size); // out: GetCounts +JSS(AL_hit_rate); // out: GetCounts +JSS(Account); // in: TransactionSign; field. +JSS(AccountDelete); // transaction type. +JSS(AccountRoot); // ledger type. +JSS(AccountSet); // transaction type. +JSS(AMM); // ledger type +JSS(AMMBid); // transaction type +JSS(AMMID); // field +JSS(AMMCreate); // transaction type +JSS(AMMDeposit); // transaction type +JSS(AMMDelete); // transaction type +JSS(AMMVote); // transaction type +JSS(AMMWithdraw); // transaction type +JSS(Amendments); // ledger type. +JSS(Amount); // in: TransactionSign; field. +JSS(Amount2); // in/out: AMM IOU/XRP pool, deposit, withdraw amount +JSS(Asset); // in: AMM Asset1 +JSS(Asset2); // in: AMM Asset2 +JSS(AssetClass); // in: Oracle +JSS(AssetPrice); // in: Oracle +JSS(AuthAccount); // in: AMM Auction Slot +JSS(AuthAccounts); // in: AMM Auction Slot +JSS(BaseAsset); // in: Oracle +JSS(Bridge); // ledger type. +JSS(Check); // ledger type. +JSS(CheckCancel); // transaction type. +JSS(CheckCash); // transaction type. +JSS(CheckCreate); // transaction type. +JSS(Clawback); // transaction type. +JSS(ClearFlag); // field. +JSS(DID); // ledger type. +JSS(DIDDelete); // transaction type. +JSS(DIDSet); // transaction type. +JSS(DeliverMax); // out: alias to Amount +JSS(DeliverMin); // in: TransactionSign +JSS(DepositPreauth); // transaction and ledger type. +JSS(Destination); // in: TransactionSign; field. +JSS(DirectoryNode); // ledger type. +JSS(EnableAmendment); // transaction type. +JSS(EPrice); // in: AMM Deposit option +JSS(Escrow); // ledger type. +JSS(EscrowCancel); // transaction type. +JSS(EscrowCreate); // transaction type. +JSS(EscrowFinish); // transaction type. +JSS(Fee); // in/out: TransactionSign; field. +JSS(FeeSettings); // ledger type. +JSS(Flags); // in/out: TransactionSign; field. +JSS(Invalid); // +JSS(LastLedgerSequence); // in: TransactionSign; field +JSS(LastUpdateTime); // field. +JSS(LedgerHashes); // ledger type. +JSS(LimitAmount); // field. +JSS(BidMax); // in: AMM Bid +JSS(BidMin); // in: AMM Bid +JSS(MPToken); // ledger type. +JSS(MPTokenIssuance); // ledger type. +JSS(MPTokenIssuanceCreate); // transaction type. +JSS(MPTokenIssuanceDestroy); // transaction type. +JSS(MPTokenAuthorize); // transaction type. +JSS(MPTokenIssuanceSet); // transaction type. +JSS(MPTokenIssuanceID); // in: MPTokenIssuanceDestroy, MPTokenAuthorize +JSS(NetworkID); // field. +JSS(NFTokenBurn); // transaction type. +JSS(NFTokenMint); // transaction type. +JSS(NFTokenOffer); // ledger type. +JSS(NFTokenAcceptOffer); // transaction type. +JSS(NFTokenCancelOffer); // transaction type. +JSS(NFTokenCreateOffer); // transaction type. +JSS(NFTokenPage); // ledger type. +JSS(LedgerStateFix); // transaction type. +JSS(LPTokenOut); // in: AMM Liquidity Provider deposit tokens +JSS(LPTokenIn); // in: AMM Liquidity Provider withdraw tokens +JSS(LPToken); // out: AMM Liquidity Provider tokens info +JSS(Offer); // ledger type. +JSS(OfferCancel); // transaction type. +JSS(OfferCreate); // transaction type. +JSS(OfferSequence); // field. +JSS(Oracle); // ledger type. +JSS(OracleDelete); // transaction type. +JSS(OracleDocumentID); // field +JSS(OracleSet); // transaction type. +JSS(Owner); // field +JSS(Paths); // in/out: TransactionSign +JSS(PayChannel); // ledger type. +JSS(Payment); // transaction type. +JSS(PaymentChannelClaim); // transaction type. +JSS(PaymentChannelCreate); // transaction type. +JSS(PaymentChannelFund); // transaction type. +JSS(PriceDataSeries); // field. +JSS(PriceData); // field. +JSS(Provider); // field. +JSS(QuoteAsset); // in: Oracle. +JSS(RippleState); // ledger type. +JSS(SLE_hit_rate); // out: GetCounts. +JSS(SetFee); // transaction type. +JSS(UNLModify); // transaction type. +JSS(Scale); // field. +JSS(SettleDelay); // in: TransactionSign +JSS(SendMax); // in: TransactionSign +JSS(Sequence); // in/out: TransactionSign; field. +JSS(SetFlag); // field. +JSS(SetRegularKey); // transaction type. +JSS(SignerList); // ledger type. +JSS(SignerListSet); // transaction type. +JSS(SigningPubKey); // field. +JSS(TakerGets); // field. +JSS(TakerPays); // field. +JSS(Ticket); // ledger type. +JSS(TicketCreate); // transaction type. +JSS(TxnSignature); // field. +JSS(TradingFee); // in/out: AMM trading fee +JSS(TransactionType); // in: TransactionSign. +JSS(TransferRate); // in: TransferRate. +JSS(TrustSet); // transaction type. +JSS(URI); // field. +JSS(VoteSlots); // out: AMM Vote JSS(XChainAddAccountCreateAttestation); // transaction type. JSS(XChainAddClaimAttestation); // transaction type. JSS(XChainAccountCreateCommit); // transaction type. @@ -236,6 +243,11 @@ JSS(build_path); // in: TransactionSign JSS(build_version); // out: NetworkOPs JSS(cancel_after); // out: AccountChannels JSS(can_delete); // out: CanDelete +JSS(mpt_amount); // out: mpt_holders +JSS(mpt_issuance); // in: LedgerEntry, AccountObjects +JSS(mpt_issuance_id); // in: Payment, mpt_holders +JSS(mptoken); // in: LedgerEntry, AccountObjects +JSS(mptoken_index); // out: mpt_holders JSS(changes); // out: BookChanges JSS(channel_id); // out: AccountChannels JSS(channels); // out: AccountChannels @@ -363,6 +375,7 @@ JSS(high); // out: BookChanges JSS(highest_sequence); // out: AccountInfo JSS(highest_ticket); // out: AccountInfo JSS(historical_perminute); // historical_perminute. +JSS(holders); // out: MPTHolders JSS(hostid); // out: NetworkOPs JSS(hotwallet); // in: GatewayBalances JSS(id); // websocket. @@ -448,6 +461,7 @@ JSS(load_fee); // out: LoadFeeTrackImp, NetworkOPs JSS(local); // out: resource/Logic.h JSS(local_txs); // out: GetCounts JSS(local_static_keys); // out: ValidatorList +JSS(locked_amount); // out: MPTHolders JSS(low); // out: BookChanges JSS(lowest_sequence); // out: AccountInfo JSS(lowest_ticket); // out: AccountInfo diff --git a/src/libxrpl/basics/MPTAmount.cpp b/src/libxrpl/basics/MPTAmount.cpp new file mode 100644 index 00000000000..6c9a50e4730 --- /dev/null +++ b/src/libxrpl/basics/MPTAmount.cpp @@ -0,0 +1,85 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { + +MPTAmount& +MPTAmount::operator+=(MPTAmount const& other) +{ + value_ += other.value(); + return *this; +} + +MPTAmount& +MPTAmount::operator-=(MPTAmount const& other) +{ + value_ -= other.value(); + return *this; +} + +MPTAmount +MPTAmount::operator-() const +{ + return MPTAmount{-value_}; +} + +bool +MPTAmount::operator==(MPTAmount const& other) const +{ + return value_ == other.value_; +} + +bool +MPTAmount::operator==(value_type other) const +{ + return value_ == other; +} + +bool +MPTAmount::operator<(MPTAmount const& other) const +{ + return value_ < other.value_; +} + +Json::Value +MPTAmount::jsonClipped() const +{ + static_assert( + std::is_signed_v && std::is_integral_v, + "Expected MPTAmount to be a signed integral type"); + + constexpr auto min = std::numeric_limits::min(); + constexpr auto max = std::numeric_limits::max(); + + if (value_ < min) + return min; + if (value_ > max) + return max; + return static_cast(value_); +} + +MPTAmount +MPTAmount::minPositiveAmount() +{ + return MPTAmount{1}; +} + +} // namespace ripple diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 14260b653a2..ebbfa0023c9 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -504,6 +504,11 @@ Number::operator XRPAmount() const return XRPAmount{static_cast(*this)}; } +Number::operator MPTAmount() const +{ + return MPTAmount{static_cast(*this)}; +} + std::string to_string(Number const& amount) { diff --git a/src/libxrpl/protocol/Feature.cpp b/src/libxrpl/protocol/Feature.cpp index 078369bf20c..4b37633b408 100644 --- a/src/libxrpl/protocol/Feature.cpp +++ b/src/libxrpl/protocol/Feature.cpp @@ -501,6 +501,7 @@ REGISTER_FIX (fixNFTokenPageLinks, Supported::yes, VoteBehavior::De // InvariantsV1_1 will be changes to Supported::yes when all the // invariants expected to be included under it are complete. REGISTER_FEATURE(InvariantsV1_1, Supported::no, VoteBehavior::DefaultNo); +REGISTER_FEATURE(MPTokensV1, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 30d97416cfa..8014d8d4dcd 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -73,6 +73,8 @@ enum class LedgerNameSpace : std::uint16_t { XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K', DID = 'I', ORACLE = 'R', + MPTOKEN_ISSUANCE = '~', + MPTOKEN = 't', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -135,6 +137,16 @@ getTicketIndex(AccountID const& account, SeqProxy ticketSeq) return getTicketIndex(account, ticketSeq.value()); } +MPTID +getMptID(AccountID const& account, std::uint32_t sequence) +{ + MPTID u; + sequence = boost::endian::native_to_big(sequence); + memcpy(u.data(), &sequence, sizeof(sequence)); + memcpy(u.data() + sizeof(sequence), account.data(), sizeof(account)); + return u; +} + //------------------------------------------------------------------------------ namespace keylet { @@ -451,6 +463,31 @@ oracle(AccountID const& account, std::uint32_t const& documentID) noexcept return {ltORACLE, indexHash(LedgerNameSpace::ORACLE, account, documentID)}; } +Keylet +mptIssuance(AccountID const& issuer, std::uint32_t seq) noexcept +{ + return mptIssuance(getMptID(issuer, seq)); +} + +Keylet +mptIssuance(MPTID const& id) noexcept +{ + return { + ltMPTOKEN_ISSUANCE, indexHash(LedgerNameSpace::MPTOKEN_ISSUANCE, id)}; +} + +Keylet +mptoken(MPTID const& issuanceID, AccountID const& holder) noexcept +{ + return mptoken(mptIssuance(issuanceID).key, holder); +} + +Keylet +mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept +{ + return { + ltMPTOKEN, indexHash(LedgerNameSpace::MPTOKEN, issuanceKey, holder)}; +} } // namespace keylet } // namespace ripple diff --git a/src/libxrpl/protocol/Issue.cpp b/src/libxrpl/protocol/Issue.cpp index 70d2c013d7b..d860e7062bc 100644 --- a/src/libxrpl/protocol/Issue.cpp +++ b/src/libxrpl/protocol/Issue.cpp @@ -26,6 +26,12 @@ namespace ripple { +AccountID const& +Issue::getIssuer() const +{ + return account; +} + std::string Issue::getText() const { diff --git a/src/libxrpl/protocol/LedgerFormats.cpp b/src/libxrpl/protocol/LedgerFormats.cpp index 9401c00278b..71f04cd8d10 100644 --- a/src/libxrpl/protocol/LedgerFormats.cpp +++ b/src/libxrpl/protocol/LedgerFormats.cpp @@ -364,6 +364,36 @@ LedgerFormats::LedgerFormats() {sfPreviousTxnLgrSeq, soeREQUIRED} }, commonFields); + + add(jss::MPTokenIssuance, + ltMPTOKEN_ISSUANCE, + { + {sfIssuer, soeREQUIRED}, + {sfSequence, soeREQUIRED}, + {sfTransferFee, soeDEFAULT}, + {sfOwnerNode, soeREQUIRED}, + {sfAssetScale, soeDEFAULT}, + {sfMaximumAmount, soeOPTIONAL}, + {sfOutstandingAmount, soeREQUIRED}, + {sfLockedAmount, soeDEFAULT}, + {sfMPTokenMetadata, soeOPTIONAL}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED} + }, + commonFields); + + add(jss::MPToken, + ltMPTOKEN, + { + {sfAccount, soeREQUIRED}, + {sfMPTokenIssuanceID, soeREQUIRED}, + {sfMPTAmount, soeDEFAULT}, + {sfLockedAmount, soeDEFAULT}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED} + }, + commonFields); // clang-format on } diff --git a/src/libxrpl/protocol/MPTIssue.cpp b/src/libxrpl/protocol/MPTIssue.cpp new file mode 100644 index 00000000000..629f00be2d2 --- /dev/null +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -0,0 +1,57 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include + +namespace ripple { + +MPTIssue::MPTIssue(MPTID const& id) : mptID_(id) +{ +} + +AccountID +MPTIssue::getIssuer() const +{ + AccountID account; + + // copy from id skipping the sequence + memcpy( + account.data(), + mptID_.data() + sizeof(std::uint32_t), + sizeof(AccountID)); + return account; +} + +MPTID const& +MPTIssue::getMptID() const +{ + return mptID_; +} + +Json::Value +to_json(MPTIssue const& issue) +{ + Json::Value jv; + jv[jss::mpt_issuance_id] = to_string(issue.getMptID()); + return jv; +} + +} // namespace ripple diff --git a/src/libxrpl/protocol/Rate2.cpp b/src/libxrpl/protocol/Rate2.cpp index d85a49a5958..ecf6de2cb77 100644 --- a/src/libxrpl/protocol/Rate2.cpp +++ b/src/libxrpl/protocol/Rate2.cpp @@ -54,6 +54,19 @@ multiply(STAmount const& amount, Rate const& rate) return multiply(amount, detail::as_amount(rate), amount.issue()); } +STMPTAmount +multiply(STMPTAmount const& amount, Rate const& rate) +{ + assert(rate.value != 0); + + if (rate == parityRate) + return amount; + + return STMPTAmount{ + amount.issue(), + static_cast(amount.value() * Number{rate.value, -9})}; +} + STAmount multiplyRound(STAmount const& amount, Rate const& rate, bool roundUp) { diff --git a/src/libxrpl/protocol/SField.cpp b/src/libxrpl/protocol/SField.cpp index f8eb2d6f877..21431cad5d7 100644 --- a/src/libxrpl/protocol/SField.cpp +++ b/src/libxrpl/protocol/SField.cpp @@ -45,6 +45,15 @@ TypedField::TypedField(private_access_tag_t pat, Args&&... args) { } +template +template +TypedVariantField::TypedVariantField( + SField::private_access_tag_t pat, + Args&&... args) + : TypedField(pat, std::forward(args)...) +{ +} + // Construct all compile-time SFields, and register them in the knownCodeToField // database: @@ -98,6 +107,7 @@ CONSTRUCT_TYPED_SFIELD(sfTickSize, "TickSize", UINT8, CONSTRUCT_TYPED_SFIELD(sfUNLModifyDisabling, "UNLModifyDisabling", UINT8, 17); CONSTRUCT_TYPED_SFIELD(sfHookResult, "HookResult", UINT8, 18); CONSTRUCT_TYPED_SFIELD(sfWasLockingChainSend, "WasLockingChainSend", UINT8, 19); +CONSTRUCT_TYPED_SFIELD(sfAssetScale, "AssetScale", UINT8, 20); // 16-bit integers CONSTRUCT_TYPED_SFIELD(sfLedgerEntryType, "LedgerEntryType", UINT16, 1, SField::sMD_Never); @@ -193,6 +203,10 @@ CONSTRUCT_TYPED_SFIELD(sfXChainClaimID, "XChainClaimID", U CONSTRUCT_TYPED_SFIELD(sfXChainAccountCreateCount, "XChainAccountCreateCount", UINT64, 21); CONSTRUCT_TYPED_SFIELD(sfXChainAccountClaimCount, "XChainAccountClaimCount", UINT64, 22); CONSTRUCT_TYPED_SFIELD(sfAssetPrice, "AssetPrice", UINT64, 23); +CONSTRUCT_TYPED_SFIELD(sfMaximumAmount, "MaximumAmount", UINT64, 24); +CONSTRUCT_TYPED_SFIELD(sfOutstandingAmount, "OutstandingAmount", UINT64, 25); +CONSTRUCT_TYPED_SFIELD(sfLockedAmount, "LockedAmount", UINT64, 26); +CONSTRUCT_TYPED_SFIELD(sfMPTAmount, "MPTAmount", UINT64, 27); // 128-bit CONSTRUCT_TYPED_SFIELD(sfEmailHash, "EmailHash", UINT128, 1); @@ -203,6 +217,9 @@ CONSTRUCT_TYPED_SFIELD(sfTakerPaysIssuer, "TakerPaysIssuer", UINT160, CONSTRUCT_TYPED_SFIELD(sfTakerGetsCurrency, "TakerGetsCurrency", UINT160, 3); CONSTRUCT_TYPED_SFIELD(sfTakerGetsIssuer, "TakerGetsIssuer", UINT160, 4); +// 192-bit (common) +CONSTRUCT_TYPED_SFIELD(sfMPTokenIssuanceID, "MPTokenIssuanceID", UINT192, 1); + // 256-bit (common) CONSTRUCT_TYPED_SFIELD(sfLedgerHash, "LedgerHash", UINT256, 1); CONSTRUCT_TYPED_SFIELD(sfParentHash, "ParentHash", UINT256, 2); @@ -240,7 +257,7 @@ CONSTRUCT_TYPED_SFIELD(sfHookNamespace, "HookNamespace", UINT256, CONSTRUCT_TYPED_SFIELD(sfHookSetTxnID, "HookSetTxnID", UINT256, 33); // currency amount (common) -CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", AMOUNT, 1); +CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", EITHER_AMOUNT, 1); CONSTRUCT_TYPED_SFIELD(sfBalance, "Balance", AMOUNT, 2); CONSTRUCT_TYPED_SFIELD(sfLimitAmount, "LimitAmount", AMOUNT, 3); CONSTRUCT_TYPED_SFIELD(sfTakerPays, "TakerPays", AMOUNT, 4); @@ -307,6 +324,7 @@ CONSTRUCT_TYPED_SFIELD(sfDIDDocument, "DIDDocument", VL, CONSTRUCT_TYPED_SFIELD(sfData, "Data", VL, 27); CONSTRUCT_TYPED_SFIELD(sfAssetClass, "AssetClass", VL, 28); CONSTRUCT_TYPED_SFIELD(sfProvider, "Provider", VL, 29); +CONSTRUCT_TYPED_SFIELD(sfMPTokenMetadata, "MPTokenMetadata", VL, 30); // account CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1); @@ -319,6 +337,7 @@ CONSTRUCT_TYPED_SFIELD(sfUnauthorize, "Unauthorize", ACCOUNT, CONSTRUCT_TYPED_SFIELD(sfRegularKey, "RegularKey", ACCOUNT, 8); CONSTRUCT_TYPED_SFIELD(sfNFTokenMinter, "NFTokenMinter", ACCOUNT, 9); CONSTRUCT_TYPED_SFIELD(sfEmitCallback, "EmitCallback", ACCOUNT, 10); +CONSTRUCT_TYPED_SFIELD(sfMPTokenHolder, "MPTokenHolder", ACCOUNT, 11); // account (uncommon) CONSTRUCT_TYPED_SFIELD(sfHookAccount, "HookAccount", ACCOUNT, 16); diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 236603d6cb8..99eb4b829c5 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -85,10 +85,9 @@ areComparable(STAmount const& v1, STAmount const& v2) v1.issue().currency == v2.issue().currency; } -STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name) +STAmount::STAmount(SerialIter& sit) { std::uint64_t value = sit.get64(); - // native if ((value & cNotNative) == 0) { @@ -158,23 +157,6 @@ STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name) canonicalize(); } -STAmount::STAmount( - SField const& name, - Issue const& issue, - mantissa_type mantissa, - exponent_type exponent, - bool native, - bool negative, - unchecked) - : STBase(name) - , mIssue(issue) - , mValue(mantissa) - , mOffset(exponent) - , mIsNative(native) - , mIsNegative(negative) -{ -} - STAmount::STAmount( Issue const& issue, mantissa_type mantissa, @@ -191,14 +173,12 @@ STAmount::STAmount( } STAmount::STAmount( - SField const& name, Issue const& issue, mantissa_type mantissa, exponent_type exponent, bool native, bool negative) - : STBase(name) - , mIssue(issue) + : mIssue(issue) , mValue(mantissa) , mOffset(exponent) , mIsNative(native) @@ -207,49 +187,6 @@ STAmount::STAmount( canonicalize(); } -STAmount::STAmount(SField const& name, std::int64_t mantissa) - : STBase(name), mOffset(0), mIsNative(true) -{ - set(mantissa); -} - -STAmount::STAmount(SField const& name, std::uint64_t mantissa, bool negative) - : STBase(name) - , mValue(mantissa) - , mOffset(0) - , mIsNative(true) - , mIsNegative(negative) -{ - assert(mValue <= std::numeric_limits::max()); -} - -STAmount::STAmount( - SField const& name, - Issue const& issue, - std::uint64_t mantissa, - int exponent, - bool negative) - : STBase(name) - , mIssue(issue) - , mValue(mantissa) - , mOffset(exponent) - , mIsNegative(negative) -{ - assert(mValue <= std::numeric_limits::max()); - canonicalize(); -} - -STAmount::STAmount(SField const& name, STAmount const& from) - : STBase(name) - , mIssue(from.mIssue) - , mValue(from.mValue) - , mOffset(from.mOffset) - , mIsNegative(from.mIsNegative) -{ - assert(mValue <= std::numeric_limits::max()); - canonicalize(); -} - //------------------------------------------------------------------------------ STAmount::STAmount(std::uint64_t mantissa, bool negative) @@ -319,21 +256,9 @@ STAmount::STAmount(XRPAmount const& amount) } std::unique_ptr -STAmount::construct(SerialIter& sit, SField const& name) -{ - return std::make_unique(sit, name); -} - -STBase* -STAmount::copy(std::size_t n, void* buf) const +STAmount::construct(SerialIter& sit) { - return emplace(n, buf, *this); -} - -STBase* -STAmount::move(std::size_t n, void* buf) -{ - return emplace(n, buf, std::move(*this)); + return std::make_unique(sit); } //------------------------------------------------------------------------------ @@ -416,16 +341,16 @@ operator+(STAmount const& v1, STAmount const& v2) if (v1 == beast::zero) { // Result must be in terms of v1 currency and issuer. - return { - v1.getFName(), - v1.issue(), - v2.mantissa(), - v2.exponent(), - v2.negative()}; + return {v1.issue(), v2.mantissa(), v2.exponent(), v2.negative()}; } if (v1.native()) - return {v1.getFName(), getSNValue(v1) + getSNValue(v2)}; + { + auto const res = getSNValue(v1) + getSNValue(v2); + auto const negative = res < 0; + return STAmount{ + static_cast(negative ? -res : res), negative}; + } if (getSTNumberSwitchover()) { @@ -462,18 +387,12 @@ operator+(STAmount const& v1, STAmount const& v2) std::int64_t fv = vv1 + vv2; if ((fv >= -10) && (fv <= 10)) - return {v1.getFName(), v1.issue()}; + return {v1.issue()}; if (fv >= 0) - return STAmount{ - v1.getFName(), - v1.issue(), - static_cast(fv), - ov1, - false}; - - return STAmount{ - v1.getFName(), v1.issue(), static_cast(-fv), ov1, true}; + return STAmount{v1.issue(), static_cast(fv), ov1, false}; + + return STAmount{v1.issue(), static_cast(-fv), ov1, true}; } STAmount @@ -543,12 +462,6 @@ STAmount::setJson(Json::Value& elem) const } } -//------------------------------------------------------------------------------ -// -// STBase -// -//------------------------------------------------------------------------------ - SerializedTypeID STAmount::getSType() const { @@ -688,13 +601,6 @@ STAmount::add(Serializer& s) const } } -bool -STAmount::isEquivalent(const STBase& t) const -{ - const STAmount* v = dynamic_cast(&t); - return v && (*v == *this); -} - bool STAmount::isDefault() const { @@ -920,128 +826,7 @@ amountFromString(Issue const& issue, std::string const& amount) exponent += beast::lexicalCastThrow(std::string(match[7])); } - return {issue, mantissa, exponent, negative}; -} - -STAmount -amountFromJson(SField const& name, Json::Value const& v) -{ - STAmount::mantissa_type mantissa = 0; - STAmount::exponent_type exponent = 0; - bool negative = false; - Issue issue; - - Json::Value value; - Json::Value currency; - Json::Value issuer; - - if (v.isNull()) - { - Throw( - "XRP may not be specified with a null Json value"); - } - else if (v.isObject()) - { - value = v[jss::value]; - currency = v[jss::currency]; - issuer = v[jss::issuer]; - } - else if (v.isArray()) - { - value = v.get(Json::UInt(0), 0); - currency = v.get(Json::UInt(1), Json::nullValue); - issuer = v.get(Json::UInt(2), Json::nullValue); - } - else if (v.isString()) - { - std::string val = v.asString(); - std::vector elements; - boost::split(elements, val, boost::is_any_of("\t\n\r ,/")); - - if (elements.size() > 3) - Throw("invalid amount string"); - - value = elements[0]; - - if (elements.size() > 1) - currency = elements[1]; - - if (elements.size() > 2) - issuer = elements[2]; - } - else - { - value = v; - } - - bool const native = !currency.isString() || currency.asString().empty() || - (currency.asString() == systemCurrencyCode()); - - if (native) - { - if (v.isObjectOrNull()) - Throw("XRP may not be specified as an object"); - issue = xrpIssue(); - } - else - { - // non-XRP - if (!to_currency(issue.currency, currency.asString())) - Throw("invalid currency"); - - if (!issuer.isString() || !to_issuer(issue.account, issuer.asString())) - Throw("invalid issuer"); - - if (isXRP(issue.currency)) - Throw("invalid issuer"); - } - - if (value.isInt()) - { - if (value.asInt() >= 0) - { - mantissa = value.asInt(); - } - else - { - mantissa = -value.asInt(); - negative = true; - } - } - else if (value.isUInt()) - { - mantissa = v.asUInt(); - } - else if (value.isString()) - { - auto const ret = amountFromString(issue, value.asString()); - - mantissa = ret.mantissa(); - exponent = ret.exponent(); - negative = ret.negative(); - } - else - { - Throw("invalid amount type"); - } - - return {name, issue, mantissa, exponent, native, negative}; -} - -bool -amountFromJsonNoThrow(STAmount& result, Json::Value const& jvSource) -{ - try - { - result = amountFromJson(sfGeneric, jvSource); - return true; - } - catch (const std::exception& e) - { - JLOG(debugLog().warn()) - << "amountFromJsonNoThrow: caught: " << e.what(); - } - return false; + return STAmount{issue, mantissa, exponent, negative}; } //------------------------------------------------------------------------------ @@ -1098,7 +883,6 @@ operator-(STAmount const& value) if (value.mantissa() == 0) return value; return STAmount( - value.getFName(), value.issue(), value.mantissa(), value.exponent(), @@ -1224,7 +1008,7 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) if (((maxV >> 32) * minV) > 2095475792ull) // cMaxNative / 2^32 Throw("Native value overflow"); - return STAmount(v1.getFName(), minV * maxV); + return STAmount(minV * maxV); } if (getSTNumberSwitchover()) @@ -1416,7 +1200,7 @@ mulRoundImpl( if (((maxV >> 32) * minV) > 2095475792ull) // cMaxNative / 2^32 Throw("Native value overflow"); - return STAmount(v1.getFName(), minV * maxV); + return STAmount(minV * maxV); } std::uint64_t value1 = v1.mantissa(), value2 = v2.mantissa(); @@ -1610,4 +1394,11 @@ divRoundStrict( return divRoundImpl(num, den, issue, roundUp); } +std::ostream& +operator<<(std::ostream& out, const STAmount& t) +{ + out << t.getFullText(); + return out; +} + } // namespace ripple diff --git a/src/libxrpl/protocol/STEitherAmount.cpp b/src/libxrpl/protocol/STEitherAmount.cpp new file mode 100644 index 00000000000..2fc4d3f00de --- /dev/null +++ b/src/libxrpl/protocol/STEitherAmount.cpp @@ -0,0 +1,425 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include + +#include + +namespace ripple { + +STEitherAmount::STEitherAmount(SerialIter& sit, SField const& name) + : STBase(name) +{ + auto const u8 = sit.peek8(); + if (((static_cast(u8) << 56) & STAmount::cNotNative) == 0 && + (u8 & STMPTAmount::cMPToken) != 0) + amount_.emplace(sit); + else + amount_.emplace(sit); +} + +STEitherAmount::STEitherAmount(XRPAmount const& amount) : amount_{amount} +{ +} + +STEitherAmount::STEitherAmount(STAmount const& amount) : amount_{amount} +{ +} + +STEitherAmount::STEitherAmount(SField const& name, STAmount const& amount) + : STBase(name), amount_{amount} +{ +} + +STEitherAmount::STEitherAmount(SField const& name, STMPTAmount const& amount) + : STBase(name), amount_{amount} +{ +} + +STEitherAmount::STEitherAmount(STMPTAmount const& amount) : amount_{amount} +{ +} + +STEitherAmount& +STEitherAmount::operator=(STAmount const& amount) +{ + amount_ = amount; + return *this; +} + +STEitherAmount& +STEitherAmount::operator=(STMPTAmount const& amount) +{ + amount_ = amount; + return *this; +} + +STEitherAmount& +STEitherAmount::operator=(XRPAmount const& amount) +{ + amount_ = amount; + return *this; +} + +SerializedTypeID +STEitherAmount::getSType() const +{ + return STI_AMOUNT; +} + +std::string +STEitherAmount::getFullText() const +{ + return std::visit([&](auto&& a) { return a.getFullText(); }, amount_); +} + +std::string +STEitherAmount::getText() const +{ + return std::visit([&](auto&& a) { return a.getText(); }, amount_); +} + +Json::Value STEitherAmount::getJson(JsonOptions) const +{ + return std::visit( + [&](auto&& a) { return a.getJson(JsonOptions::none); }, amount_); +} + +void +STEitherAmount::setJson(Json::Value& jv) const +{ + std::visit([&](auto&& a) { a.setJson(jv); }, amount_); +} + +void +STEitherAmount::add(Serializer& s) const +{ + std::visit([&](auto&& a) { a.add(s); }, amount_); +} + +bool +STEitherAmount::isEquivalent(const STBase& t) const +{ + const STEitherAmount* v = dynamic_cast(&t); + return v && *this == *v; +} + +bool +STEitherAmount::isDefault() const +{ + return std::visit([&](auto&& a) { return a.isDefault(); }, amount_); +} +//------------------------------------------------------------------------------ +bool +STEitherAmount::isMPT() const +{ + return std::holds_alternative(amount_); +} + +bool +STEitherAmount::isIssue() const +{ + return std::holds_alternative(amount_); +} + +bool +STEitherAmount::negative() const +{ + if (isIssue()) + return std::get(amount_).negative(); + return false; +} + +bool +STEitherAmount::native() const +{ + if (isIssue()) + return std::get(amount_).native(); + return false; +} + +STEitherAmount +STEitherAmount::zeroed() const +{ + return std::visit( + [&](auto&& a) { return STEitherAmount{a.zeroed()}; }, amount_); +} + +STEitherAmount const& +STEitherAmount::value() const +{ + return *this; +} + +std::variant const& +STEitherAmount::getValue() const +{ + return amount_; +} + +std::variant& +STEitherAmount::getValue() +{ + return amount_; +} + +AccountID +STEitherAmount::getIssuer() const +{ + if (isIssue()) + return get().getIssuer(); + return get().getIssuer(); +} + +STBase* +STEitherAmount::copy(std::size_t n, void* buf) const +{ + return emplace(n, buf, *this); +} + +STBase* +STEitherAmount::move(std::size_t n, void* buf) +{ + return emplace(n, buf, std::move(*this)); +} + +int +STEitherAmount::signum() const noexcept +{ + return std::visit([&](auto&& a) { return a.signum(); }, amount_); +} + +static bool +validJSONIssue(Json::Value const& jv) +{ + return (jv.isMember(jss::currency) && !jv.isMember(jss::mpt_issuance_id)) || + (!jv.isMember(jss::currency) && !jv.isMember(jss::issuer) && + jv.isMember(jss::mpt_issuance_id)); +} + +namespace detail { + +static STEitherAmount +amountFromJson(SField const& name, Json::Value const& v) +{ + STAmount::mantissa_type mantissa = 0; + STAmount::exponent_type exponent = 0; + bool negative = false; + std::variant issue; + + Json::Value value; + Json::Value currencyOrMPTID; + Json::Value issuer; + bool isMPT = false; + + if (v.isNull()) + { + Throw( + "XRP may not be specified with a null Json value"); + } + else if (v.isObject()) + { + if (!validJSONIssue(v)) + Throw("Invalid Issue's Json specification"); + + value = v[jss::value]; + if (v.isMember(jss::mpt_issuance_id)) + { + isMPT = true; + currencyOrMPTID = v[jss::mpt_issuance_id]; + } + else + { + currencyOrMPTID = v[jss::currency]; + issuer = v[jss::issuer]; + } + } + else if (v.isArray()) + { + value = v.get(Json::UInt(0), 0); + currencyOrMPTID = v.get(Json::UInt(1), Json::nullValue); + issuer = v.get(Json::UInt(2), Json::nullValue); + } + else if (v.isString()) + { + std::string val = v.asString(); + std::vector elements; + boost::split(elements, val, boost::is_any_of("\t\n\r ,/")); + + if (elements.size() > 3) + Throw("invalid amount string"); + + value = elements[0]; + + if (elements.size() > 1) + currencyOrMPTID = elements[1]; + + if (elements.size() > 2) + issuer = elements[2]; + } + else + { + value = v; + } + + bool const native = !currencyOrMPTID.isString() || + currencyOrMPTID.asString().empty() || + (currencyOrMPTID.asString() == systemCurrencyCode()); + + if (native) + { + if (v.isObjectOrNull()) + Throw("XRP may not be specified as an object"); + issue = xrpIssue(); + } + else + { + if (isMPT) + { + // sequence (32 bits) + account (160 bits) + MPTID u; + if (!u.parseHex(currencyOrMPTID.asString())) + Throw("invalid MPTokenIssuanceID"); + issue = u; + } + else + { + issue = Issue{}; + if (!to_currency( + std::get(issue).currency, + currencyOrMPTID.asString())) + Throw("invalid currency"); + if (!issuer.isString() || + !to_issuer(std::get(issue).account, issuer.asString())) + Throw("invalid issuer"); + if (isXRP(std::get(issue))) + Throw("invalid issuer"); + } + } + + if (value.isInt()) + { + if (value.asInt() >= 0) + { + mantissa = value.asInt(); + } + else + { + mantissa = -value.asInt(); + negative = true; + } + } + else if (value.isUInt()) + { + mantissa = v.asUInt(); + } + else if (value.isString()) + { + if (std::holds_alternative(issue)) + { + STAmount const ret = + amountFromString(std::get(issue), value.asString()); + mantissa = ret.mantissa(); + exponent = ret.exponent(); + negative = ret.negative(); + } + else + { + STMPTAmount const ret = + amountFromString(std::get(issue), value.asString()); + negative = ret.value() < 0; + mantissa = !negative ? ret.value() : -ret.value(); + exponent = 0; + } + } + else + { + Throw("invalid amount type"); + } + + if (std::holds_alternative(issue)) + return STEitherAmount{ + name, + STAmount{ + std::get(issue), mantissa, exponent, native, negative}}; + while (exponent-- > 0) + mantissa *= 10; + if (mantissa > maxMPTokenAmount) + Throw("MPT amount out of range"); + return STEitherAmount{ + name, STMPTAmount{std::get(issue), mantissa, negative}}; +} + +} // namespace detail + +STEitherAmount +amountFromJson(SField const& name, Json::Value const& v) +{ + return detail::amountFromJson(name, v); +} + +STAmount +amountFromJson(SF_AMOUNT const& name, Json::Value const& v) +{ + auto res = detail::amountFromJson(name, v); + if (!res.isIssue()) + Throw("Amount is not STAmount"); + return get(res); +} + +bool +amountFromJsonNoThrow(STEitherAmount& result, Json::Value const& jvSource) +{ + try + { + result = amountFromJson(sfGeneric, jvSource); + return true; + } + catch (const std::exception& e) + { + JLOG(debugLog().warn()) + << "amountFromJsonNoThrow: caught: " << e.what(); + } + return false; +} + +bool +amountFromJsonNoThrow(STAmount& result, Json::Value const& jvSource) +{ + try + { + STEitherAmount amount; + const bool res = amountFromJsonNoThrow(amount, jvSource); + if (res) + result = get(amount); + return res; + } + catch (const std::exception& e) + { + JLOG(debugLog().warn()) + << "amountFromJsonNoThrow: caught: " << e.what(); + } + return false; +} + +} // namespace ripple diff --git a/src/libxrpl/protocol/STMPTAmount.cpp b/src/libxrpl/protocol/STMPTAmount.cpp new file mode 100644 index 00000000000..c4e40e3ce36 --- /dev/null +++ b/src/libxrpl/protocol/STMPTAmount.cpp @@ -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 +#include +#include +#include + +#include + +namespace ripple { + +STMPTAmount::STMPTAmount(SerialIter& sit) +{ + auto const mask = sit.get8(); + assert((mask & cMPToken)); + if (((mask & cMPToken) == 0)) + Throw("Not MPT Amount."); + + value_ = sit.get64(); + if ((mask & cPositive) == 0) + value_ = -value_; + issue_ = sit.get192(); +} + +STMPTAmount::STMPTAmount(MPTIssue const& issue, value_type value) + : MPTAmount(value), issue_(issue) +{ +} + +STMPTAmount::STMPTAmount( + MPTIssue const& issue, + std::uint64_t value, + bool negative) + : issue_(issue) +{ + if (value > maxMPTokenAmount) + Throw("MPTAmount is out of range"); + value_ = static_cast(value); + if (negative) + value_ = -value_; +} + +STMPTAmount::STMPTAmount(value_type value) : MPTAmount(value) +{ +} + +SerializedTypeID +STMPTAmount::getSType() const +{ + return STI_AMOUNT; +} + +std::string +STMPTAmount::getFullText() const +{ + std::string ret; + + ret.reserve(64); + ret = getText() + "/" + to_string(issue_.getMptID()); + return ret; +} + +std::string +STMPTAmount::getText() const +{ + return std::to_string(value_); +} + +Json::Value STMPTAmount::getJson(JsonOptions) const +{ + Json::Value elem; + setJson(elem); + return elem; +} + +void +STMPTAmount::setJson(Json::Value& elem) const +{ + elem[jss::mpt_issuance_id] = to_string(issue_.getMptID()); + elem[jss::value] = getText(); +} + +void +STMPTAmount::add(Serializer& s) const +{ + auto u8 = cMPToken; + if (value_ >= 0) + u8 |= cPositive; + s.add8(u8); + s.add64(value_ >= 0 ? value_ : -value_); + s.addBitString(issue_.getMptID()); +} + +bool +STMPTAmount::isDefault() const +{ + return value_ == 0 && issue_ == noMPT(); +} + +AccountID +STMPTAmount::getIssuer() const +{ + return issue_.getIssuer(); +} + +MPTID const& +STMPTAmount::getCurrency() const +{ + return issue_.getMptID(); +} + +MPTIssue const& +STMPTAmount::issue() const +{ + return issue_; +} + +void +STMPTAmount::clear() +{ + value_ = 0; +} + +void +STMPTAmount::clear(MPTIssue const& issue) +{ + issue_ = issue; + value_ = 0; +} + +STMPTAmount +STMPTAmount::zeroed() const +{ + return STMPTAmount{issue_}; +} + +int +STMPTAmount::signum() const noexcept +{ + return MPTAmount::signum(); +} + +STMPTAmount +amountFromString(MPTIssue const& issue, std::string const& amount) +{ + static boost::regex const reNumber( + "^" // the beginning of the string + "([+-]?)" // (optional) + character + "(0|[1-9][0-9]*)" // a number (no leading zeroes, unless 0) + "(\\.([0-9]+))?" // (optional) period followed by any number + "([eE]([+-]?)([0-9]+))?" // (optional) E, optional + or -, any number + "$", + boost::regex_constants::optimize); + + boost::smatch match; + + if (!boost::regex_match(amount, match, reNumber)) + Throw("MPT '" + amount + "' is not valid"); + + // Match fields: + // 0 = whole input + // 1 = sign + // 2 = integer portion + // 3 = whole fraction (with '.') + // 4 = fraction (without '.') + // 5 = whole exponent (with 'e') + // 6 = exponent sign + // 7 = exponent number + + // CHECKME: Why 32? Shouldn't this be 16? + if ((match[2].length() + match[4].length()) > 32) + Throw("Number '" + amount + "' is overlong"); + + // Can't specify MPT using fractional representation + if (match[3].matched) + Throw("MPT must be specified as integral."); + + std::uint64_t mantissa; + int exponent; + + bool negative = (match[1].matched && (match[1] == "-")); + + if (!match[4].matched) // integer only + { + mantissa = + beast::lexicalCastThrow(std::string(match[2])); + exponent = 0; + } + else + { + // integer and fraction + mantissa = beast::lexicalCastThrow(match[2] + match[4]); + exponent = -(match[4].length()); + } + + if (match[5].matched) + { + // we have an exponent + if (match[6].matched && (match[6] == "-")) + exponent -= beast::lexicalCastThrow(std::string(match[7])); + else + exponent += beast::lexicalCastThrow(std::string(match[7])); + } + + while (exponent-- > 0) + mantissa *= 10; + + return {issue, mantissa, negative}; +} + +} // namespace ripple \ No newline at end of file diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index bde83ec31a1..ccd34d16d33 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -604,6 +604,12 @@ STObject::getFieldH160(SField const& field) const return getFieldByValue(field); } +uint192 +STObject::getFieldH192(SField const& field) const +{ + return getFieldByValue(field); +} + uint256 STObject::getFieldH256(SField const& field) const { @@ -624,11 +630,19 @@ STObject::getFieldVL(SField const& field) const return Blob(b.data(), b.data() + b.size()); } -STAmount const& +STEitherAmount const& STObject::getFieldAmount(SField const& field) const { - static STAmount const empty{}; - return getFieldByConstRef(field, empty); + static STEitherAmount const empty{}; + return getFieldByConstRef(field, empty); +} + +STAmount const& +STObject::getFieldAmount( + TypedVariantField const& field) const +{ + static STEitherAmount const empty{}; + return get(getFieldByConstRef(field, empty)); } STPathSet const& @@ -742,7 +756,7 @@ STObject::setFieldVL(SField const& field, Slice const& s) } void -STObject::setFieldAmount(SField const& field, STAmount const& v) +STObject::setFieldAmount(SField const& field, STEitherAmount const& v) { setFieldUsingAssignment(field, v); } diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index dec5e87eaee..92044334431 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -454,6 +454,30 @@ parseLeaf( break; } + case STI_UINT192: { + if (!value.isString()) + { + error = bad_type(json_name, fieldName); + return ret; + } + + uint192 num; + + if (auto const s = value.asString(); !num.parseHex(s)) + { + if (!s.empty()) + { + error = invalid_data(json_name, fieldName); + return ret; + } + + num.zero(); + } + + ret = detail::make_stvar(field, num); + break; + } + case STI_UINT160: { if (!value.isString()) { @@ -532,8 +556,8 @@ parseLeaf( case STI_AMOUNT: try { - ret = - detail::make_stvar(amountFromJson(field, value)); + ret = detail::make_stvar( + amountFromJson(field, value)); } catch (std::exception const&) { diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 149186d43ce..d9726ccbd06 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -543,6 +543,32 @@ isAccountFieldOkay(STObject const& st) return true; } +static bool +invalidMPTAmountInTx(STObject const& tx) +{ + auto const txType = tx[~sfTransactionType]; + if (!txType) + return false; + if (auto const* item = + TxFormats::getInstance().findByType(safe_cast(*txType))) + { + for (auto const& e : item->getSOTemplate()) + { + if (tx.isFieldPresent(e.sField()) && e.supportMPT() != soeMPTNone) + { + if (auto const& field = tx.peekAtField(e.sField()); + field.getSType() == STI_AMOUNT && + static_cast(field).isMPT()) + { + if (e.supportMPT() == soeMPTNotSupported) + return true; + } + } + } + } + return false; +} + bool passesLocalChecks(STObject const& st, std::string& reason) { @@ -560,6 +586,13 @@ passesLocalChecks(STObject const& st, std::string& reason) reason = "Cannot submit pseudo transactions."; return false; } + + if (invalidMPTAmountInTx(st)) + { + reason = "Amount can not be MPT."; + return false; + } + return true; } diff --git a/src/libxrpl/protocol/STVar.cpp b/src/libxrpl/protocol/STVar.cpp index c8466259f32..8dbe0a70550 100644 --- a/src/libxrpl/protocol/STVar.cpp +++ b/src/libxrpl/protocol/STVar.cpp @@ -133,7 +133,7 @@ STVar::STVar(SerialIter& sit, SField const& name, int depth) construct(sit, name); return; case STI_AMOUNT: - construct(sit, name); + construct(sit, name); return; case STI_UINT128: construct(sit, name); @@ -141,6 +141,9 @@ STVar::STVar(SerialIter& sit, SField const& name, int depth) case STI_UINT160: construct(sit, name); return; + case STI_UINT192: + construct(sit, name); + return; case STI_UINT256: construct(sit, name); return; @@ -197,7 +200,7 @@ STVar::STVar(SerializedTypeID id, SField const& name) construct(name); return; case STI_AMOUNT: - construct(name); + construct(name); return; case STI_UINT128: construct(name); @@ -205,6 +208,9 @@ STVar::STVar(SerializedTypeID id, SField const& name) case STI_UINT160: construct(name); return; + case STI_UINT192: + construct(name); + return; case STI_UINT256: construct(name); return; diff --git a/src/libxrpl/protocol/Serializer.cpp b/src/libxrpl/protocol/Serializer.cpp index b99375f80dd..d9dddab74c1 100644 --- a/src/libxrpl/protocol/Serializer.cpp +++ b/src/libxrpl/protocol/Serializer.cpp @@ -358,6 +358,15 @@ SerialIter::skip(int length) remain_ -= length; } +unsigned char +SerialIter::peek8() +{ + if (remain_ < 1) + Throw("invalid SerialIter peek8"); + unsigned char t = *p_; + return t; +} + unsigned char SerialIter::get8() { diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 917bbf26a9f..950163e3aa5 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -85,6 +85,7 @@ transResults() MAKE_ERROR(tecHAS_OBLIGATIONS, "The account cannot be deleted since it has obligations."), MAKE_ERROR(tecTOO_SOON, "It is too early to attempt the requested operation. Please wait."), MAKE_ERROR(tecMAX_SEQUENCE_REACHED, "The maximum sequence number was reached."), + MAKE_ERROR(tecMPT_NOT_SUPPORTED, "MPT is not supported."), MAKE_ERROR(tecNO_SUITABLE_NFTOKEN_PAGE, "A suitable NFToken page could not be located."), MAKE_ERROR(tecNFTOKEN_BUY_SELL_MISMATCH, "The 'Buy' and 'Sell' NFToken offers are mismatched."), MAKE_ERROR(tecNFTOKEN_OFFER_TYPE_MISMATCH, "The type of NFToken offer is incorrect."), @@ -115,6 +116,9 @@ transResults() MAKE_ERROR(tecTOKEN_PAIR_NOT_FOUND, "Token pair is not found in Oracle object."), MAKE_ERROR(tecARRAY_EMPTY, "Array is empty."), MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."), + MAKE_ERROR(tecMPTOKEN_EXISTS, "The account already owns the MPToken object."), + MAKE_ERROR(tecMPT_MAX_AMOUNT_EXCEEDED, "The MPT's maximum amount is exceeded."), + MAKE_ERROR(tecMPT_LOCKED, "MPT is locked by the issuer."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), @@ -197,6 +201,7 @@ transResults() MAKE_ERROR(temINVALID_COUNT, "Malformed: Count field outside valid range."), MAKE_ERROR(temSEQ_AND_TICKET, "Transaction contains a TicketSequence and a non-zero Sequence."), MAKE_ERROR(temBAD_NFTOKEN_TRANSFER_FEE, "Malformed: The NFToken transfer fee must be between 1 and 5000, inclusive."), + MAKE_ERROR(temBAD_MPTOKEN_TRANSFER_FEE, "Malformed: The MPToken transfer fee must be between 1 and 5000, inclusive."), MAKE_ERROR(temXCHAIN_EQUAL_DOOR_ACCOUNTS, "Malformed: Bridge must have unique door accounts."), MAKE_ERROR(temXCHAIN_BAD_PROOF, "Malformed: Bad cross-chain claim proof."), MAKE_ERROR(temXCHAIN_BRIDGE_BAD_ISSUES, "Malformed: Bad bridge issues."), diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index 8a93232604e..92e8ff3b690 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -162,7 +162,7 @@ TxFormats::TxFormats() ttPAYMENT, { {sfDestination, soeREQUIRED}, - {sfAmount, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, {sfSendMax, soeOPTIONAL}, {sfPaths, soeDEFAULT}, {sfInvoiceID, soeOPTIONAL}, @@ -377,7 +377,8 @@ TxFormats::TxFormats() add(jss::Clawback, ttCLAWBACK, { - {sfAmount, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, + {sfMPTokenHolder, soeOPTIONAL}, }, commonFields); @@ -513,6 +514,39 @@ TxFormats::TxFormats() {sfOwner, soeOPTIONAL}, }, commonFields); + + add(jss::MPTokenIssuanceCreate, + ttMPTOKEN_ISSUANCE_CREATE, + { + {sfAssetScale, soeOPTIONAL}, + {sfTransferFee, soeOPTIONAL}, + {sfMaximumAmount, soeOPTIONAL}, + {sfMPTokenMetadata, soeOPTIONAL}, + }, + commonFields); + + add(jss::MPTokenIssuanceDestroy, + ttMPTOKEN_ISSUANCE_DESTROY, + { + {sfMPTokenIssuanceID, soeREQUIRED}, + }, + commonFields); + + add(jss::MPTokenAuthorize, + ttMPTOKEN_AUTHORIZE, + { + {sfMPTokenIssuanceID, soeREQUIRED}, + {sfMPTokenHolder, soeOPTIONAL}, + }, + commonFields); + + add(jss::MPTokenIssuanceSet, + ttMPTOKEN_ISSUANCE_SET, + { + {sfMPTokenIssuanceID, soeREQUIRED}, + {sfMPTokenHolder, soeOPTIONAL}, + }, + commonFields); } TxFormats const& diff --git a/src/libxrpl/protocol/TxMeta.cpp b/src/libxrpl/protocol/TxMeta.cpp index 253d00e8414..85ed5dc487e 100644 --- a/src/libxrpl/protocol/TxMeta.cpp +++ b/src/libxrpl/protocol/TxMeta.cpp @@ -144,7 +144,7 @@ TxMeta::getAffectedAccounts() const (field.getFName() == sfTakerPays) || (field.getFName() == sfTakerGets)) { - auto lim = dynamic_cast(&field); + auto lim = dynamic_cast(&field); assert(lim); if (lim != nullptr) diff --git a/src/libxrpl/protocol/UintTypes.cpp b/src/libxrpl/protocol/UintTypes.cpp index c57bcc153a3..e50e52c1c30 100644 --- a/src/libxrpl/protocol/UintTypes.cpp +++ b/src/libxrpl/protocol/UintTypes.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -125,6 +126,13 @@ noCurrency() return currency; } +MPTID const& +noMPT() +{ + static MPTID const id = getMptID(noAccount(), 0); + return id; +} + Currency const& badCurrency() { diff --git a/src/libxrpl/protocol/XChainAttestations.cpp b/src/libxrpl/protocol/XChainAttestations.cpp index 82e73445693..2d736ebff43 100644 --- a/src/libxrpl/protocol/XChainAttestations.cpp +++ b/src/libxrpl/protocol/XChainAttestations.cpp @@ -107,7 +107,7 @@ AttestationBase::AttestationBase(STObject const& o) , publicKey{o[sfPublicKey]} , signature{o[sfSignature]} , sendingAccount{o[sfAccount]} - , sendingAmount{o[sfAmount]} + , sendingAmount{get(o[sfAmount])} , rewardAccount{o[sfAttestationRewardAccount]} , wasLockingChainSend{bool(o[sfWasLockingChainSend])} { @@ -132,7 +132,7 @@ AttestationBase::addHelper(STObject& o) const o[sfAttestationSignerAccount] = attestationSignerAccount; o[sfPublicKey] = publicKey; o[sfSignature] = signature; - o[sfAmount] = sendingAmount; + o[sfAmount] = STEitherAmount{sendingAmount}; o[sfAccount] = sendingAccount; o[sfAttestationRewardAccount] = rewardAccount; o[sfWasLockingChainSend] = wasLockingChainSend; @@ -216,7 +216,7 @@ std::vector AttestationClaim::message( STXChainBridge const& bridge, AccountID const& sendingAccount, - STAmount const& sendingAmount, + STEitherAmount const& sendingAmount, AccountID const& rewardAccount, bool wasLockingChainSend, std::uint64_t claimID, @@ -361,8 +361,8 @@ std::vector AttestationCreateAccount::message( STXChainBridge const& bridge, AccountID const& sendingAccount, - STAmount const& sendingAmount, - STAmount const& rewardAmount, + STEitherAmount const& sendingAmount, + STEitherAmount const& rewardAmount, AccountID const& rewardAccount, bool wasLockingChainSend, std::uint64_t createCount, @@ -371,7 +371,7 @@ AttestationCreateAccount::message( STObject o{sfGeneric}; // Serialize in SField order to make python serializers easier to write o[sfXChainAccountCreateCount] = createCount; - o[sfAmount] = sendingAmount; + o[sfAmount] = STEitherAmount{sendingAmount}; o[sfSignatureReward] = rewardAmount; o[sfDestination] = dst; o[sfOtherChainSource] = sendingAccount; @@ -438,7 +438,7 @@ XChainClaimAttestation::XChainClaimAttestation( std::optional const& dst_) : keyAccount(keyAccount_) , publicKey(publicKey_) - , amount(sfAmount, amount_) + , amount(amount_) , rewardAccount(rewardAccount_) , wasLockingChainSend(wasLockingChainSend_) , dst(dst_) @@ -466,7 +466,7 @@ XChainClaimAttestation::XChainClaimAttestation(STObject const& o) : XChainClaimAttestation{ o[sfAttestationSignerAccount], PublicKey{o[sfPublicKey]}, - o[sfAmount], + get(o[sfAmount]), o[sfAttestationRewardAccount], o[sfWasLockingChainSend] != 0, o[~sfDestination]} {}; @@ -503,7 +503,7 @@ XChainClaimAttestation::toSTObject() const o[sfAttestationSignerAccount] = STAccount{sfAttestationSignerAccount, keyAccount}; o[sfPublicKey] = publicKey; - o[sfAmount] = STAmount{sfAmount, amount}; + o[sfAmount] = STEitherAmount(amount); o[sfAttestationRewardAccount] = STAccount{sfAttestationRewardAccount, rewardAccount}; o[sfWasLockingChainSend] = wasLockingChainSend; @@ -563,8 +563,8 @@ XChainCreateAccountAttestation::XChainCreateAccountAttestation( AccountID const& dst_) : keyAccount(keyAccount_) , publicKey(publicKey_) - , amount(sfAmount, amount_) - , rewardAmount(sfSignatureReward, rewardAmount_) + , amount(amount_) + , rewardAmount(rewardAmount_) , rewardAccount(rewardAccount_) , wasLockingChainSend(wasLockingChainSend_) , dst(dst_) @@ -576,7 +576,7 @@ XChainCreateAccountAttestation::XChainCreateAccountAttestation( : XChainCreateAccountAttestation{ o[sfAttestationSignerAccount], PublicKey{o[sfPublicKey]}, - o[sfAmount], + get(o[sfAmount]), o[sfSignatureReward], o[sfAttestationRewardAccount], o[sfWasLockingChainSend] != 0, @@ -616,8 +616,8 @@ XChainCreateAccountAttestation::toSTObject() const o[sfAttestationSignerAccount] = STAccount{sfAttestationSignerAccount, keyAccount}; o[sfPublicKey] = publicKey; - o[sfAmount] = STAmount{sfAmount, amount}; - o[sfSignatureReward] = STAmount{sfSignatureReward, rewardAmount}; + o[sfAmount] = STEitherAmount{sfAmount, amount}; + o[sfSignatureReward] = STAmount{rewardAmount}; o[sfAttestationRewardAccount] = STAccount{sfAttestationRewardAccount, rewardAccount}; o[sfWasLockingChainSend] = wasLockingChainSend; diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index 96053b93b44..fc53ba3f016 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -1547,8 +1547,7 @@ struct AMMExtended_test : public jtx::AMMTest Env env = pathTestEnv(); fund(env, gw, {alice, bob, charlie}, {USD(11)}, Fund::All); AMM ammCharlie(env, charlie, XRP(10), USD(11)); - auto [st, sa, da] = - find_paths(env, alice, bob, USD(-1), XRP(1).value()); + auto [st, sa, da] = find_paths(env, alice, bob, USD(-1), XRP(1)); BEAST_EXPECT(sa == XRP(1)); BEAST_EXPECT(equal(da, USD(1))); if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1)) @@ -1565,8 +1564,7 @@ struct AMMExtended_test : public jtx::AMMTest fund(env, gw, {alice, bob, charlie}, {USD(11)}, Fund::All); AMM ammCharlie(env, charlie, XRP(11), USD(10)); env.close(); - auto [st, sa, da] = - find_paths(env, alice, bob, drops(-1), USD(1).value()); + auto [st, sa, da] = find_paths(env, alice, bob, drops(-1), USD(1)); BEAST_EXPECT(sa == USD(1)); BEAST_EXPECT(equal(da, XRP(1))); if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1)) @@ -1916,7 +1914,7 @@ struct AMMExtended_test : public jtx::AMMTest sendmax(EUR(500)), txflags(tfNoRippleDirect | tfPartialPayment)); - auto const carolUSD = env.balance(carol, USD).value(); + auto const carolUSD = get(env.balance(carol, USD)); BEAST_EXPECT(carolUSD > USD(0) && carolUSD < USD(50)); } diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index 31b45abf43a..cb82bef9b59 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -211,7 +211,7 @@ class Check_test : public beast::unit_test::suite Env env{*this, features}; - STAmount const startBalance{XRP(1000).value()}; + STAmount const startBalance{XRP(1000)}; env.fund(startBalance, gw, alice, bob); // Note that no trust line has been set up for alice, but alice can @@ -321,7 +321,7 @@ class Check_test : public beast::unit_test::suite Env env{*this, features | disallowIncoming}; - STAmount const startBalance{XRP(1000).value()}; + STAmount const startBalance{XRP(1000)}; env.fund(startBalance, gw, alice, bob); /* @@ -405,7 +405,7 @@ class Check_test : public beast::unit_test::suite Env env{*this, features}; - STAmount const startBalance{XRP(1000).value()}; + STAmount const startBalance{XRP(1000)}; env.fund(startBalance, gw1, gwF, alice, bob); // Bad fee. @@ -587,7 +587,7 @@ class Check_test : public beast::unit_test::suite Env env{*this, features}; XRPAmount const baseFeeDrops{env.current()->fees().base}; - STAmount const startBalance{XRP(300).value()}; + STAmount const startBalance{XRP(300)}; env.fund(startBalance, alice, bob); { // Basic XRP check. @@ -1192,8 +1192,8 @@ class Check_test : public beast::unit_test::suite double pct, double amount) { // Capture bob's and alice's balances so we can test at the end. - STAmount const aliceStart{env.balance(alice, USD.issue()).value()}; - STAmount const bobStart{env.balance(bob, USD.issue()).value()}; + STAmount const aliceStart{env.balance(alice, USD.issue())}; + STAmount const bobStart{env.balance(bob, USD.issue())}; // Set the modified quality. env(trust(truster, iou(1000)), inOrOut(pct)); @@ -1217,8 +1217,8 @@ class Check_test : public beast::unit_test::suite double pct, double amount) { // Capture bob's and alice's balances so we can test at the end. - STAmount const aliceStart{env.balance(alice, USD.issue()).value()}; - STAmount const bobStart{env.balance(bob, USD.issue()).value()}; + STAmount const aliceStart{env.balance(alice, USD.issue())}; + STAmount const bobStart{env.balance(bob, USD.issue())}; // Set the modified quality. env(trust(truster, iou(1000)), inOrOut(pct)); @@ -1281,7 +1281,7 @@ class Check_test : public beast::unit_test::suite double max2) { // Capture alice's balance so we can test at the end. It doesn't // make any sense to look at the balance of a gateway. - STAmount const aliceStart{env.balance(alice, USD.issue()).value()}; + STAmount const aliceStart{env.balance(alice, USD.issue())}; // Set the modified quality. env(trust(truster, iou(1000)), inOrOut(pct)); @@ -1314,7 +1314,7 @@ class Check_test : public beast::unit_test::suite double max2) { // Capture alice's balance so we can test at the end. It doesn't // make any sense to look at the balance of the issuer. - STAmount const aliceStart{env.balance(alice, USD.issue()).value()}; + STAmount const aliceStart{env.balance(alice, USD.issue())}; // Set the modified quality. env(trust(truster, iou(1000)), inOrOut(pct)); diff --git a/src/test/app/Clawback_test.cpp b/src/test/app/Clawback_test.cpp index a6909bb2f62..8a42d4c38ef 100644 --- a/src/test/app/Clawback_test.cpp +++ b/src/test/app/Clawback_test.cpp @@ -965,6 +965,7 @@ class Clawback_test : public beast::unit_test::suite using namespace test::jtx; FeatureBitset const all{supported_amendments()}; + testWithFeats(all - featureMPTokensV1); testWithFeats(all); } }; diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index 9d1257d16bf..ee11f5e4747 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -763,7 +763,7 @@ struct Flow_test : public beast::unit_test::suite sendmax(EUR(500)), txflags(tfNoRippleDirect | tfPartialPayment)); - auto const carolUSD = env.balance(carol, USD).value(); + auto const carolUSD = get(env.balance(carol, USD)); BEAST_EXPECT(carolUSD > USD(0) && carolUSD < USD(50)); } diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp new file mode 100644 index 00000000000..d534b24320a --- /dev/null +++ b/src/test/app/MPToken_test.cpp @@ -0,0 +1,1625 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include + +namespace ripple { + +class MPToken_test : public beast::unit_test::suite +{ + void + testCreateValidation(FeatureBitset features) + { + testcase("Create Validate"); + using namespace test::jtx; + Account const alice("alice"); + + // test preflight of MPTokenIssuanceCreate + { + // If the MPT amendment is not enabled, you should not be able to + // create MPTokenIssuances + Env env{*this, features - featureMPTokensV1}; + MPTTester mptAlice(env, alice); + + mptAlice.create({.ownerCount = 0, .err = temDISABLED}); + } + + // test preflight of MPTokenIssuanceCreate + { + Env env{*this, features}; + MPTTester mptAlice(env, alice); + + mptAlice.create({.flags = 0x00000001, .err = temINVALID_FLAG}); + + // tries to set a txfee while not enabling in the flag + mptAlice.create( + {.maxAmt = 100, + .assetScale = 0, + .transferFee = 1, + .metadata = "test", + .err = temMALFORMED}); + + // tries to set a txfee greater than max + mptAlice.create( + {.maxAmt = 100, + .assetScale = 0, + .transferFee = maxTransferFee + 1, + .metadata = "test", + .flags = tfMPTCanTransfer, + .err = temBAD_MPTOKEN_TRANSFER_FEE}); + + // tries to set a txfee while not enabling transfer + mptAlice.create( + {.maxAmt = 100, + .assetScale = 0, + .transferFee = maxTransferFee, + .metadata = "test", + .err = temMALFORMED}); + + // empty metadata returns error + mptAlice.create( + {.maxAmt = 100, + .assetScale = 0, + .transferFee = 0, + .metadata = "", + .err = temMALFORMED}); + + // MaximumAmout of 0 returns error + mptAlice.create( + {.maxAmt = 0, + .assetScale = 1, + .transferFee = 1, + .metadata = "test", + .err = temMALFORMED}); + + // MaximumAmount larger than 63 bit returns error + mptAlice.create( + {.maxAmt = 0xFFFFFFFFFFFFFFF0ull, + .assetScale = 0, + .transferFee = 0, + .metadata = "test", + .err = temMALFORMED}); + } + } + + void + testCreateEnabled(FeatureBitset features) + { + testcase("Create Enabled"); + + using namespace test::jtx; + Account const alice("alice"); + + { + // If the MPT amendment IS enabled, you should be able to create + // MPTokenIssuances + Env env{*this, features}; + MPTTester mptAlice(env, alice); + mptAlice.create( + {.maxAmt = 0x7FFFFFFFFFFFFFFF, + .assetScale = 1, + .transferFee = 10, + .metadata = "123", + .ownerCount = 1, + .flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | + tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback}); + } + } + + void + testDestroyValidation(FeatureBitset features) + { + testcase("Destroy Validate"); + + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + // MPTokenIssuanceDestroy (preflight) + { + Env env{*this, features - featureMPTokensV1}; + MPTTester mptAlice(env, alice); + auto const id = getMptID(alice, env.seq(alice)); + mptAlice.destroy({.id = id, .ownerCount = 0, .err = temDISABLED}); + + env.enableFeature(featureMPTokensV1); + + mptAlice.destroy( + {.id = id, .flags = 0x00000001, .err = temINVALID_FLAG}); + } + + // MPTokenIssuanceDestroy (preclaim) + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.destroy( + {.id = getMptID(alice.id(), env.seq(alice)), + .ownerCount = 0, + .err = tecOBJECT_NOT_FOUND}); + + mptAlice.create({.ownerCount = 1}); + + // a non-issuer tries to destroy a mptissuance they didn't issue + mptAlice.destroy({.issuer = &bob, .err = tecNO_PERMISSION}); + + // Make sure that issuer can't delete issuance when it still has + // outstanding balance + { + // bob now holds a mptoken object + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + mptAlice.destroy({.err = tecHAS_OBLIGATIONS}); + } + } + } + + void + testDestroyEnabled(FeatureBitset features) + { + testcase("Destroy Enabled"); + + using namespace test::jtx; + Account const alice("alice"); + + // If the MPT amendment IS enabled, you should be able to destroy + // MPTokenIssuances + Env env{*this, features}; + MPTTester mptAlice(env, alice); + + mptAlice.create({.ownerCount = 1}); + + mptAlice.destroy({.ownerCount = 0}); + } + + void + testAuthorizeValidation(FeatureBitset features) + { + testcase("Validate authorize transaction"); + + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const cindy("cindy"); + // Validate amendment enable in MPTokenAuthorize (preflight) + { + Env env{*this, features - featureMPTokensV1}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.authorize( + {.account = &bob, + .id = getMptID(alice, env.seq(alice)), + .err = temDISABLED}); + } + + // Validate fields in MPTokenAuthorize (preflight) + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1}); + + mptAlice.authorize( + {.account = &bob, .flags = 0x00000002, .err = temINVALID_FLAG}); + + mptAlice.authorize( + {.account = &bob, .holder = &bob, .err = temMALFORMED}); + + mptAlice.authorize({.holder = &alice, .err = temMALFORMED}); + } + + // Try authorizing when MPTokenIssuance doesn't exist in + // MPTokenAuthorize (preclaim) + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + auto const id = getMptID(alice, env.seq(alice)); + + mptAlice.authorize( + {.holder = &bob, .id = id, .err = tecOBJECT_NOT_FOUND}); + + mptAlice.authorize( + {.account = &bob, .id = id, .err = tecOBJECT_NOT_FOUND}); + } + + // Test bad scenarios without allowlisting in MPTokenAuthorize + // (preclaim) + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1}); + + // bob submits a tx with a holder field + mptAlice.authorize( + {.account = &bob, .holder = &alice, .err = tecNO_PERMISSION}); + + // alice tries to hold onto her own token + mptAlice.authorize({.account = &alice, .err = tecNO_PERMISSION}); + + // the mpt does not enable allowlisting + mptAlice.authorize({.holder = &bob, .err = tecNO_AUTH}); + + // bob now holds a mptoken object + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // bob cannot create the mptoken the second time + mptAlice.authorize({.account = &bob, .err = tecMPTOKEN_EXISTS}); + + // Check that bob cannot delete MPToken when his balance is + // non-zero + { + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + // bob tries to delete his MPToken, but fails since he still + // holds tokens + mptAlice.authorize( + {.account = &bob, + .flags = tfMPTUnauthorize, + .err = tecHAS_OBLIGATIONS}); + + // bob pays back alice 100 tokens + mptAlice.pay(bob, alice, 100); + } + + // bob deletes/unauthorizes his MPToken + mptAlice.authorize({.account = &bob, .flags = tfMPTUnauthorize}); + + // bob receives error when he tries to delete his MPToken that has + // already been deleted + mptAlice.authorize( + {.account = &bob, + .holderCount = 0, + .flags = tfMPTUnauthorize, + .err = tecOBJECT_NOT_FOUND}); + } + + // Test bad scenarios with allow-listing in MPTokenAuthorize (preclaim) + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth}); + + // alice submits a tx without specifying a holder's account + mptAlice.authorize({.err = tecNO_PERMISSION}); + + // alice submits a tx to authorize a holder that hasn't created + // a mptoken yet + mptAlice.authorize({.holder = &bob, .err = tecOBJECT_NOT_FOUND}); + + // alice specifys a holder acct that doesn't exist + mptAlice.authorize({.holder = &cindy, .err = tecNO_DST}); + + // bob now holds a mptoken object + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // alice tries to unauthorize bob. + // although tx is successful, + // but nothing happens because bob hasn't been authorized yet + mptAlice.authorize({.holder = &bob, .flags = tfMPTUnauthorize}); + + // alice authorizes bob + // make sure bob's mptoken has set lsfMPTAuthorized + mptAlice.authorize({.holder = &bob}); + + // alice tries authorizes bob again. + // tx is successful, but bob is already authorized, + // so no changes + mptAlice.authorize({.holder = &bob}); + + // bob deletes his mptoken + mptAlice.authorize( + {.account = &bob, .holderCount = 0, .flags = tfMPTUnauthorize}); + } + + // Test mptoken reserve requirement - first two mpts free (doApply) + { + Env env{*this, features}; + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + + MPTTester mptAlice1( + env, + alice, + {.holders = {&bob}, + .xrpHolders = + acctReserve + get(XRP(1).value()).xrp()}); + mptAlice1.create(); + + MPTTester mptAlice2(env, alice, {.fund = false}); + mptAlice2.create(); + + MPTTester mptAlice3(env, alice, {.fund = false}); + mptAlice3.create({.ownerCount = 3}); + + // first mpt for free + mptAlice1.authorize({.account = &bob, .holderCount = 1}); + + // second mpt free + mptAlice2.authorize({.account = &bob, .holderCount = 2}); + + mptAlice3.authorize( + {.account = &bob, .err = tecINSUFFICIENT_RESERVE}); + + env(pay( + env.master, bob, drops(incReserve + incReserve + incReserve))); + env.close(); + + mptAlice3.authorize({.account = &bob, .holderCount = 3}); + } + } + + void + testAuthorizeEnabled(FeatureBitset features) + { + testcase("Authorize Enabled"); + + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + // Basic authorization without allowlisting + { + Env env{*this, features}; + + // alice create mptissuance without allowisting + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1}); + + // bob creates a mptoken + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // bob deletes his mptoken + mptAlice.authorize( + {.account = &bob, .holderCount = 0, .flags = tfMPTUnauthorize}); + } + + // With allowlisting + { + Env env{*this, features}; + + // alice creates a mptokenissuance that requires authorization + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth}); + + // bob creates a mptoken + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // alice authorizes bob + mptAlice.authorize({.account = &alice, .holder = &bob}); + + // Unauthorize bob's mptoken + mptAlice.authorize( + {.account = &alice, + .holder = &bob, + .holderCount = 1, + .flags = tfMPTUnauthorize}); + + mptAlice.authorize( + {.account = &bob, .holderCount = 0, .flags = tfMPTUnauthorize}); + } + + // Holder can have dangling MPToken even if issuance has been destroyed. + // Make sure they can still delete/unauthorize the MPToken + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1}); + + // bob creates a mptoken + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // alice deletes her issuance + mptAlice.destroy({.ownerCount = 0}); + + // bob can delete his mptoken even though issuance is no longer + // existent + mptAlice.authorize( + {.account = &bob, .holderCount = 0, .flags = tfMPTUnauthorize}); + } + } + + void + testSetValidation(FeatureBitset features) + { + testcase("Validate set transaction"); + + using namespace test::jtx; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + Account const cindy("cindy"); + // Validate fields in MPTokenIssuanceSet (preflight) + { + Env env{*this, features - featureMPTokensV1}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.set( + {.account = &bob, + .id = getMptID(alice, env.seq(alice)), + .err = temDISABLED}); + + env.enableFeature(featureMPTokensV1); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // test invalid flag + mptAlice.set( + {.account = &alice, + .flags = 0x00000008, + .err = temINVALID_FLAG}); + + // set both lock and unlock flags at the same time will fail + mptAlice.set( + {.account = &alice, + .flags = tfMPTLock | tfMPTUnlock, + .err = temINVALID_FLAG}); + + // if the holder is the same as the acct that submitted the tx, + // tx fails + mptAlice.set( + {.account = &alice, + .holder = &alice, + .flags = tfMPTLock, + .err = temMALFORMED}); + } + + // Validate fields in MPTokenIssuanceSet (preclaim) + // test when a mptokenissuance has disabled locking + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1}); + + // alice tries to lock a mptissuance that has disabled locking + mptAlice.set( + {.account = &alice, + .flags = tfMPTLock, + .err = tecNO_PERMISSION}); + + // alice tries to unlock mptissuance that has disabled locking + mptAlice.set( + {.account = &alice, + .flags = tfMPTUnlock, + .err = tecNO_PERMISSION}); + + // issuer tries to lock a bob's mptoken that has disabled + // locking + mptAlice.set( + {.account = &alice, + .holder = &bob, + .flags = tfMPTLock, + .err = tecNO_PERMISSION}); + + // issuer tries to unlock a bob's mptoken that has disabled + // locking + mptAlice.set( + {.account = &alice, + .holder = &bob, + .flags = tfMPTUnlock, + .err = tecNO_PERMISSION}); + } + + // Validate fields in MPTokenIssuanceSet (preclaim) + // test when mptokenissuance has enabled locking + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice trying to set when the mptissuance doesn't exist yet + mptAlice.set( + {.id = getMptID(alice.id(), env.seq(alice)), + .flags = tfMPTLock, + .err = tecOBJECT_NOT_FOUND}); + + // create a mptokenissuance with locking + mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock}); + + // a non-issuer acct tries to set the mptissuance + mptAlice.set( + {.account = &bob, .flags = tfMPTLock, .err = tecNO_PERMISSION}); + + // trying to set a holder who doesn't have a mptoken + mptAlice.set( + {.holder = &bob, + .flags = tfMPTLock, + .err = tecOBJECT_NOT_FOUND}); + + // trying to set a holder who doesn't exist + mptAlice.set( + {.holder = &cindy, .flags = tfMPTLock, .err = tecNO_DST}); + } + } + + void + testSetEnabled(FeatureBitset features) + { + testcase("Enabled set transaction"); + + using namespace test::jtx; + + // Test locking and unlocking + Env env{*this, features}; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // create a mptokenissuance with locking + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock}); + + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // locks bob's mptoken + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTLock}); + + // trying to lock bob's mptoken again will still succeed + // but no changes to the objects + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTLock}); + + // alice locks the mptissuance + mptAlice.set({.account = &alice, .flags = tfMPTLock}); + + // alice tries to lock up both mptissuance and mptoken again + // it will not change the flags and both will remain locked. + mptAlice.set({.account = &alice, .flags = tfMPTLock}); + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTLock}); + + // alice unlocks bob's mptoken + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTUnlock}); + + // locks up bob's mptoken again + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTLock}); + + // alice unlocks mptissuance + mptAlice.set({.account = &alice, .flags = tfMPTUnlock}); + + // alice unlocks bob's mptoken + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTUnlock}); + + // alice unlocks mptissuance and bob's mptoken again despite that + // they are already unlocked. Make sure this will not change the + // flags + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTUnlock}); + mptAlice.set({.account = &alice, .flags = tfMPTUnlock}); + } + + void + testPayment(FeatureBitset features) + { + testcase("Payment"); + + using namespace test::jtx; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + Account const carol("carol"); // holder + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + + // env(mpt::authorize(alice, id.key, std::nullopt)); + // env.close(); + + mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = &carol}); + + // issuer to holder + mptAlice.pay(alice, bob, 100); + + // holder to issuer + mptAlice.pay(bob, alice, 100); + + // holder to holder + mptAlice.pay(alice, bob, 100); + mptAlice.pay(bob, carol, 50); + } + + // Holder is not authorized + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + + // issuer to holder + mptAlice.pay(alice, bob, 100, tecNO_AUTH); + + // holder to issuer + mptAlice.pay(bob, alice, 100, tecNO_AUTH); + + // holder to holder + mptAlice.pay(bob, carol, 50, tecNO_AUTH); + } + + // If allowlisting is enabled, Payment fails if the receiver is not + // authorized + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTRequireAuth | tfMPTCanTransfer}); + + mptAlice.authorize({.account = &bob}); + + mptAlice.pay(alice, bob, 100, tecNO_AUTH); + } + + // If allowlisting is enabled, Payment fails if the sender is not + // authorized + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTRequireAuth | tfMPTCanTransfer}); + + // bob creates an empty MPToken + mptAlice.authorize({.account = &bob}); + + // alice authorizes bob to hold funds + mptAlice.authorize({.account = &alice, .holder = &bob}); + + // alice sends 100 MPT to bob + mptAlice.pay(alice, bob, 100); + + // alice UNAUTHORIZES bob + mptAlice.authorize( + {.account = &alice, .holder = &bob, .flags = tfMPTUnauthorize}); + + // bob fails to send back to alice because he is no longer + // authorize to move his funds! + mptAlice.pay(bob, alice, 100, tecNO_AUTH); + } + + // Payer doesn't have enough funds + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer}); + + mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = &carol}); + + mptAlice.pay(alice, bob, 100); + + // Pay to another holder + mptAlice.pay(bob, carol, 101, tecINSUFFICIENT_FUNDS); + + // Pay to the issuer + mptAlice.pay(bob, alice, 101, tecINSUFFICIENT_FUNDS); + } + + // MPT is locked + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create( + {.ownerCount = 1, .flags = tfMPTCanLock | tfMPTCanTransfer}); + + mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = &carol}); + + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 100); + + // Global lock + mptAlice.set({.account = &alice, .flags = tfMPTLock}); + // Can't send between holders + mptAlice.pay(bob, carol, 1, tecMPT_LOCKED); + mptAlice.pay(carol, bob, 2, tecMPT_LOCKED); + // Issuer can send + mptAlice.pay(alice, bob, 3); + // Holder can send back to issuer + mptAlice.pay(bob, alice, 4); + + // Global unlock + mptAlice.set({.account = &alice, .flags = tfMPTUnlock}); + // Individual lock + mptAlice.set( + {.account = &alice, .holder = &bob, .flags = tfMPTLock}); + // Can't send between holders + mptAlice.pay(bob, carol, 5, tecMPT_LOCKED); + mptAlice.pay(carol, bob, 6, tecMPT_LOCKED); + // Issuer can send + mptAlice.pay(alice, bob, 7); + // Holder can send back to issuer + mptAlice.pay(bob, alice, 8); + } + + // Issuer fails trying to send more than the maximum amount allowed + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create( + {.maxAmt = 100, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer}); + + mptAlice.authorize({.account = &bob}); + + // issuer sends holder the max amount allowed + mptAlice.pay(alice, bob, 100); + + // issuer tries to exceed max amount + mptAlice.pay(alice, bob, 1, tecMPT_MAX_AMOUNT_EXCEEDED); + } + + // Issuer fails trying to send more than the default maximum + // amount allowed + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &bob}); + + // issuer sends holder the default max amount allowed + mptAlice.pay(alice, bob, maxMPTokenAmount); + + // issuer tries to exceed max amount + mptAlice.pay(alice, bob, 1, tecMPT_MAX_AMOUNT_EXCEEDED); + } + + // Can't pay negative amount + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &bob}); + + mptAlice.pay(alice, bob, -1, temBAD_AMOUNT); + } + + // pay more than max amount + // fails in the json parser before + // transactor is called + { + Env env{*this, features}; + env.fund(XRP(1'000), alice, bob); + STMPTAmount mpt{MPTIssue{getMptID(alice.id(), 1)}, UINT64_C(100)}; + Json::Value jv; + jv[jss::secret] = alice.name(); + jv[jss::tx_json] = pay(alice, bob, mpt); + jv[jss::tx_json][jss::Amount][jss::value] = + to_string(maxMPTokenAmount + 1); + auto const jrr = env.rpc("json", "submit", to_string(jv)); + BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); + } + + // Transfer fee + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + // Transfer fee is 10% + mptAlice.create( + {.transferFee = 10'000, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer}); + + // Holders create MPToken + mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = &carol}); + + // Payment between the issuer and the holder, no transfer fee. + mptAlice.pay(alice, bob, 2'000); + + // Payment between the holder and the issuer, no transfer fee. + mptAlice.pay(alice, bob, 1'000); + + // Payment between the holders. The sender doesn't have + // enough funds to cover the transfer fee. + mptAlice.pay(bob, carol, 1'000); + + // Payment between the holders. The sender pays 10% transfer fee. + mptAlice.pay(bob, carol, 100); + } + + // Test that non-issuer cannot send to each other if MPTCanTransfer + // isn't set + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const cindy{"cindy"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &cindy}}); + + // alice creates issuance without MPTCanTransfer + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // cindy creates a MPToken + mptAlice.authorize({.account = &cindy}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + // bob tries to send cindy 10 tokens, but fails because canTransfer + // is off + mptAlice.pay(bob, cindy, 10, tecNO_AUTH); + + // bob can send back to alice(issuer) just fine + mptAlice.pay(bob, alice, 10); + } + + // MPT is disabled + { + Env env{*this, features - featureMPTokensV1}; + Account const alice("alice"); + Account const bob("bob"); + + env.fund(XRP(1'000), alice); + env.fund(XRP(1'000), bob); + STMPTAmount mpt{MPTIssue{getMptID(alice.id(), 1)}, UINT64_C(100)}; + + env(pay(alice, bob, mpt), ter(temDISABLED)); + } + + // MPT is disabled, unsigned request + { + Env env{*this, features - featureMPTokensV1}; + Account const alice("alice"); // issuer + Account const carol("carol"); + auto const USD = alice["USD"]; + + env.fund(XRP(1'000), alice); + env.fund(XRP(1'000), carol); + STMPTAmount mpt{MPTIssue{getMptID(alice.id(), 1)}, UINT64_C(100)}; + + Json::Value jv; + jv[jss::secret] = alice.name(); + jv[jss::tx_json][jss::Fee] = to_string(env.current()->fees().base); + jv[jss::tx_json] = pay(alice, carol, mpt); + auto const jrr = env.rpc("json", "submit", to_string(jv)); + BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "temDISABLED"); + } + + // Invalid combination of send, sendMax, deliverMin + { + Env env{*this, features}; + Account const alice("alice"); + Account const carol("carol"); + + MPTTester mptAlice(env, alice, {.holders = {&carol}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &carol}); + + // sendMax and DeliverMin are valid XRP amount, + // but is invalid combination with MPT amount + env(pay(alice, carol, mptAlice.mpt(100)), + sendmax(XRP(100)), + ter(temMALFORMED)); + env(pay(alice, carol, mptAlice.mpt(100)), + delivermin(XRP(100)), + ter(temMALFORMED)); + } + + // build_path is invalid if MPT + { + Env env{*this, features}; + Account const alice("alice"); + Account const carol("carol"); + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &carol}); + + Json::Value payment; + payment[jss::secret] = alice.name(); + payment[jss::tx_json] = pay(alice, carol, mptAlice.mpt(100)); + + payment[jss::build_path] = true; + auto jrr = env.rpc("json", "submit", to_string(payment)); + BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); + BEAST_EXPECT( + jrr[jss::result][jss::error_message] == + "Field 'build_path' not allowed in this context."); + } + } + + void + testMPTInvalidInTx(FeatureBitset features) + { + testcase("MPT Amount Invalid in Transaction"); + using namespace test::jtx; + + std::set txWithAmounts; + for (auto const& format : TxFormats::getInstance()) + { + for (auto const& e : format.getSOTemplate()) + { + // Transaction has amount fields. + // Exclude Clawback, which only supports sfAmount and is checked + // in the transactor for amendment enable/disable. Exclude + // pseudo-transaction SetFee. Don't consider the Fee field since + // it's included in every transaction. + if (e.supportMPT() != soeMPTNone && + e.sField().getName() != jss::Fee && + format.getName() != jss::Clawback && + format.getName() != jss::SetFee) + { + txWithAmounts.insert(format.getName()); + break; + } + } + } + + Account const alice("alice"); + auto const USD = alice["USD"]; + Account const carol("carol"); + MPTIssue issue(getMptID(alice.id(), 1)); + STMPTAmount mpt{issue, UINT64_C(100)}; + auto const jvb = bridge(alice, USD, alice, USD); + for (auto const& feature : {features, features - featureMPTokensV1}) + { + Env env{*this, feature}; + env.fund(XRP(1'000), alice); + env.fund(XRP(1'000), carol); + auto test = [&](Json::Value const& jv) { + txWithAmounts.erase(jv[jss::TransactionType].asString()); + + // tx is signed + auto jtx = env.jt(jv); + Serializer s; + jtx.stx->add(s); + auto jrr = env.rpc("submit", strHex(s.slice())); + BEAST_EXPECT( + jrr[jss::result][jss::error] == "invalidTransaction"); + + // tx is unsigned + Json::Value jv1; + jv1[jss::secret] = alice.name(); + jv1[jss::tx_json] = jv; + jrr = env.rpc("json", "submit", to_string(jv1)); + BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); + }; + // All transactions with sfAmount, which don't support MPT + // and transactions with amount fields, which can't be MPT + + // AMMCreate + auto ammCreate = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::AMMCreate; + jv[jss::Account] = alice.human(); + jv[jss::Amount] = (field.fieldName == sfAmount.fieldName) + ? mpt.getJson(JsonOptions::none) + : "100000000"; + jv[jss::Amount2] = (field.fieldName == sfAmount2.fieldName) + ? mpt.getJson(JsonOptions::none) + : "100000000"; + jv[jss::TradingFee] = 0; + test(jv); + }; + ammCreate(sfAmount); + ammCreate(sfAmount2); + // AMMDeposit + auto ammDeposit = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::AMMDeposit; + jv[jss::Account] = alice.human(); + jv[jss::Asset] = to_json(xrpIssue()); + jv[jss::Asset2] = to_json(USD.issue()); + jv[field.fieldName] = mpt.getJson(JsonOptions::none); + jv[jss::Flags] = tfSingleAsset; + test(jv); + }; + ammDeposit(sfAmount); + for (SField const& field : + {std::ref(sfAmount2), + std::ref(sfEPrice), + std::ref(sfLPTokenOut)}) + ammDeposit(field); + // AMMWithdraw + auto ammWithdraw = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::AMMWithdraw; + jv[jss::Account] = alice.human(); + jv[jss::Asset] = to_json(xrpIssue()); + jv[jss::Asset2] = to_json(USD.issue()); + jv[jss::Flags] = tfSingleAsset; + jv[field.fieldName] = mpt.getJson(JsonOptions::none); + test(jv); + }; + ammWithdraw(sfAmount); + for (SField const& field : + {std::ref(sfAmount2), + std::ref(sfEPrice), + std::ref(sfLPTokenIn)}) + ammWithdraw(field); + // AMMBid + auto ammBid = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::AMMBid; + jv[jss::Account] = alice.human(); + jv[jss::Asset] = to_json(xrpIssue()); + jv[jss::Asset2] = to_json(USD.issue()); + jv[field.fieldName] = mpt.getJson(JsonOptions::none); + test(jv); + }; + ammBid(sfBidMin); + ammBid(sfBidMax); + // CheckCash + auto checkCash = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::CheckCash; + jv[jss::Account] = alice.human(); + jv[sfCheckID.fieldName] = to_string(uint256{1}); + jv[field.fieldName] = mpt.getJson(JsonOptions::none); + test(jv); + }; + checkCash(sfAmount); + checkCash(sfDeliverMin); + // CheckCreate + { + Json::Value jv; + jv[jss::TransactionType] = jss::CheckCreate; + jv[jss::Account] = alice.human(); + jv[jss::Destination] = carol.human(); + jv[jss::SendMax] = mpt.getJson(JsonOptions::none); + test(jv); + } + // EscrowCreate + { + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowCreate; + jv[jss::Account] = alice.human(); + jv[jss::Destination] = carol.human(); + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + test(jv); + } + // OfferCreate + { + Json::Value const jv = offer(alice, USD(100), mpt); + test(jv); + } + // PaymentChannelCreate + { + Json::Value jv; + jv[jss::TransactionType] = jss::PaymentChannelCreate; + jv[jss::Account] = alice.human(); + jv[jss::Destination] = carol.human(); + jv[jss::SettleDelay] = 1; + jv[sfPublicKey.fieldName] = strHex(alice.pk().slice()); + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + test(jv); + } + // PaymentChannelFund + { + Json::Value jv; + jv[jss::TransactionType] = jss::PaymentChannelFund; + jv[jss::Account] = alice.human(); + jv[sfChannel.fieldName] = to_string(uint256{1}); + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + test(jv); + } + // PaymentChannelClaim + { + Json::Value jv; + jv[jss::TransactionType] = jss::PaymentChannelClaim; + jv[jss::Account] = alice.human(); + jv[sfChannel.fieldName] = to_string(uint256{1}); + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + test(jv); + } + // Payment + auto payment = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::Payment; + jv[jss::Account] = alice.human(); + jv[jss::Destination] = carol.human(); + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + if (field == sfSendMax) + jv[jss::SendMax] = mpt.getJson(JsonOptions::none); + else + jv[jss::DeliverMin] = mpt.getJson(JsonOptions::none); + test(jv); + }; + payment(sfSendMax); + payment(sfDeliverMin); + // NFTokenCreateOffer + { + Json::Value jv; + jv[jss::TransactionType] = jss::NFTokenCreateOffer; + jv[jss::Account] = alice.human(); + jv[sfNFTokenID.fieldName] = to_string(uint256{1}); + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + test(jv); + } + // NFTokenAcceptOffer + { + Json::Value jv; + jv[jss::TransactionType] = jss::NFTokenAcceptOffer; + jv[jss::Account] = alice.human(); + jv[sfNFTokenBrokerFee.fieldName] = + mpt.getJson(JsonOptions::none); + test(jv); + } + // NFTokenMint + { + Json::Value jv; + jv[jss::TransactionType] = jss::NFTokenMint; + jv[jss::Account] = alice.human(); + jv[sfNFTokenTaxon.fieldName] = 1; + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + test(jv); + } + // TrustSet + auto trustSet = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::TrustSet; + jv[jss::Account] = alice.human(); + jv[jss::Flags] = 0; + jv[field.fieldName] = mpt.getJson(JsonOptions::none); + test(jv); + }; + trustSet(sfLimitAmount); + trustSet(sfFee); + // XChainCommit + { + Json::Value const jv = xchain_commit(alice, jvb, 1, mpt); + test(jv); + } + // XChainClaim + { + Json::Value const jv = xchain_claim(alice, jvb, 1, mpt, alice); + test(jv); + } + // XChainCreateClaimID + { + Json::Value const jv = + xchain_create_claim_id(alice, jvb, mpt, alice); + test(jv); + } + // XChainAddClaimAttestation + { + Json::Value const jv = claim_attestation( + alice, + jvb, + alice, + mpt, + alice, + true, + 1, + alice, + signer(alice)); + test(jv); + } + // XChainAddAccountCreateAttestation + { + Json::Value const jv = create_account_attestation( + alice, + jvb, + alice, + mpt, + XRP(10), + alice, + false, + 1, + alice, + signer(alice)); + test(jv); + } + // XChainAccountCreateCommit + { + Json::Value const jv = sidechain_xchain_account_create( + alice, jvb, alice, mpt, XRP(10)); + test(jv); + } + // XChain[Create|Modify]Bridge + auto bridgeTx = [&](Json::StaticString const& tt, + bool minAmount = false) { + Json::Value jv; + jv[jss::TransactionType] = tt; + jv[jss::Account] = alice.human(); + jv[sfXChainBridge.fieldName] = jvb; + jv[sfSignatureReward.fieldName] = + mpt.getJson(JsonOptions::none); + if (minAmount) + jv[sfMinAccountCreateAmount.fieldName] = + mpt.getJson(JsonOptions::none); + test(jv); + }; + bridgeTx(jss::XChainCreateBridge); + bridgeTx(jss::XChainCreateBridge, true); + bridgeTx(jss::XChainModifyBridge); + bridgeTx(jss::XChainModifyBridge, true); + } + BEAST_EXPECT(txWithAmounts.empty()); + } + + void + testTxJsonMetaFields(FeatureBitset features) + { + // checks synthetically parsed mptissuanceid from `tx` response + // it checks the parsing logic + testcase("Test synthetic fields from tx response"); + + using namespace test::jtx; + + Account const alice{"alice"}; + + Env env{*this, features}; + MPTTester mptAlice(env, alice); + + mptAlice.create(); + + std::string const txHash{ + env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; + + Json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta]; + + // Expect mpt_issuance_id field + BEAST_EXPECT(meta.isMember(jss::mpt_issuance_id)); + BEAST_EXPECT( + meta[jss::mpt_issuance_id] == to_string(mptAlice.issuanceID())); + } + + void + testClawbackValidation(FeatureBitset features) + { + testcase("MPT clawback validations"); + using namespace test::jtx; + + // Make sure clawback cannot work when featureMPTokensV1 is disabled + { + Env env(*this, features - featureMPTokensV1); + Account const alice{"alice"}; + Account const bob{"bob"}; + + env.fund(XRP(1000), alice, bob); + env.close(); + + auto const USD = alice["USD"]; + auto const mpt = ripple::test::jtx::MPT( + alice.name(), getMptID(alice.id(), env.seq(alice))); + + env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED)); + env.close(); + + env(claw(alice, mpt(5)), ter(temDISABLED)); + env.close(); + + env(claw(alice, mpt(5), bob), ter(temDISABLED)); + env.close(); + } + + // Test preflight + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + env.fund(XRP(1000), alice, bob); + env.close(); + + auto const USD = alice["USD"]; + auto const mpt = ripple::test::jtx::MPT( + alice.name(), getMptID(alice.id(), env.seq(alice))); + + // clawing back IOU from a MPT holder fails + env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED)); + env.close(); + + // clawing back MPT without specifying a holder fails + env(claw(alice, mpt(5)), ter(temMALFORMED)); + env.close(); + + // clawing back zero amount fails + env(claw(alice, mpt(0), bob), ter(temBAD_AMOUNT)); + env.close(); + + // alice can't claw back from herself + env(claw(alice, mpt(5), alice), ter(temMALFORMED)); + env.close(); + + // can't clawback negative amount + env(claw(alice, mpt(-1), bob), ter(temBAD_AMOUNT)); + env.close(); + } + + // Preclaim - clawback fails when MPTCanClawback is disabled on issuance + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // enable asfAllowTrustLineClawback for alice + env(fset(alice, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(alice, asfAllowTrustLineClawback)); + + // Create issuance without enabling clawback + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &bob}); + + mptAlice.pay(alice, bob, 100); + + // alice cannot clawback before she didn't enable MPTCanClawback + // asfAllowTrustLineClawback has no effect + mptAlice.claw(alice, bob, 1, tecNO_PERMISSION); + } + + // Preclaim - test various scenarios + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + env.fund(XRP(1000), carol); + env.close(); + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + auto const fakeMpt = ripple::test::jtx::MPT( + alice.name(), getMptID(alice.id(), env.seq(alice))); + + // issuer tries to clawback MPT where issuance doesn't exist + env(claw(alice, fakeMpt(5), bob), ter(tecOBJECT_NOT_FOUND)); + env.close(); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback}); + + // alice tries to clawback from someone who doesn't have MPToken + mptAlice.claw(alice, bob, 1, tecOBJECT_NOT_FOUND); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // clawback fails because bob currently has a balance of zero + mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + // carol fails tries to clawback from bob because he is not the + // issuer + mptAlice.claw(carol, bob, 1, tecNO_PERMISSION); + } + + // clawback more than max amount + // fails in the json parser before + // transactor is called + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + env.fund(XRP(1000), alice, bob); + env.close(); + + auto const mpt = ripple::test::jtx::MPT( + alice.name(), getMptID(alice.id(), env.seq(alice))); + + Json::Value jv = claw(alice, mpt(1), bob); + jv[jss::Amount][jss::value] = to_string(maxMPTokenAmount + 1); + Json::Value jv1; + jv1[jss::secret] = alice.name(); + jv1[jss::tx_json] = jv; + auto const jrr = env.rpc("json", "submit", to_string(jv1)); + BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); + } + } + + void + testClawback(FeatureBitset features) + { + testcase("MPT Clawback"); + using namespace test::jtx; + + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + mptAlice.claw(alice, bob, 1); + + mptAlice.claw(alice, bob, 1000); + + // clawback fails because bob currently has a balance of zero + mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS); + } + + // Test that globally locked funds can be clawed + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanLock | tfMPTCanClawback}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + mptAlice.set({.account = &alice, .flags = tfMPTLock}); + + mptAlice.claw(alice, bob, 100); + } + + // Test that individually locked funds can be clawed + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanLock | tfMPTCanClawback}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + mptAlice.set( + {.account = &alice, .holder = &bob, .flags = tfMPTLock}); + + mptAlice.claw(alice, bob, 100); + } + + // Test that unauthorized funds can be clawed back + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanClawback | tfMPTRequireAuth}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // alice authorizes bob + mptAlice.authorize({.account = &alice, .holder = &bob}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + // alice unauthorizes bob + mptAlice.authorize( + {.account = &alice, .holder = &bob, .flags = tfMPTUnauthorize}); + + mptAlice.claw(alice, bob, 100); + } + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + + // MPTokenIssuanceCreate + testCreateValidation(all); + testCreateEnabled(all); + + // MPTokenIssuanceDestroy + testDestroyValidation(all); + testDestroyEnabled(all); + + // MPTokenAuthorize + testAuthorizeValidation(all); + testAuthorizeEnabled(all); + + // MPTokenIssuanceSet + testSetValidation(all); + testSetEnabled(all); + + // MPT clawback + testClawbackValidation(all); + testClawback(all); + + // Test Direct Payment + testPayment(all); + + // Test MPT Amount is invalid in Tx, which don't support MPT + testMPTInvalidInTx(all); + + // Test parsed MPTokenIssuanceID in API response metadata + testTxJsonMetaFields(all); + } +}; + +BEAST_DEFINE_TESTSUITE_PRIO(MPToken, tx, ripple, 2); + +} // namespace ripple diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 2b4245a1ae4..01608b5b805 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -1876,8 +1876,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite jrr = ledgerEntryRoot(env, bob); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - std::to_string( - XRP(10000).value().mantissa() + XRP(250).value().mantissa())); + std::to_string((XRP(10000) + XRP(250)).value().mantissa())); auto jro = ledgerEntryOffer(env, carol, carolOfferSeq); BEAST_EXPECT( @@ -2302,12 +2301,12 @@ class OfferBaseUtil_test : public beast::unit_test::suite jtx::Account const& account, jtx::PrettyAmount const& expectBalance) { - auto const sleTrust = - env.le(keylet::line(account.id(), expectBalance.value().issue())); + auto const sleTrust = env.le( + keylet::line(account.id(), get(expectBalance).issue())); BEAST_EXPECT(sleTrust); if (sleTrust) { - Issue const issue = expectBalance.value().issue(); + Issue const issue = get(expectBalance).issue(); bool const accountLow = account.id() < issue.account; STAmount low{issue}; @@ -5134,12 +5133,12 @@ class OfferBaseUtil_test : public beast::unit_test::suite env(pay(issuer, maker, EUR(1'000))); env.close(); - auto makerUSDBalance = env.balance(maker, USD).value(); - auto takerUSDBalance = env.balance(taker, USD).value(); - auto makerEURBalance = env.balance(maker, EUR).value(); - auto takerEURBalance = env.balance(taker, EUR).value(); - auto makerXRPBalance = env.balance(maker, XRP).value(); - auto takerXRPBalance = env.balance(taker, XRP).value(); + auto makerUSDBalance = get(env.balance(maker, USD)); + auto takerUSDBalance = get(env.balance(taker, USD)); + auto makerEURBalance = get(env.balance(maker, EUR)); + auto takerEURBalance = get(env.balance(taker, EUR)); + auto makerXRPBalance = get(env.balance(maker, XRP)); + auto takerXRPBalance = get(env.balance(taker, XRP)); // tfFillOrKill, TakerPays must be filled { @@ -5162,8 +5161,8 @@ class OfferBaseUtil_test : public beast::unit_test::suite { makerUSDBalance -= USD(100); takerUSDBalance += USD(100); - makerXRPBalance += XRP(100).value(); - takerXRPBalance -= XRP(100).value(); + makerXRPBalance += XRP(100); + takerXRPBalance -= XRP(100); } BEAST_EXPECT(expectOffers(env, taker, 0)); @@ -5181,8 +5180,8 @@ class OfferBaseUtil_test : public beast::unit_test::suite { makerUSDBalance += USD(100); takerUSDBalance -= USD(100); - makerXRPBalance -= XRP(100).value(); - takerXRPBalance += XRP(100).value(); + makerXRPBalance -= XRP(100); + takerXRPBalance += XRP(100); } BEAST_EXPECT(expectOffers(env, taker, 0)); @@ -5217,8 +5216,8 @@ class OfferBaseUtil_test : public beast::unit_test::suite makerUSDBalance -= USD(101); takerUSDBalance += USD(101); - makerXRPBalance += XRP(101).value() - txfee(env, 1); - takerXRPBalance -= XRP(101).value() + txfee(env, 1); + makerXRPBalance += XRP(101) - txfee(env, 1); + takerXRPBalance -= XRP(101) + txfee(env, 1); BEAST_EXPECT(expectOffers(env, taker, 0)); env(offer(maker, USD(101), XRP(101))); @@ -5230,8 +5229,8 @@ class OfferBaseUtil_test : public beast::unit_test::suite makerUSDBalance += USD(101); takerUSDBalance -= USD(101); - makerXRPBalance -= XRP(101).value() + txfee(env, 1); - takerXRPBalance += XRP(101).value() - txfee(env, 1); + makerXRPBalance -= XRP(101) + txfee(env, 1); + takerXRPBalance += XRP(101) - txfee(env, 1); BEAST_EXPECT(expectOffers(env, taker, 0)); env(offer(maker, USD(101), EUR(101))); diff --git a/src/test/app/Path_test.cpp b/src/test/app/Path_test.cpp index 1db15388ff0..cb0f031a360 100644 --- a/src/test/app/Path_test.cpp +++ b/src/test/app/Path_test.cpp @@ -197,7 +197,8 @@ class Path_test : public beast::unit_test::suite STAmount da; if (result.isMember(jss::destination_amount)) - da = amountFromJson(sfGeneric, result[jss::destination_amount]); + da = get( + amountFromJson(sfGeneric, result[jss::destination_amount])); STAmount sa; STPathSet paths; @@ -209,11 +210,12 @@ class Path_test : public beast::unit_test::suite auto const& path = alts[0u]; if (path.isMember(jss::source_amount)) - sa = amountFromJson(sfGeneric, path[jss::source_amount]); + sa = get( + amountFromJson(sfGeneric, path[jss::source_amount])); if (path.isMember(jss::destination_amount)) - da = amountFromJson( - sfGeneric, path[jss::destination_amount]); + da = get(amountFromJson( + sfGeneric, path[jss::destination_amount])); if (path.isMember(jss::paths_computed)) { @@ -1244,8 +1246,7 @@ class Path_test : public beast::unit_test::suite env.close(); env(offer(charlie, XRP(10), USD(10))); env.close(); - auto [st, sa, da] = - find_paths(env, alice, bob, USD(-1), XRP(100).value()); + auto [st, sa, da] = find_paths(env, alice, bob, USD(-1), XRP(100)); BEAST_EXPECT(sa == XRP(10)); BEAST_EXPECT(equal(da, USD(10))); if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1)) @@ -1268,7 +1269,7 @@ class Path_test : public beast::unit_test::suite env(offer(charlie, USD(10), XRP(10))); env.close(); auto [st, sa, da] = - find_paths(env, alice, bob, drops(-1), USD(100).value()); + find_paths(env, alice, bob, drops(-1), USD(100)); BEAST_EXPECT(sa == USD(10)); BEAST_EXPECT(equal(da, XRP(10))); if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1)) diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index e49e5cbd6dc..dedf6fe0bc8 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -65,7 +65,7 @@ struct PayChan_test : public beast::unit_test::suite auto const slep = view.read({ltPAYCHAN, chan}); if (!slep) return XRPAmount{-1}; - return (*slep)[sfAmount]; + return get((*slep)[sfAmount]); } static std::optional @@ -137,9 +137,9 @@ struct PayChan_test : public beast::unit_test::suite { // No signature claim with bad amounts (negative and non-xrp) - auto const iou = USDA(100).value(); - auto const negXRP = XRP(-100).value(); - auto const posXRP = XRP(100).value(); + auto const iou = USDA(100); + auto const negXRP = XRP(-100); + auto const posXRP = XRP(100); env(claim(alice, chan, iou, iou), ter(temBAD_AMOUNT)); env(claim(alice, chan, posXRP, iou), ter(temBAD_AMOUNT)); env(claim(alice, chan, iou, posXRP), ter(temBAD_AMOUNT)); @@ -215,13 +215,7 @@ struct PayChan_test : public beast::unit_test::suite { // Wrong signing key auto const sig = signClaimAuth(bob.pk(), bob.sk(), chan, XRP(1500)); - env(claim( - bob, - chan, - XRP(1500).value(), - XRP(1500).value(), - Slice(sig), - bob.pk()), + env(claim(bob, chan, XRP(1500), XRP(1500), Slice(sig), bob.pk()), ter(temBAD_SIGNER)); BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); @@ -229,13 +223,7 @@ struct PayChan_test : public beast::unit_test::suite { // Bad signature auto const sig = signClaimAuth(bob.pk(), bob.sk(), chan, XRP(1500)); - env(claim( - bob, - chan, - XRP(1500).value(), - XRP(1500).value(), - Slice(sig), - alice.pk()), + env(claim(bob, chan, XRP(1500), XRP(1500), Slice(sig), alice.pk()), ter(temBAD_SIGNATURE)); BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); @@ -551,7 +539,7 @@ struct PayChan_test : public beast::unit_test::suite { // claim the entire amount auto const preBob = env.balance(bob); - env(claim(alice, chan, channelFunds.value(), channelFunds.value())); + env(claim(alice, chan, channelFunds, channelFunds)); BEAST_EXPECT(channelBalance(*env.current(), chan) == channelFunds); BEAST_EXPECT(env.balance(bob) == preBob + channelFunds); } @@ -659,7 +647,7 @@ struct PayChan_test : public beast::unit_test::suite BEAST_EXPECT(channelExists(*env.current(), chan)); env(fset(bob, asfDisallowXRP)); - auto const reqBal = XRP(500).value(); + auto const reqBal = XRP(500); env(claim(alice, chan, reqBal, reqBal), ter(tecNO_TARGET)); } { @@ -673,7 +661,7 @@ struct PayChan_test : public beast::unit_test::suite BEAST_EXPECT(channelExists(*env.current(), chan)); env(fset(bob, asfDisallowXRP)); - auto const reqBal = XRP(500).value(); + auto const reqBal = XRP(500); env(claim(alice, chan, reqBal, reqBal)); } } @@ -741,15 +729,14 @@ struct PayChan_test : public beast::unit_test::suite env.close(); // alice claims. Fails because bob's lsfDepositAuth flag is set. - env(claim(alice, chan, XRP(500).value(), XRP(500).value()), - ter(tecNO_PERMISSION)); + env(claim(alice, chan, XRP(500), XRP(500)), ter(tecNO_PERMISSION)); env.close(); // Claim with signature auto const baseFee = env.current()->fees().base; auto const preBob = env.balance(bob); { - auto const delta = XRP(500).value(); + auto const delta = XRP(500); auto const sig = signClaimAuth(pk, alice.sk(), chan, delta); // alice claims with signature. Fails since bob has @@ -773,7 +760,7 @@ struct PayChan_test : public beast::unit_test::suite } { // Explore the limits of deposit preauthorization. - auto const delta = XRP(600).value(); + auto const delta = XRP(600); auto const sig = signClaimAuth(pk, alice.sk(), chan, delta); // carol claims and fails. Only channel participants (bob or @@ -810,7 +797,7 @@ struct PayChan_test : public beast::unit_test::suite { // bob removes preauthorization of alice. Once again she // cannot submit a claim. - auto const delta = XRP(800).value(); + auto const delta = XRP(800); env(deposit::unauth(bob, alice)); env.close(); @@ -1543,13 +1530,7 @@ struct PayChan_test : public beast::unit_test::suite auto const authAmt = XRP(100); auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); - jv = claim( - bob, - chan, - authAmt.value(), - authAmt.value(), - Slice(sig), - alice.pk()); + jv = claim(bob, chan, authAmt, authAmt, Slice(sig), alice.pk()); jv["PublicKey"] = pkHex.substr(2, pkHex.size() - 2); env(jv, ter(temMALFORMED)); jv["PublicKey"] = pkHex.substr(0, pkHex.size() - 2); diff --git a/src/test/app/TheoreticalQuality_test.cpp b/src/test/app/TheoreticalQuality_test.cpp index 917d23377bf..95e2dd0d9ab 100644 --- a/src/test/app/TheoreticalQuality_test.cpp +++ b/src/test/app/TheoreticalQuality_test.cpp @@ -48,7 +48,7 @@ struct RippleCalcTestParams explicit RippleCalcTestParams(Json::Value const& jv) : srcAccount{*parseBase58(jv[jss::Account].asString())} , dstAccount{*parseBase58(jv[jss::Destination].asString())} - , dstAmt{amountFromJson(sfAmount, jv[jss::Amount])} + , dstAmt{get(amountFromJson(sfAmount, jv[jss::Amount]))} { if (jv.isMember(jss::SendMax)) sendMax = amountFromJson(sfSendMax, jv[jss::SendMax]); diff --git a/src/test/app/XChain_test.cpp b/src/test/app/XChain_test.cpp index 4f24d17601e..21a5ed44c30 100644 --- a/src/test/app/XChain_test.cpp +++ b/src/test/app/XChain_test.cpp @@ -122,13 +122,13 @@ struct SEnv STAmount balance(jtx::Account const& account) const { - return env_.balance(account).value(); + return env_.balance(account); } STAmount balance(jtx::Account const& account, Issue const& issue) const { - return env_.balance(account, issue).value(); + return env_.balance(account, issue); } XRPAmount diff --git a/src/test/jtx.h b/src/test/jtx.h index 6de7cd480fa..49790e34022 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 2c5f2f37062..d90d2bc1228 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -446,6 +446,12 @@ class Env PrettyAmount balance(Account const& account, Issue const& issue) const; + /** Return the number of objects owned by an account. + * Returns 0 if the account does not exist. + */ + std::uint32_t + ownerCount(Account const& account) const; + /** Return an account root. @return empty if the account does not exist. */ diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index c8e0d0c3701..fb5cef102ef 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -73,8 +73,7 @@ constexpr XRPAmount dropsPerXRP{1'000'000}; struct PrettyAmount { private: - // VFALCO TODO should be Amount - STAmount amount_; + STEitherAmount amount_; std::string name_; public: @@ -87,6 +86,10 @@ struct PrettyAmount : amount_(amount), name_(name) { } + PrettyAmount(STMPTAmount const& amount, std::string const& name) + : amount_(amount), name_(name) + { + } /** drops */ template @@ -95,7 +98,7 @@ struct PrettyAmount std::enable_if_t< sizeof(T) >= sizeof(int) && std::is_integral_v && std::is_signed_v>* = nullptr) - : amount_((v > 0) ? v : -v, v < 0) + : amount_(STAmount((v > 0) ? v : -v, v < 0)) { } @@ -105,7 +108,7 @@ struct PrettyAmount T v, std::enable_if_t= sizeof(int) && std::is_unsigned_v>* = nullptr) - : amount_(v) + : amount_(STAmount(v)) { } @@ -120,13 +123,18 @@ struct PrettyAmount return name_; } - STAmount const& + STEitherAmount const& value() const { return amount_; } operator STAmount const &() const + { + return get(amount_); + } + + operator STEitherAmount const &() const { return amount_; } @@ -134,6 +142,13 @@ struct PrettyAmount operator AnyAmount() const; }; +template +A const& +get(PrettyAmount const& pa) +{ + return get(pa.value()); +} + inline bool operator==(PrettyAmount const& lhs, PrettyAmount const& rhs) { @@ -351,6 +366,53 @@ operator<<(std::ostream& os, IOU const& iou); //------------------------------------------------------------------------------ +/** Converts to MPT Issue or STMPTAmount. + + Examples: + MPT Converts to the underlying Issue + MPT(10) Returns STMPTAmount of 10 of + the underlying MPT +*/ +class MPT +{ +public: + std::string name; + ripple::MPTID mptID; + + MPT(std::string const& n, ripple::MPTID const& mptID_) + : name(n), mptID(mptID_) + { + } + + ripple::MPTID const& + mpt() const + { + return mptID; + } + + /** Implicit conversion to MPTIssue. + + This allows passing an MPT + value where an Issue is expected. + */ + operator ripple::MPTID() const + { + return mpt(); + } + + template + requires(sizeof(T) >= sizeof(int) && std::is_arithmetic_v) STMPTAmount + operator()(T v) const + { + return amountFromString(mptID, std::to_string(v)); + } +}; + +std::ostream& +operator<<(std::ostream& os, MPT const& mpt); + +//------------------------------------------------------------------------------ + struct any_t { inline AnyAmount @@ -361,13 +423,22 @@ struct any_t struct AnyAmount { bool is_any; - STAmount value; + STEitherAmount value; AnyAmount() = delete; AnyAmount(AnyAmount const&) = default; AnyAmount& operator=(AnyAmount const&) = default; + AnyAmount(STEitherAmount const& amount) : is_any(false), value(amount) + { + } + + AnyAmount(STEitherAmount const& amount, any_t const*) + : is_any(true), value(amount) + { + } + AnyAmount(STAmount const& amount) : is_any(false), value(amount) { } @@ -377,13 +448,25 @@ struct AnyAmount { } + AnyAmount(STMPTAmount const& amount) : is_any(false), value(amount) + { + } + + AnyAmount(STMPTAmount const& amount, any_t const*) + : is_any(true), value(amount) + { + } + // Reset the issue to a specific account void to(AccountID const& id) { if (!is_any) return; - value.setIssuer(id); + if (value.isIssue()) + get(value).setIssuer(id); + else + Throw("AnyAmount: Amount is not STAmount"); } }; diff --git a/src/test/jtx/attester.h b/src/test/jtx/attester.h index 327fb2e4873..8bb5d57196a 100644 --- a/src/test/jtx/attester.h +++ b/src/test/jtx/attester.h @@ -42,7 +42,7 @@ sign_claim_attestation( SecretKey const& sk, STXChainBridge const& bridge, AccountID const& sendingAccount, - STAmount const& sendingAmount, + STEitherAmount const& sendingAmount, AccountID const& rewardAccount, bool wasLockingChainSend, std::uint64_t claimID, @@ -54,8 +54,8 @@ sign_create_account_attestation( SecretKey const& sk, STXChainBridge const& bridge, AccountID const& sendingAccount, - STAmount const& sendingAmount, - STAmount const& rewardAmount, + STEitherAmount const& sendingAmount, + STEitherAmount const& rewardAmount, AccountID const& rewardAccount, bool wasLockingChainSend, std::uint64_t createCount, diff --git a/src/test/jtx/delivermin.h b/src/test/jtx/delivermin.h index 46e633dab20..95ca676c292 100644 --- a/src/test/jtx/delivermin.h +++ b/src/test/jtx/delivermin.h @@ -31,12 +31,15 @@ namespace jtx { class delivermin { private: - STAmount amount_; + STEitherAmount amount_; public: delivermin(STAmount const& amount) : amount_(amount) { } + delivermin(STMPTAmount const& amount) : amount_(amount) + { + } void operator()(Env&, JTx& jtx) const; diff --git a/src/test/jtx/impl/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index 575e2e1d889..b7e4af927dd 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -249,7 +249,8 @@ AMMTest::find_paths( STAmount da; if (result.isMember(jss::destination_amount)) - da = amountFromJson(sfGeneric, result[jss::destination_amount]); + da = get( + amountFromJson(sfGeneric, result[jss::destination_amount])); STAmount sa; STPathSet paths; @@ -261,10 +262,12 @@ AMMTest::find_paths( auto const& path = alts[0u]; if (path.isMember(jss::source_amount)) - sa = amountFromJson(sfGeneric, path[jss::source_amount]); + sa = get( + amountFromJson(sfGeneric, path[jss::source_amount])); if (path.isMember(jss::destination_amount)) - da = amountFromJson(sfGeneric, path[jss::destination_amount]); + da = get( + amountFromJson(sfGeneric, path[jss::destination_amount])); if (path.isMember(jss::paths_computed)) { diff --git a/src/test/jtx/impl/amount.cpp b/src/test/jtx/impl/amount.cpp index 01fa5369592..752d68ef1fd 100644 --- a/src/test/jtx/impl/amount.cpp +++ b/src/test/jtx/impl/amount.cpp @@ -72,29 +72,40 @@ to_places(const T d, std::uint8_t places) std::ostream& operator<<(std::ostream& os, PrettyAmount const& amount) { - if (amount.value().native()) + if (amount.value().isIssue()) { - // measure in hundredths - auto const c = dropsPerXRP.drops() / 100; - auto const n = amount.value().mantissa(); - if (n < c) + auto const& stAmount = get(amount); + if (stAmount.native()) { - if (amount.value().negative()) - os << "-" << n << " drops"; - else - os << n << " drops"; - return os; + // measure in hundredths + auto const c = dropsPerXRP.drops() / 100; + auto const n = stAmount.mantissa(); + if (n < c) + { + if (stAmount.negative()) + os << "-" << n << " drops"; + else + os << n << " drops"; + return os; + } + auto const d = double(n) / dropsPerXRP.drops(); + if (stAmount.negative()) + os << "-"; + + os << to_places(d, 6) << " XRP"; + } + else + { + os << stAmount.getText() << "/" + << to_string(stAmount.issue().currency) << "(" << amount.name() + << ")"; } - auto const d = double(n) / dropsPerXRP.drops(); - if (amount.value().negative()) - os << "-"; - - os << to_places(d, 6) << " XRP"; } else { - os << amount.value().getText() << "/" - << to_string(amount.value().issue().currency) << "(" << amount.name() + auto const& mptAmount = get(amount); + os << mptAmount.getText() << "/" + << to_string(mptAmount.issue().getMptID()) << "(" << amount.name() << ")"; } return os; diff --git a/src/test/jtx/impl/attester.cpp b/src/test/jtx/impl/attester.cpp index 66be9da83b3..02a61ffe9bd 100644 --- a/src/test/jtx/impl/attester.cpp +++ b/src/test/jtx/impl/attester.cpp @@ -35,7 +35,7 @@ sign_claim_attestation( SecretKey const& sk, STXChainBridge const& bridge, AccountID const& sendingAccount, - STAmount const& sendingAmount, + STEitherAmount const& sendingAmount, AccountID const& rewardAccount, bool wasLockingChainSend, std::uint64_t claimID, @@ -58,8 +58,8 @@ sign_create_account_attestation( SecretKey const& sk, STXChainBridge const& bridge, AccountID const& sendingAccount, - STAmount const& sendingAmount, - STAmount const& rewardAmount, + STEitherAmount const& sendingAmount, + STEitherAmount const& rewardAmount, AccountID const& rewardAccount, bool wasLockingChainSend, std::uint64_t createCount, diff --git a/src/test/jtx/impl/envconfig.cpp b/src/test/jtx/impl/envconfig.cpp index c9788a6d70f..94e02132c35 100644 --- a/src/test/jtx/impl/envconfig.cpp +++ b/src/test/jtx/impl/envconfig.cpp @@ -45,8 +45,8 @@ setupConfigForUnitTests(Config& cfg) // Default fees to old values, so tests don't have to worry about changes in // Config.h cfg.FEES.reference_fee = 10; - cfg.FEES.account_reserve = XRP(200).value().xrp().drops(); - cfg.FEES.owner_reserve = XRP(50).value().xrp().drops(); + cfg.FEES.account_reserve = get(XRP(200)).xrp().drops(); + cfg.FEES.owner_reserve = get(XRP(50)).xrp().drops(); // The Beta API (currently v2) is always available to tests cfg.BETA_RPC_API = true; diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp new file mode 100644 index 00000000000..88f206796bf --- /dev/null +++ b/src/test/jtx/impl/mpt.cpp @@ -0,0 +1,407 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include + +namespace ripple { +namespace test { +namespace jtx { + +static std::array +uint64ToByteArray(std::uint64_t value) +{ + value = boost::endian::native_to_big(value); + std::array result; + std::memcpy(result.data(), &value, sizeof(value)); + return result; +} + +void +mptflags::operator()(Env& env) const +{ + env.test.expect(tester_.checkFlags(flags_, holder_)); +} + +void +mptpay::operator()(Env& env) const +{ + env.test.expect(amount_ == tester_.getAmount(account_)); +} + +void +requireAny::operator()(Env& env) const +{ + env.test.expect(cb_()); +} + +std::unordered_map +MPTTester::makeHolders(std::vector const& holders) +{ + std::unordered_map accounts; + for (auto const& h : holders) + { + assert(h && holders_.find(h->human()) == accounts.cend()); + accounts.emplace(h->human(), h); + } + return accounts; +} + +MPTTester::MPTTester(Env& env, Account const& issuer, MPTConstr const& arg) + : env_(env) + , issuer_(issuer) + , holders_(makeHolders(arg.holders)) + , close_(arg.close) +{ + if (arg.fund) + { + env_.fund(arg.xrp, issuer_); + for (auto it : holders_) + env_.fund(arg.xrpHolders, *it.second); + } + if (close_) + env.close(); + if (arg.fund) + { + env_.require(owners(issuer_, 0)); + for (auto it : holders_) + { + assert(issuer_.id() != it.second->id()); + env_.require(owners(*it.second, 0)); + } + } +} + +void +MPTTester::create(const MPTCreate& arg) +{ + if (issuanceKey_) + Throw("MPT can't be reused"); + id_ = getMptID(issuer_.id(), env_.seq(issuer_)); + issuanceKey_ = keylet::mptIssuance(*id_).key; + Json::Value jv; + jv[sfAccount.jsonName] = issuer_.human(); + jv[sfTransactionType.jsonName] = jss::MPTokenIssuanceCreate; + if (arg.assetScale) + jv[sfAssetScale.jsonName] = *arg.assetScale; + if (arg.transferFee) + jv[sfTransferFee.jsonName] = *arg.transferFee; + if (arg.metadata) + jv[sfMPTokenMetadata.jsonName] = strHex(*arg.metadata); + + // convert maxAmt to hex string, since json doesn't accept 64-bit int + if (arg.maxAmt) + jv[sfMaximumAmount.jsonName] = strHex(uint64ToByteArray(*arg.maxAmt)); + if (submit(arg, jv) != tesSUCCESS) + { + // Verify issuance doesn't exist + env_.require(requireAny([&]() -> bool { + return env_.le(keylet::mptIssuance(*id_)) == nullptr; + })); + + id_.reset(); + issuanceKey_.reset(); + } + else if (arg.flags) + env_.require(mptflags(*this, *arg.flags)); +} + +void +MPTTester::destroy(MPTDestroy const& arg) +{ + Json::Value jv; + if (arg.issuer) + jv[sfAccount.jsonName] = arg.issuer->human(); + else + jv[sfAccount.jsonName] = issuer_.human(); + if (arg.id) + jv[sfMPTokenIssuanceID.jsonName] = to_string(*arg.id); + else + { + assert(id_); + jv[sfMPTokenIssuanceID.jsonName] = to_string(*id_); + } + jv[sfTransactionType.jsonName] = jss::MPTokenIssuanceDestroy; + submit(arg, jv); +} + +Account const& +MPTTester::holder(std::string const& holder_) const +{ + auto const& it = holders_.find(holder_); + assert(it != holders_.cend()); + if (it == holders_.cend()) + Throw("Holder is not found"); + return *it->second; +} + +void +MPTTester::authorize(MPTAuthorize const& arg) +{ + Json::Value jv; + if (arg.account) + jv[sfAccount.jsonName] = arg.account->human(); + else + jv[sfAccount.jsonName] = issuer_.human(); + jv[sfTransactionType.jsonName] = jss::MPTokenAuthorize; + if (arg.id) + jv[sfMPTokenIssuanceID.jsonName] = to_string(*arg.id); + else + { + assert(id_); + jv[sfMPTokenIssuanceID.jsonName] = to_string(*id_); + } + if (arg.holder) + jv[sfMPTokenHolder.jsonName] = arg.holder->human(); + if (auto const result = submit(arg, jv); result == tesSUCCESS) + { + // Issuer authorizes + if (arg.account == nullptr || *arg.account == issuer_) + { + auto const flags = getFlags(arg.holder); + // issuer un-authorizes the holder + if (arg.flags.value_or(0) == tfMPTUnauthorize) + env_.require(mptflags(*this, flags, arg.holder)); + // issuer authorizes the holder + else + env_.require( + mptflags(*this, flags | lsfMPTAuthorized, arg.holder)); + } + // Holder authorizes + else if (arg.flags.value_or(0) == 0) + { + auto const flags = getFlags(arg.account); + // holder creates a token + env_.require(mptflags(*this, flags, arg.account)); + env_.require(mptpay(*this, *arg.account, 0)); + } + } + else if ( + arg.account != nullptr && *arg.account != issuer_ && + arg.flags.value_or(0) == 0 && issuanceKey_) + { + if (result == tecMPTOKEN_EXISTS) + { + // Verify that MPToken already exists + env_.require(requireAny([&]() -> bool { + return env_.le(keylet::mptoken( + *issuanceKey_, arg.account->id())) != nullptr; + })); + } + else + { + // Verify MPToken doesn't exist if holder failed authorizing(unless + // it already exists) + env_.require(requireAny([&]() -> bool { + return env_.le(keylet::mptoken( + *issuanceKey_, arg.account->id())) == nullptr; + })); + } + } +} + +void +MPTTester::set(MPTSet const& arg) +{ + Json::Value jv; + if (arg.account) + jv[sfAccount.jsonName] = arg.account->human(); + else + jv[sfAccount.jsonName] = issuer_.human(); + jv[sfTransactionType.jsonName] = jss::MPTokenIssuanceSet; + if (arg.id) + jv[sfMPTokenIssuanceID.jsonName] = to_string(*arg.id); + else + { + assert(id_); + jv[sfMPTokenIssuanceID.jsonName] = to_string(*id_); + } + if (arg.holder) + jv[sfMPTokenHolder.jsonName] = arg.holder->human(); + if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0)) + { + auto require = [&](AccountP holder, bool unchanged) { + auto flags = getFlags(holder); + if (!unchanged) + { + if (*arg.flags & tfMPTLock) + flags |= lsfMPTLocked; + else if (*arg.flags & tfMPTUnlock) + flags &= ~lsfMPTLocked; + else + assert(0); + } + env_.require(mptflags(*this, flags, holder)); + }; + if (arg.account) + require(nullptr, arg.holder != nullptr); + if (arg.holder) + require(arg.holder, false); + } +} + +bool +MPTTester::forObject( + std::function const& cb, + AccountP holder_) const +{ + assert(issuanceKey_); + auto const key = [&]() { + if (holder_) + return keylet::mptoken(*issuanceKey_, holder_->id()); + return keylet::mptIssuance(*issuanceKey_); + }(); + if (auto const sle = env_.le(key)) + return cb(sle); + return false; +} + +[[nodiscard]] bool +MPTTester::checkMPTokenAmount( + Account const& holder_, + std::int64_t expectedAmount) const +{ + return forObject( + [&](SLEP const& sle) { return expectedAmount == (*sle)[sfMPTAmount]; }, + &holder_); +} + +[[nodiscard]] bool +MPTTester::checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const +{ + return forObject([&](SLEP const& sle) { + return expectedAmount == (*sle)[sfOutstandingAmount]; + }); +} + +[[nodiscard]] bool +MPTTester::checkFlags(uint32_t const expectedFlags, AccountP holder) const +{ + return expectedFlags == getFlags(holder); +} + +void +MPTTester::pay( + Account const& src, + Account const& dest, + std::int64_t amount, + std::optional err) +{ + assert(id_); + auto const srcAmt = getAmount(src); + auto const destAmt = getAmount(dest); + auto const outstnAmt = getAmount(issuer_); + if (err) + env_(jtx::pay(src, dest, mpt(amount)), ter(*err)); + else + env_(jtx::pay(src, dest, mpt(amount))); + if (env_.ter() != tesSUCCESS) + amount = 0; + if (close_) + env_.close(); + if (src == issuer_) + { + env_.require(mptpay(*this, src, srcAmt + amount)); + env_.require(mptpay(*this, dest, destAmt + amount)); + } + else if (dest == issuer_) + { + env_.require(mptpay(*this, src, srcAmt - amount)); + env_.require(mptpay(*this, dest, destAmt - amount)); + } + else + { + auto const actual = static_cast( + amount * Number{transferRate(*env_.current(), *id_).value, -9}); + // Sender pays the transfer fee if any + env_.require(mptpay(*this, src, srcAmt - actual)); + env_.require(mptpay(*this, dest, destAmt + amount)); + // Outstanding amount is reduced by the transfer fee if any + env_.require(mptpay(*this, issuer_, outstnAmt - (actual - amount))); + } +} + +void +MPTTester::claw( + Account const& issuer, + Account const& holder, + std::int64_t amount, + std::optional err) +{ + assert(id_); + auto const issuerAmt = getAmount(issuer); + auto const holderAmt = getAmount(holder); + if (err) + env_(jtx::claw(issuer, mpt(amount), holder), ter(*err)); + else + env_(jtx::claw(issuer, mpt(amount), holder)); + if (env_.ter() != tesSUCCESS) + amount = 0; + if (close_) + env_.close(); + + env_.require( + mptpay(*this, issuer, issuerAmt - std::min(holderAmt, amount))); + env_.require( + mptpay(*this, holder, holderAmt - std::min(holderAmt, amount))); +} + +STMPTAmount +MPTTester::mpt(std::int64_t amount) const +{ + assert(id_); + return ripple::test::jtx::MPT(issuer_.name(), *id_)(amount); +} + +std::int64_t +MPTTester::getAmount(Account const& account) const +{ + assert(issuanceKey_); + if (account == issuer_) + { + if (auto const sle = env_.le(keylet::mptIssuance(*issuanceKey_))) + return sle->getFieldU64(sfOutstandingAmount); + } + else + { + if (auto const sle = + env_.le(keylet::mptoken(*issuanceKey_, account.id()))) + return sle->getFieldU64(sfMPTAmount); + } + return 0; +} + +std::uint32_t +MPTTester::getFlags(ripple::test::jtx::AccountP holder) const +{ + std::uint32_t flags = 0; + if (!forObject( + [&](SLEP const& sle) { + flags = sle->getFlags(); + return true; + }, + holder)) + Throw("Failed to get the flags"); + return flags; +} + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/offer.cpp b/src/test/jtx/impl/offer.cpp index 55a1af4beab..d6e2d89bc21 100644 --- a/src/test/jtx/impl/offer.cpp +++ b/src/test/jtx/impl/offer.cpp @@ -27,8 +27,8 @@ namespace jtx { Json::Value offer( Account const& account, - STAmount const& takerPays, - STAmount const& takerGets, + STEitherAmount const& takerPays, + STEitherAmount const& takerGets, std::uint32_t flags) { Json::Value jv; diff --git a/src/test/jtx/impl/paths.cpp b/src/test/jtx/impl/paths.cpp index 393e36e9d61..8c58f81a627 100644 --- a/src/test/jtx/impl/paths.cpp +++ b/src/test/jtx/impl/paths.cpp @@ -31,7 +31,8 @@ paths::operator()(Env& env, JTx& jt) const auto& jv = jt.jv; auto const from = env.lookup(jv[jss::Account].asString()); auto const to = env.lookup(jv[jss::Destination].asString()); - auto const amount = amountFromJson(sfAmount, jv[jss::Amount]); + auto const amount = + get(amountFromJson(sfAmount, jv[jss::Amount])); Pathfinder pf( std::make_shared( env.current(), env.app().journal("RippleLineCache")), diff --git a/src/test/jtx/impl/trust.cpp b/src/test/jtx/impl/trust.cpp index 641a0f79f28..94f9d49ed46 100644 --- a/src/test/jtx/impl/trust.cpp +++ b/src/test/jtx/impl/trust.cpp @@ -64,13 +64,19 @@ trust( } Json::Value -claw(Account const& account, STAmount const& amount) +claw( + Account const& account, + STEitherAmount const& amount, + std::optional const& mptHolder) { Json::Value jv; jv[jss::Account] = account.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); jv[jss::TransactionType] = jss::Clawback; + if (mptHolder) + jv[sfMPTokenHolder.jsonName] = mptHolder->human(); + return jv; } diff --git a/src/test/jtx/impl/xchain_bridge.cpp b/src/test/jtx/impl/xchain_bridge.cpp index 43b0e7c2f96..32d24c0027a 100644 --- a/src/test/jtx/impl/xchain_bridge.cpp +++ b/src/test/jtx/impl/xchain_bridge.cpp @@ -115,7 +115,7 @@ Json::Value xchain_create_claim_id( Account const& acc, Json::Value const& bridge, - STAmount const& reward, + STEitherAmount const& reward, Account const& otherChainSource) { Json::Value jv; diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h new file mode 100644 index 00000000000..e96b73f039a --- /dev/null +++ b/src/test/jtx/mpt.h @@ -0,0 +1,264 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_MPT_H_INCLUDED +#define RIPPLE_TEST_JTX_MPT_H_INCLUDED + +#include +#include +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace { +using AccountP = Account const*; +} + +class MPTTester; + +// Check flags settings on MPT create +class mptflags +{ +private: + MPTTester& tester_; + std::uint32_t flags_; + AccountP holder_; + +public: + mptflags(MPTTester& tester, std::uint32_t flags, AccountP holder = nullptr) + : tester_(tester), flags_(flags), holder_(holder) + { + } + + void + operator()(Env& env) const; +}; + +// Check mptissuance or mptoken amount balances on payment +class mptpay +{ +private: + MPTTester const& tester_; + Account const& account_; + std::int64_t const amount_; + +public: + mptpay(MPTTester& tester, Account const& account, std::int64_t amount) + : tester_(tester), account_(account), amount_(amount) + { + } + + void + operator()(Env& env) const; +}; + +class requireAny +{ +private: + std::function cb_; + +public: + requireAny(std::function const& cb) : cb_(cb) + { + } + + void + operator()(Env& env) const; +}; + +struct MPTConstr +{ + std::vector holders = {}; + PrettyAmount const& xrp = XRP(10'000); + PrettyAmount const& xrpHolders = XRP(10'000); + bool fund = true; + bool close = true; +}; + +struct MPTCreate +{ + std::optional maxAmt = std::nullopt; + std::optional assetScale = std::nullopt; + std::optional transferFee = std::nullopt; + std::optional metadata = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + bool fund = true; + std::optional flags = {0}; + std::optional err = std::nullopt; +}; + +struct MPTDestroy +{ + AccountP issuer = nullptr; + std::optional id = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; + std::optional err = std::nullopt; +}; + +struct MPTAuthorize +{ + AccountP account = nullptr; + AccountP holder = nullptr; + std::optional id = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; + std::optional err = std::nullopt; +}; + +struct MPTSet +{ + AccountP account = nullptr; + AccountP holder = nullptr; + std::optional id = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; + std::optional err = std::nullopt; +}; + +class MPTTester +{ + Env& env_; + Account const& issuer_; + std::unordered_map const holders_; + std::optional id_; + std::optional issuanceKey_; + bool close_; + +public: + MPTTester(Env& env, Account const& issuer, MPTConstr const& constr = {}); + + void + create(MPTCreate const& arg = MPTCreate{}); + + void + destroy(MPTDestroy const& arg = MPTDestroy{}); + + void + authorize(MPTAuthorize const& arg = MPTAuthorize{}); + + void + set(MPTSet const& set = {}); + + [[nodiscard]] bool + checkMPTokenAmount(Account const& holder, std::int64_t expectedAmount) + const; + + [[nodiscard]] bool + checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const; + + [[nodiscard]] bool + checkFlags(uint32_t const expectedFlags, AccountP holder = nullptr) const; + + Account const& + issuer() const + { + return issuer_; + } + Account const& + holder(std::string const& h) const; + + void + pay(Account const& src, + Account const& dest, + std::int64_t amount, + std::optional err = std::nullopt); + + void + claw( + Account const& issuer, + Account const& holder, + std::int64_t amount, + std::optional err = std::nullopt); + + STMPTAmount + mpt(std::int64_t amount) const; + + uint256 const& + issuanceKey() const + { + assert(issuanceKey_); + return *issuanceKey_; + } + + MPTID const& + issuanceID() const + { + assert(id_); + return *id_; + } + + std::int64_t + getAmount(Account const& account) const; + +private: + using SLEP = std::shared_ptr; + bool + forObject( + std::function const& cb, + AccountP holder = nullptr) const; + + template + TER + submit(A const& arg, Json::Value const& jv) + { + if (arg.err) + { + if (arg.flags && arg.flags > 0) + env_(jv, txflags(*arg.flags), ter(*arg.err)); + else + env_(jv, ter(*arg.err)); + } + else if (arg.flags && arg.flags > 0) + env_(jv, txflags(*arg.flags)); + else + env_(jv); + auto const err = env_.ter(); + if (close_) + env_.close(); + if (arg.ownerCount) + env_.require(owners(issuer_, *arg.ownerCount)); + if (arg.holderCount) + { + for (auto it : holders_) + env_.require(owners(*it.second, *arg.holderCount)); + } + return err; + } + + std::unordered_map + makeHolders(std::vector const& holders); + + std::uint32_t + getFlags(AccountP holder) const; +}; + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/offer.h b/src/test/jtx/offer.h index 3951f4f934a..2d66b24436e 100644 --- a/src/test/jtx/offer.h +++ b/src/test/jtx/offer.h @@ -32,8 +32,8 @@ namespace jtx { Json::Value offer( Account const& account, - STAmount const& takerPays, - STAmount const& takerGets, + STEitherAmount const& takerPays, + STEitherAmount const& takerGets, std::uint32_t flags = 0); /** Cancel an offer. */ diff --git a/src/test/jtx/sendmax.h b/src/test/jtx/sendmax.h index 495c61c33b8..86f23dc6894 100644 --- a/src/test/jtx/sendmax.h +++ b/src/test/jtx/sendmax.h @@ -31,12 +31,15 @@ namespace jtx { class sendmax { private: - STAmount amount_; + STEitherAmount amount_; public: sendmax(STAmount const& amount) : amount_(amount) { } + sendmax(STMPTAmount const& amount) : amount_(amount) + { + } void operator()(Env&, JTx& jtx) const; diff --git a/src/test/jtx/trust.h b/src/test/jtx/trust.h index f9fddf4871a..03e045591ce 100644 --- a/src/test/jtx/trust.h +++ b/src/test/jtx/trust.h @@ -41,7 +41,10 @@ trust( std::uint32_t flags); Json::Value -claw(Account const& account, STAmount const& amount); +claw( + Account const& account, + STEitherAmount const& amount, + std::optional const& mptHolder = std::nullopt); } // namespace jtx } // namespace test diff --git a/src/test/jtx/xchain_bridge.h b/src/test/jtx/xchain_bridge.h index 9968317c8de..dd219d0dfcd 100644 --- a/src/test/jtx/xchain_bridge.h +++ b/src/test/jtx/xchain_bridge.h @@ -61,7 +61,7 @@ Json::Value xchain_create_claim_id( Account const& acc, Json::Value const& bridge, - STAmount const& reward, + STEitherAmount const& reward, Account const& otherChainSource); Json::Value diff --git a/src/test/protocol/STAmount_test.cpp b/src/test/protocol/STAmount_test.cpp index e48d0500ba6..532a0b984a3 100644 --- a/src/test/protocol/STAmount_test.cpp +++ b/src/test/protocol/STAmount_test.cpp @@ -34,7 +34,7 @@ class STAmount_test : public beast::unit_test::suite s.add(ser); SerialIter sit(ser.slice()); - return STAmount(sit, sfGeneric); + return STAmount(sit); } //-------------------------------------------------------------------------- diff --git a/src/test/protocol/STObject_test.cpp b/src/test/protocol/STObject_test.cpp index 39a5b9c2f65..b55c1959363 100644 --- a/src/test/protocol/STObject_test.cpp +++ b/src/test/protocol/STObject_test.cpp @@ -560,10 +560,10 @@ class STObject_test : public beast::unit_test::suite { STObject st(sfGeneric); - st[sfAmount] = STAmount{}; + st[sfAmount] = STEitherAmount{}; st[sfAccount] = AccountID{}; st[sfDigest] = uint256{}; - [&](STAmount) {}(st[sfAmount]); + [&](STEitherAmount) {}(st[sfAmount]); [&](AccountID) {}(st[sfAccount]); [&](uint256) {}(st[sfDigest]); } diff --git a/src/test/protocol/STTx_test.cpp b/src/test/protocol/STTx_test.cpp index e0f6796af33..934bd8050c0 100644 --- a/src/test/protocol/STTx_test.cpp +++ b/src/test/protocol/STTx_test.cpp @@ -1344,22 +1344,22 @@ class STTx_test : public beast::unit_test::suite 0x73, 0x00, 0x81, 0x14, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe5, 0xfe, 0xf3, 0xe7, 0xe5, 0x65, 0x24, 0x00, 0x00, 0x00, 0x00, - 0x20, 0x1e, 0x00, 0x6f, 0x00, 0x00, 0x20, 0x1f, 0x03, 0xf6, 0x00, - 0x00, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0x59, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x40, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x02, 0x00, 0x12, 0x00, 0x65, 0x24, 0x00, 0x00, 0x00, 0x00, - 0x20, 0x1e, 0x00, 0x4f, 0x00, 0x00, 0x20, 0x1f, 0x03, 0xf6, 0x00, - 0x00, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x35, 0x24, 0x59, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x40, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x02, 0x00, 0x54, 0x72, 0x61, 0x6e, 0x00, 0x10, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x65, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xe5, 0xfe, 0xf3, 0xe7, 0xe5, 0x65, 0x24, 0x00, - 0x00, 0x00, 0x00, 0x20, 0x1e, 0x00, 0x6f, 0x00, 0x00, 0x20, 0xf6, - 0x00, 0x00, 0x03, 0x1f, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x35, - 0x00, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x40, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x73, 0x00, 0x81, 0x14, 0x00, - 0x10, 0x00, 0x73, 0x00, 0x81, 0x14, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0xe5, 0xfe}; + 0x20, 0x1e, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x20, 0x1f, 0x03, 0xf6, + 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0x59, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x12, 0x00, 0x65, 0x24, 0x00, 0x00, 0x00, + 0x00, 0x20, 0x1e, 0x00, 0x4f, 0x00, 0x00, 0x20, 0x1f, 0x03, 0xf6, + 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x35, 0x24, 0x59, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x54, 0x72, 0x61, 0x6e, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x24, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xe5, 0xfe, 0xf3, 0xe7, 0xe5, 0x65, 0x24, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x1e, 0x00, 0x6f, 0x00, 0x00, 0x20, + 0xf6, 0x00, 0x00, 0x03, 0x1f, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x35, 0x00, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x73, 0x00, 0x81, 0x14, + 0x00, 0x10, 0x00, 0x73, 0x00, 0x81, 0x14, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0xe5, 0xfe}; // Construct an STObject with 11 levels of object nesting so the // maximum nesting level exception is thrown. diff --git a/src/test/rpc/AccountSet_test.cpp b/src/test/rpc/AccountSet_test.cpp index e5475e3f530..b84f7f191f3 100644 --- a/src/test/rpc/AccountSet_test.cpp +++ b/src/test/rpc/AccountSet_test.cpp @@ -388,7 +388,7 @@ class AccountSet_test : public beast::unit_test::suite auto const amount = USD(1); Rate const rate(transferRate * QUALITY_ONE); auto const amountWithRate = - toAmount(multiply(amount.value(), rate)); + toAmount(multiply(amount, rate)); env(pay(gw, alice, USD(10))); env.close(); @@ -455,7 +455,7 @@ class AccountSet_test : public beast::unit_test::suite auto const amount = USD(1); auto const amountWithRate = toAmount( - multiply(amount.value(), Rate(transferRate * QUALITY_ONE))); + multiply(amount, Rate(transferRate * QUALITY_ONE))); env(pay(gw, alice, USD(10))); env(pay(alice, bob, amount), sendmax(USD(10))); diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp b/src/xrpld/app/ledger/detail/LedgerToJson.cpp index 95b572e9736..11f42cd66ec 100644 --- a/src/xrpld/app/ledger/detail/LedgerToJson.cpp +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -157,6 +158,12 @@ fillJsonTx( fill.ledger, txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); + + // If applicable, insert mpt issuance id + RPC::insertMPTokenIssuanceID( + txJson[jss::meta], + txn, + {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } if (!fill.ledger.open()) @@ -188,6 +195,12 @@ fillJsonTx( fill.ledger, txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); + + // If applicable, insert mpt issuance id + RPC::insertMPTokenIssuanceID( + txJson[jss::metaData], + txn, + {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } } diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index a7ee935f102..141de7e6b1b 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -3117,6 +3118,8 @@ NetworkOPsImp::transJson( jvObj[jss::meta] = meta->get().getJson(JsonOptions::none); RPC::insertDeliveredAmount( jvObj[jss::meta], *ledger, transaction, meta->get()); + RPC::insertMPTokenIssuanceID( + jvObj[jss::meta], transaction, meta->get()); } if (!ledger->open()) diff --git a/src/xrpld/app/tx/detail/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index 237e1afa240..3ed35d72656 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -47,7 +47,7 @@ AMMCreate::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - auto const amount = ctx.tx[sfAmount]; + auto const amount = get(ctx.tx[sfAmount]); auto const amount2 = ctx.tx[sfAmount2]; if (amount.issue() == amount2.issue()) @@ -89,7 +89,7 @@ TER AMMCreate::preclaim(PreclaimContext const& ctx) { auto const accountID = ctx.tx[sfAccount]; - auto const amount = ctx.tx[sfAmount]; + auto const amount = get(ctx.tx[sfAmount]); auto const amount2 = ctx.tx[sfAmount2]; // Check if AMM already exists for the token pair @@ -208,7 +208,7 @@ applyCreate( AccountID const& account_, beast::Journal j_) { - auto const amount = ctx_.tx[sfAmount]; + auto const amount = get(ctx_.tx[sfAmount]); auto const amount2 = ctx_.tx[sfAmount2]; auto const ammKeylet = keylet::amm(amount.issue(), amount2.issue()); diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 9bbf5b4a60a..75993ad4189 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -48,7 +48,7 @@ AMMDeposit::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - auto const amount = ctx.tx[~sfAmount]; + auto const amount = get(ctx.tx[~sfAmount]); auto const amount2 = ctx.tx[~sfAmount2]; auto const ePrice = ctx.tx[~sfEPrice]; auto const lpTokens = ctx.tx[~sfLPTokenOut]; @@ -244,7 +244,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) : tecUNFUNDED_AMM; }; - auto const amount = ctx.tx[~sfAmount]; + auto const amount = get(ctx.tx[~sfAmount]); auto const amount2 = ctx.tx[~sfAmount2]; auto const ammAccountID = ammSle->getAccountID(sfAccount); @@ -339,7 +339,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) std::pair AMMDeposit::applyGuts(Sandbox& sb) { - auto const amount = ctx_.tx[~sfAmount]; + auto const amount = get(ctx_.tx[~sfAmount]); auto const amount2 = ctx_.tx[~sfAmount2]; auto const ePrice = ctx_.tx[~sfEPrice]; auto const lpTokensDeposit = ctx_.tx[~sfLPTokenOut]; diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 51b512aba0a..394fd79dd8c 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -48,7 +48,7 @@ AMMWithdraw::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - auto const amount = ctx.tx[~sfAmount]; + auto const amount = get(ctx.tx[~sfAmount]); auto const amount2 = ctx.tx[~sfAmount2]; auto const ePrice = ctx.tx[~sfEPrice]; auto const lpTokens = ctx.tx[~sfLPTokenIn]; @@ -181,7 +181,7 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) return terNO_AMM; } - auto const amount = ctx.tx[~sfAmount]; + auto const amount = get(ctx.tx[~sfAmount]); auto const amount2 = ctx.tx[~sfAmount2]; auto const expected = ammHolds( @@ -295,7 +295,7 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) std::pair AMMWithdraw::applyGuts(Sandbox& sb) { - auto const amount = ctx_.tx[~sfAmount]; + auto const amount = get(ctx_.tx[~sfAmount]); auto const amount2 = ctx_.tx[~sfAmount2]; auto const ePrice = ctx_.tx[~sfEPrice]; auto ammSle = sb.peek(keylet::amm(ctx_.tx[sfAsset], ctx_.tx[sfAsset2])); diff --git a/src/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index 8b5ef79b6d4..585bf9dcc99 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -50,7 +50,7 @@ CashCheck::preflight(PreflightContext const& ctx) } // Exactly one of Amount or DeliverMin must be present. - auto const optAmount = ctx.tx[~sfAmount]; + auto const optAmount = get(ctx.tx[~sfAmount]); auto const optDeliverMin = ctx.tx[~sfDeliverMin]; if (static_cast(optAmount) == static_cast(optDeliverMin)) @@ -136,7 +136,7 @@ CashCheck::preclaim(PreclaimContext const& ctx) // Preflight verified exactly one of Amount or DeliverMin is present. // Make sure the requested amount is reasonable. STAmount const value{[](STTx const& tx) { - auto const optAmount = tx[~sfAmount]; + auto const optAmount = get(tx[~sfAmount]); return optAmount ? *optAmount : tx[sfDeliverMin]; }(ctx.tx)}; @@ -307,7 +307,7 @@ CashCheck::doApply() STAmount const xrpDeliver{ optDeliverMin ? std::max(*optDeliverMin, std::min(sendMax, srcLiquid)) - : ctx_.tx.getFieldAmount(sfAmount)}; + : get(ctx_.tx.getFieldAmount(sfAmount))}; if (srcLiquid < xrpDeliver) { @@ -340,11 +340,12 @@ CashCheck::doApply() // transfer rate to account for. Since the transfer rate cannot // exceed 200%, we use 1/2 maxValue as our limit. STAmount const flowDeliver{ - optDeliverMin ? STAmount( - optDeliverMin->issue(), - STAmount::cMaxValue / 2, - STAmount::cMaxOffset) - : ctx_.tx.getFieldAmount(sfAmount)}; + optDeliverMin + ? STAmount( + optDeliverMin->issue(), + STAmount::cMaxValue / 2, + STAmount::cMaxOffset) + : get(ctx_.tx.getFieldAmount(sfAmount))}; // If a trust line does not exist yet create one. Issue const& trustLineIssue = flowDeliver.issue(); diff --git a/src/xrpld/app/tx/detail/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index 15d76526094..a6ca307bb3d 100644 --- a/src/xrpld/app/tx/detail/Clawback.cpp +++ b/src/xrpld/app/tx/detail/Clawback.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -27,8 +28,13 @@ namespace ripple { +template +static NotTEC +preflightHelper(PreflightContext const& ctx); + +template <> NotTEC -Clawback::preflight(PreflightContext const& ctx) +preflightHelper(PreflightContext const& ctx) { if (!ctx.rules.enabled(featureClawback)) return temDISABLED; @@ -39,8 +45,11 @@ Clawback::preflight(PreflightContext const& ctx) if (ctx.tx.getFlags() & tfClawbackMask) return temINVALID_FLAG; + if (ctx.tx.isFieldPresent(sfMPTokenHolder)) + return temMALFORMED; + AccountID const issuer = ctx.tx[sfAccount]; - STAmount const clawAmount = ctx.tx[sfAmount]; + STAmount const clawAmount = get(ctx.tx[sfAmount]); // The issuer field is used for the token holder instead AccountID const& holder = clawAmount.getIssuer(); @@ -51,11 +60,48 @@ Clawback::preflight(PreflightContext const& ctx) return preflight2(ctx); } +template <> +NotTEC +preflightHelper(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureClawback)) + return temDISABLED; + + auto const mptHolder = ctx.tx[~sfMPTokenHolder]; + auto const clawAmount = get(ctx.tx[sfAmount]); + + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (!mptHolder) + return temMALFORMED; + + if (ctx.tx.getFlags() & tfClawbackMask) + return temINVALID_FLAG; + + // issuer is the same as holder + if (ctx.tx[sfAccount] == *mptHolder) + return temMALFORMED; + + if (clawAmount > MPTAmount{maxMPTokenAmount} || clawAmount <= beast::zero) + return temBAD_AMOUNT; + + return preflight2(ctx); +} + +template +static TER +preclaimHelper(PreclaimContext const& ctx); + +template <> TER -Clawback::preclaim(PreclaimContext const& ctx) +preclaimHelper(PreclaimContext const& ctx) { AccountID const issuer = ctx.tx[sfAccount]; - STAmount const clawAmount = ctx.tx[sfAmount]; + STAmount const clawAmount = get(ctx.tx[sfAmount]); AccountID const& holder = clawAmount.getIssuer(); auto const sleIssuer = ctx.view.read(keylet::account(issuer)); @@ -110,11 +156,58 @@ Clawback::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +template <> TER -Clawback::doApply() +preclaimHelper(PreclaimContext const& ctx) +{ + AccountID const issuer = ctx.tx[sfAccount]; + auto const clawAmount = get(ctx.tx[sfAmount]); + AccountID const& holder = ctx.tx[sfMPTokenHolder]; + + auto const sleIssuer = ctx.view.read(keylet::account(issuer)); + auto const sleHolder = ctx.view.read(keylet::account(holder)); + if (!sleIssuer || !sleHolder) + return terNO_ACCOUNT; + + if (sleHolder->isFieldPresent(sfAMMID)) + return tecAMM_ACCOUNT; + + auto const issuanceKey = keylet::mptIssuance(clawAmount.issue().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + if (!((*sleIssuance)[sfFlags] & lsfMPTCanClawback)) + return tecNO_PERMISSION; + + if (sleIssuance->getAccountID(sfIssuer) != issuer) + return tecNO_PERMISSION; + + if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, holder))) + return tecOBJECT_NOT_FOUND; + + if (accountHolds( + ctx.view, + holder, + clawAmount.issue(), + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + ctx.j) <= beast::zero) + return tecINSUFFICIENT_FUNDS; + + return tesSUCCESS; +} + +template +static TER +applyHelper(ApplyContext& ctx); + +template <> +TER +applyHelper(ApplyContext& ctx) { - AccountID const& issuer = account_; - STAmount clawAmount = ctx_.tx[sfAmount]; + AccountID const& issuer = ctx.tx[sfAccount]; + STAmount clawAmount = get(ctx.tx[sfAmount]); AccountID const holder = clawAmount.getIssuer(); // cannot be reference // Replace the `issuer` field with issuer's account @@ -124,20 +217,69 @@ Clawback::doApply() // Get the spendable balance. Must use `accountHolds`. STAmount const spendableAmount = accountHolds( - view(), + ctx.view(), holder, clawAmount.getCurrency(), clawAmount.getIssuer(), fhIGNORE_FREEZE, - j_); + ctx.journal); return rippleCredit( - view(), + ctx.view(), holder, issuer, std::min(spendableAmount, clawAmount), true, - j_); + ctx.journal); +} + +template <> +TER +applyHelper(ApplyContext& ctx) +{ + AccountID const& issuer = ctx.tx[sfAccount]; + auto clawAmount = get(ctx.tx[sfAmount]); + AccountID const holder = ctx.tx[sfMPTokenHolder]; + + // Get the spendable balance. Must use `accountHolds`. + STMPTAmount const spendableAmount = accountHolds( + ctx.view(), + holder, + clawAmount.issue(), + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + ctx.journal); + + return rippleCredit( + ctx.view(), + holder, + issuer, + std::min(spendableAmount, clawAmount), + ctx.journal); +} + +NotTEC +Clawback::preflight(PreflightContext const& ctx) +{ + return std::visit( + [&](T const&) { return preflightHelper(ctx); }, + ctx.tx[sfAmount].getValue()); +} + +TER +Clawback::preclaim(PreclaimContext const& ctx) +{ + return std::visit( + [&](T const&) { return preclaimHelper(ctx); }, + ctx.tx[sfAmount].getValue()); +} + +TER +Clawback::doApply() +{ + return std::visit( + [&](T const&) { return applyHelper(ctx_); }, + ctx_.tx[sfAmount].getValue()); } } // namespace ripple diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index e34b675998d..bcf187262c5 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -93,7 +93,7 @@ after(NetClock::time_point now, std::uint32_t mark) TxConsequences EscrowCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ctx.tx, get(ctx.tx[sfAmount]).xrp()}; } NotTEC @@ -108,7 +108,7 @@ EscrowCreate::preflight(PreflightContext const& ctx) if (!isXRP(ctx.tx[sfAmount])) return temBAD_AMOUNT; - if (ctx.tx[sfAmount] <= beast::zero) + if (get(ctx.tx[sfAmount]) <= beast::zero) return temBAD_AMOUNT; // We must specify at least one timeout value @@ -219,7 +219,7 @@ EscrowCreate::doApply() if (balance < reserve) return tecINSUFFICIENT_RESERVE; - if (balance < reserve + STAmount(ctx_.tx[sfAmount]).xrp()) + if (balance < reserve + get(ctx_.tx[sfAmount]).xrp()) return tecUNFUNDED; } @@ -276,7 +276,7 @@ EscrowCreate::doApply() } // Deduct owner's balance, increment owner count - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + (*sle)[sfBalance] = (*sle)[sfBalance] - get(ctx_.tx[sfAmount]); adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); ctx_.view().update(sle); @@ -496,7 +496,7 @@ EscrowFinish::doApply() } // Transfer amount to destination - (*sled)[sfBalance] = (*sled)[sfBalance] + (*slep)[sfAmount]; + (*sled)[sfBalance] = (*sled)[sfBalance] + get((*slep)[sfAmount]); ctx_.view().update(sled); // Adjust source owner count @@ -582,7 +582,7 @@ EscrowCancel::doApply() // Transfer amount back to owner, decrement owner count auto const sle = ctx_.view().peek(keylet::account(account)); - (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount]; + (*sle)[sfBalance] = (*sle)[sfBalance] + get((*slep)[sfAmount]); adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); ctx_.view().update(sle); diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index f855ad8578c..718efb33017 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -102,10 +102,12 @@ XRPNotCreated::visitEntry( break; case ltPAYCHAN: drops_ -= - ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops(); + (get((*before)[sfAmount]) - (*before)[sfBalance]) + .xrp() + .drops(); break; case ltESCROW: - drops_ -= (*before)[sfAmount].xrp().drops(); + drops_ -= get((*before)[sfAmount]).xrp().drops(); break; default: break; @@ -121,13 +123,14 @@ XRPNotCreated::visitEntry( break; case ltPAYCHAN: if (!isDelete) - drops_ += ((*after)[sfAmount] - (*after)[sfBalance]) + drops_ += (get((*after)[sfAmount]) - + (*after)[sfBalance]) .xrp() .drops(); break; case ltESCROW: if (!isDelete) - drops_ += (*after)[sfAmount].xrp().drops(); + drops_ += get((*after)[sfAmount]).xrp().drops(); break; default: break; @@ -279,10 +282,10 @@ NoZeroEscrow::visitEntry( }; if (before && before->getType() == ltESCROW) - bad_ |= isBad((*before)[sfAmount]); + bad_ |= isBad(get((*before)[sfAmount])); if (after && after->getType() == ltESCROW) - bad_ |= isBad((*after)[sfAmount]); + bad_ |= isBad(get((*after)[sfAmount])); } bool @@ -478,6 +481,8 @@ LedgerEntryTypesMatch::visitEntry( case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: case ltDID: case ltORACLE: + case ltMPTOKEN_ISSUANCE: + case ltMPTOKEN: break; default: invalidTypeAdded_ = true; @@ -882,6 +887,9 @@ ValidClawback::visitEntry( { if (before && before->getType() == ltRIPPLE_STATE) trustlinesChanged++; + + if (before && before->getType() == ltMPTOKEN) + mptokensChanged++; } bool @@ -904,18 +912,28 @@ ValidClawback::finalize( return false; } - AccountID const issuer = tx.getAccountID(sfAccount); - STAmount const amount = tx.getFieldAmount(sfAmount); - AccountID const& holder = amount.getIssuer(); - STAmount const holderBalance = accountHolds( - view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j); - - if (holderBalance.signum() < 0) + if (mptokensChanged > 1) { JLOG(j.fatal()) - << "Invariant failed: trustline balance is negative"; + << "Invariant failed: more than one mptokens changed."; return false; } + + if (trustlinesChanged == 1) + { + AccountID const issuer = tx.getAccountID(sfAccount); + STAmount const& amount = get(tx.getFieldAmount(sfAmount)); + AccountID const& holder = amount.getIssuer(); + STAmount const holderBalance = accountHolds( + view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j); + + if (holderBalance.signum() < 0) + { + JLOG(j.fatal()) + << "Invariant failed: trustline balance is negative"; + return false; + } + } } else { @@ -925,9 +943,182 @@ ValidClawback::finalize( "despite failure of the transaction."; return false; } + + if (mptokensChanged != 0) + { + JLOG(j.fatal()) << "Invariant failed: some mptokens were changed " + "despite failure of the transaction."; + return false; + } } return true; } +//------------------------------------------------------------------------------ + +void +ValidMPTIssuance::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltMPTOKEN_ISSUANCE) + { + if (isDelete) + mptIssuancesDeleted_++; + else if (!before) + mptIssuancesCreated_++; + } + + if (after && after->getType() == ltMPTOKEN) + { + if (isDelete) + mptokensDeleted_++; + else if (!before) + mptokensCreated_++; + } +} + +bool +ValidMPTIssuance::finalize( + STTx const& tx, + TER const result, + XRPAmount const _fee, + ReadView const& _view, + beast::Journal const& j) +{ + if (result == tesSUCCESS) + { + if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_CREATE) + { + if (mptIssuancesCreated_ == 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance creation " + "succeeded without creating a MPT issuance"; + } + else if (mptIssuancesDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance creation " + "succeeded while removing MPT issuances"; + } + else if (mptIssuancesCreated_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance creation " + "succeeded but created multiple issuances"; + } + + return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0; + } + + if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_DESTROY) + { + if (mptIssuancesDeleted_ == 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " + "succeeded without removing a MPT issuance"; + } + else if (mptIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " + "succeeded while creating MPT issuances"; + } + else if (mptIssuancesDeleted_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " + "succeeded but deleted multiple issuances"; + } + + return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1; + } + + if (tx.getTxnType() == ttMPTOKEN_AUTHORIZE) + { + bool const submittedByIssuer = tx.isFieldPresent(sfMPTokenHolder); + + if (mptIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT authorize " + "succeeded but created MPT issuances"; + return false; + } + else if (mptIssuancesDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT authorize " + "succeeded but deleted issuances"; + return false; + } + else if ( + submittedByIssuer && + (mptokensCreated_ > 0 || mptokensDeleted_ > 0)) + { + JLOG(j.fatal()) + << "Invariant failed: MPT authorize submitted by issuer " + "succeeded but created/deleted mptokens"; + return false; + } + else if ( + !submittedByIssuer && + (mptokensCreated_ + mptokensDeleted_ != 1)) + { + // if the holder submitted this tx, then a mptoken must be + // either created or deleted. + JLOG(j.fatal()) + << "Invariant failed: MPT authorize submitted by holder " + "succeeded but created/deleted bad number of mptokens"; + return false; + } + + return true; + } + + if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_SET) + { + if (mptIssuancesDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while removing MPT issuances"; + } + else if (mptIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while creating MPT issuances"; + } + else if (mptokensDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while removing MPTokens"; + } + else if (mptokensCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while creating MPTokens"; + } + + return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && + mptokensCreated_ == 0 && mptokensDeleted_ == 0; + } + } + + if (mptIssuancesCreated_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPT issuance was created"; + } + else if (mptIssuancesDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPT issuance was deleted"; + } + else if (mptokensCreated_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPToken was created"; + } + else if (mptokensDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPToken was deleted"; + } + + return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && + mptokensCreated_ == 0 && mptokensDeleted_ == 0; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 1b3234bae69..23ec8005556 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -433,6 +433,31 @@ class NFTokenCountTracking class ValidClawback { std::uint32_t trustlinesChanged = 0; + std::uint32_t mptokensChanged = 0; + +public: + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); +}; + +class ValidMPTIssuance +{ + std::uint32_t mptIssuancesCreated_ = 0; + std::uint32_t mptIssuancesDeleted_ = 0; + + std::uint32_t mptokensCreated_ = 0; + std::uint32_t mptokensDeleted_ = 0; public: void @@ -465,7 +490,8 @@ using InvariantChecks = std::tuple< ValidNewAccountRoot, ValidNFTokenPage, NFTokenCountTracking, - ValidClawback>; + ValidClawback, + ValidMPTIssuance>; /** * @brief get a tuple of all invariant checks diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp new file mode 100644 index 00000000000..1e9871e0ae3 --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -0,0 +1,251 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include + +namespace ripple { + +NotTEC +MPTokenAuthorize::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfMPTokenAuthorizeMask) + return temINVALID_FLAG; + + if (ctx.tx[sfAccount] == ctx.tx[~sfMPTokenHolder]) + return temMALFORMED; + + return preflight2(ctx); +} + +TER +MPTokenAuthorize::preclaim(PreclaimContext const& ctx) +{ + auto const accountID = ctx.tx[sfAccount]; + auto const holderID = ctx.tx[~sfMPTokenHolder]; + + if (holderID && !(ctx.view.exists(keylet::account(*holderID)))) + return tecNO_DST; + + // if non-issuer account submits this tx, then they are trying either: + // 1. Unauthorize/delete MPToken + // 2. Use/create MPToken + // + // Note: `accountID` is holder's account + // `holderID` is NOT used + if (!holderID) + { + std::shared_ptr sleMpt = ctx.view.read( + keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], accountID)); + + // There is an edge case where holder deletes MPT after issuance has + // already been destroyed. So we must check for unauthorize before + // fetching the MPTIssuance object(since it doesn't exist) + + // if holder wants to delete/unauthorize a mpt + if (ctx.tx.getFlags() & tfMPTUnauthorize) + { + if (!sleMpt) + return tecOBJECT_NOT_FOUND; + + if ((*sleMpt)[sfMPTAmount] != 0) + return tecHAS_OBLIGATIONS; + + return tesSUCCESS; + } + + // Now test when the holder wants to hold/create/authorize a new MPT + auto const sleMptIssuance = + ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + + if (!sleMptIssuance) + return tecOBJECT_NOT_FOUND; + + if (accountID == (*sleMptIssuance)[sfIssuer]) + return tecNO_PERMISSION; + + // if holder wants to use and create a mpt + if (sleMpt) + return tecMPTOKEN_EXISTS; + + return tesSUCCESS; + } + + auto const sleMptIssuance = + ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + if (!sleMptIssuance) + return tecOBJECT_NOT_FOUND; + + std::uint32_t const mptIssuanceFlags = sleMptIssuance->getFieldU32(sfFlags); + + // If tx is submitted by issuer, they would either try to do the following + // for allowlisting: + // 1. authorize an account + // 2. unauthorize an account + // + // Note: `accountID` is issuer's account + // `holderID` is holder's account + if (accountID != (*sleMptIssuance)[sfIssuer]) + return tecNO_PERMISSION; + + // If tx is submitted by issuer, it only applies for MPT with + // lsfMPTRequireAuth set + if (!(mptIssuanceFlags & lsfMPTRequireAuth)) + return tecNO_AUTH; + + if (!ctx.view.exists( + keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], *holderID))) + return tecOBJECT_NOT_FOUND; + + return tesSUCCESS; +} + +TER +MPTokenAuthorize::authorize( + ApplyView& view, + beast::Journal journal, + MPTAuthorizeArgs const& args) +{ + auto const sleAcct = view.peek(keylet::account(args.account)); + if (!sleAcct) + return tecINTERNAL; + + // If the account that submitted the tx is a holder + // Note: `account_` is holder's account + // `holderID` is NOT used + if (!args.holderID) + { + // 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) + return tecINTERNAL; + + if (!view.dirRemove( + keylet::ownerDir(args.account), + (*sleMpt)[sfOwnerNode], + sleMpt->key(), + false)) + return tecINTERNAL; + + adjustOwnerCount(view, sleAcct, -1, journal); + + view.erase(sleMpt); + return tesSUCCESS; + } + + // A potential holder wants to authorize/hold a mpt, the ledger must: + // - add the new mptokenKey to the owner directory + // - create the MPToken object for the holder + std::uint32_t const uOwnerCount = sleAcct->getFieldU32(sfOwnerCount); + XRPAmount const reserveCreate( + (uOwnerCount < 2) ? XRPAmount(beast::zero) + : view.fees().accountReserve(uOwnerCount + 1)); + + if (args.priorBalance < reserveCreate) + return tecINSUFFICIENT_RESERVE; + + auto const mptokenKey = + keylet::mptoken(args.mptIssuanceID, args.account); + + auto const ownerNode = view.dirInsert( + keylet::ownerDir(args.account), + mptokenKey, + describeOwnerDir(args.account)); + + if (!ownerNode) + return tecDIR_FULL; + + auto mptoken = std::make_shared(mptokenKey); + (*mptoken)[sfAccount] = args.account; + (*mptoken)[sfMPTokenIssuanceID] = args.mptIssuanceID; + (*mptoken)[sfFlags] = 0; + (*mptoken)[sfOwnerNode] = *ownerNode; + view.insert(mptoken); + + // Update owner count. + adjustOwnerCount(view, sleAcct, 1, journal); + + return tesSUCCESS; + } + + auto const sleMptIssuance = + view.read(keylet::mptIssuance(args.mptIssuanceID)); + if (!sleMptIssuance) + return tecINTERNAL; + + // 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]) + return tecINTERNAL; + + auto const sleMpt = + view.peek(keylet::mptoken(args.mptIssuanceID, *args.holderID)); + if (!sleMpt) + return tecINTERNAL; + + std::uint32_t const flagsIn = sleMpt->getFieldU32(sfFlags); + std::uint32_t flagsOut = flagsIn; + + // Issuer wants to unauthorize the holder, unset lsfMPTAuthorized on + // their MPToken + if (args.flags & tfMPTUnauthorize) + flagsOut &= ~lsfMPTAuthorized; + // Issuer wants to authorize a holder, set lsfMPTAuthorized on their + // MPToken + else + flagsOut |= lsfMPTAuthorized; + + if (flagsIn != flagsOut) + sleMpt->setFieldU32(sfFlags, flagsOut); + + view.update(sleMpt); + return tesSUCCESS; +} + +TER +MPTokenAuthorize::doApply() +{ + auto const& tx = ctx_.tx; + return authorize( + ctx_.view(), + ctx_.journal, + {.priorBalance = mPriorBalance, + .mptIssuanceID = tx[sfMPTokenIssuanceID], + .account = account_, + .flags = tx.getFlags(), + .holderID = tx[~sfMPTokenHolder]}); +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h new file mode 100644 index 00000000000..94451a61c88 --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_MPTOKENAUTHORIZE_H_INCLUDED +#define RIPPLE_TX_MPTOKENAUTHORIZE_H_INCLUDED + +#include + +namespace ripple { + +struct MPTAuthorizeArgs +{ + XRPAmount const& priorBalance; + uint192 const& mptIssuanceID; + AccountID const& account; + std::uint32_t flags; + std::optional holderID; +}; + +class MPTokenAuthorize : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit MPTokenAuthorize(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + static TER + authorize( + ApplyView& view, + beast::Journal journal, + MPTAuthorizeArgs const& args); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp new file mode 100644 index 00000000000..de01341e296 --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -0,0 +1,142 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include + +namespace ripple { + +NotTEC +MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfMPTokenIssuanceCreateMask) + return temINVALID_FLAG; + + if (auto const fee = ctx.tx[~sfTransferFee]) + { + if (fee > maxTransferFee) + return temBAD_MPTOKEN_TRANSFER_FEE; + + // If a non-zero TransferFee is set then the tfTransferable flag + // must also be set. + if (fee > 0u && !ctx.tx.isFlag(tfMPTCanTransfer)) + return temMALFORMED; + } + + if (auto const metadata = ctx.tx[~sfMPTokenMetadata]) + { + if (metadata->length() == 0 || + metadata->length() > maxMPTokenMetadataLength) + return temMALFORMED; + } + + // Check if maximumAmount is within 63 bit range + if (auto const maxAmt = ctx.tx[~sfMaximumAmount]) + { + if (maxAmt == 0) + return temMALFORMED; + + if (maxAmt > maxMPTokenAmount) + return temMALFORMED; + } + return preflight2(ctx); +} + +TER +MPTokenIssuanceCreate::create( + ApplyView& view, + beast::Journal journal, + MPTCreateArgs const& args) +{ + auto const acct = view.peek(keylet::account(args.account)); + if (!acct) + return tecINTERNAL; + + if (args.priorBalance < + view.fees().accountReserve((*acct)[sfOwnerCount] + 1)) + return tecINSUFFICIENT_RESERVE; + + auto const mptIssuanceKeylet = + keylet::mptIssuance(args.account, args.sequence); + + // create the MPTokenIssuance + { + auto const ownerNode = view.dirInsert( + keylet::ownerDir(args.account), + mptIssuanceKeylet, + describeOwnerDir(args.account)); + + if (!ownerNode) + return tecDIR_FULL; + + auto mptIssuance = std::make_shared(mptIssuanceKeylet); + (*mptIssuance)[sfFlags] = args.flags & ~tfUniversal; + (*mptIssuance)[sfIssuer] = args.account; + (*mptIssuance)[sfOutstandingAmount] = 0; + (*mptIssuance)[sfOwnerNode] = *ownerNode; + (*mptIssuance)[sfSequence] = args.sequence; + + if (args.maxAmount) + (*mptIssuance)[sfMaximumAmount] = *args.maxAmount; + + if (args.assetScale) + (*mptIssuance)[sfAssetScale] = *args.assetScale; + + if (args.transferFee) + (*mptIssuance)[sfTransferFee] = *args.transferFee; + + if (args.metadata) + (*mptIssuance)[sfMPTokenMetadata] = *args.metadata; + + view.insert(mptIssuance); + } + + // Update owner count. + adjustOwnerCount(view, acct, 1, journal); + + return tesSUCCESS; +} + +TER +MPTokenIssuanceCreate::doApply() +{ + auto const& tx = ctx_.tx; + return create( + ctx_.view(), + ctx_.journal, + {.priorBalance = mPriorBalance, + .account = account_, + .sequence = tx.getSeqProxy().value(), + .flags = tx.getFlags(), + .maxAmount = tx[~sfMaximumAmount], + .assetScale = tx[~sfAssetScale], + .transferFee = tx[~sfTransferFee], + .metadata = tx[~sfMPTokenMetadata]}); +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h new file mode 100644 index 00000000000..2fdf2bdd152 --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h @@ -0,0 +1,60 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_MPTOKENISSUANCECREATE_H_INCLUDED +#define RIPPLE_TX_MPTOKENISSUANCECREATE_H_INCLUDED + +#include + +namespace ripple { + +struct MPTCreateArgs +{ + XRPAmount const& priorBalance; + AccountID const& account; + std::uint32_t sequence; + std::uint32_t flags; + std::optional maxAmount; + std::optional assetScale; + std::optional transferFee; + std::optional const& metadata; +}; + +class MPTokenIssuanceCreate : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit MPTokenIssuanceCreate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + TER + doApply() override; + + static TER + create(ApplyView& view, beast::Journal journal, MPTCreateArgs const& args); +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp new file mode 100644 index 00000000000..4eb6225c0b4 --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include + +namespace ripple { + +NotTEC +MPTokenIssuanceDestroy::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + // check flags + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfMPTokenIssuanceDestroyMask) + return temINVALID_FLAG; + + return preflight2(ctx); +} + +TER +MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx) +{ + // ensure that issuance exists + auto const sleMPT = + ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + if (!sleMPT) + return tecOBJECT_NOT_FOUND; + + // ensure it is issued by the tx submitter + if ((*sleMPT)[sfIssuer] != ctx.tx[sfAccount]) + return tecNO_PERMISSION; + + // ensure it has no outstanding balances + if ((*sleMPT)[~sfOutstandingAmount] != 0) + return tecHAS_OBLIGATIONS; + + return tesSUCCESS; +} + +TER +MPTokenIssuanceDestroy::doApply() +{ + auto const mpt = + view().peek(keylet::mptIssuance(ctx_.tx[sfMPTokenIssuanceID])); + auto const issuer = (*mpt)[sfIssuer]; + + if (!view().dirRemove( + keylet::ownerDir(issuer), (*mpt)[sfOwnerNode], mpt->key(), false)) + return tefBAD_LEDGER; + + view().erase(mpt); + + adjustOwnerCount(view(), view().peek(keylet::account(issuer)), -1, j_); + + return tesSUCCESS; +} + +} // namespace ripple \ No newline at end of file diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h new file mode 100644 index 00000000000..3e9f9b7e5cf --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_MPTOKENISSUANCEDESTROY_H_INCLUDED +#define RIPPLE_TX_MPTOKENISSUANCEDESTROY_H_INCLUDED + +#include + +namespace ripple { + +class MPTokenIssuanceDestroy : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit MPTokenIssuanceDestroy(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif \ No newline at end of file diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp new file mode 100644 index 00000000000..2b4ff2bcb8a --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -0,0 +1,118 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include + +namespace ripple { + +NotTEC +MPTokenIssuanceSet::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const txFlags = ctx.tx.getFlags(); + + // check flags + if (txFlags & tfMPTokenIssuanceSetMask) + return temINVALID_FLAG; + // fails if both flags are set + else if ((txFlags & tfMPTLock) && (txFlags & tfMPTUnlock)) + return temINVALID_FLAG; + + auto const accountID = ctx.tx[sfAccount]; + auto const holderID = ctx.tx[~sfMPTokenHolder]; + if (holderID && accountID == holderID) + return temMALFORMED; + + return preflight2(ctx); +} + +TER +MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) +{ + // ensure that issuance exists + auto const sleMptIssuance = + ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + if (!sleMptIssuance) + return tecOBJECT_NOT_FOUND; + + // if the mpt has disabled locking + if (!((*sleMptIssuance)[sfFlags] & lsfMPTCanLock)) + return tecNO_PERMISSION; + + // ensure it is issued by the tx submitter + if ((*sleMptIssuance)[sfIssuer] != ctx.tx[sfAccount]) + return tecNO_PERMISSION; + + if (auto const holderID = ctx.tx[~sfMPTokenHolder]) + { + // make sure holder account exists + if (!ctx.view.exists(keylet::account(*holderID))) + return tecNO_DST; + + // the mptoken must exist + if (!ctx.view.exists( + keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], *holderID))) + return tecOBJECT_NOT_FOUND; + } + + return tesSUCCESS; +} + +TER +MPTokenIssuanceSet::doApply() +{ + auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; + auto const txFlags = ctx_.tx.getFlags(); + auto const holderID = ctx_.tx[~sfMPTokenHolder]; + std::shared_ptr sle; + + if (holderID) + sle = view().peek(keylet::mptoken(mptIssuanceID, *holderID)); + else + sle = view().peek(keylet::mptIssuance(mptIssuanceID)); + + if (!sle) + return tecINTERNAL; + + std::uint32_t const flagsIn = sle->getFieldU32(sfFlags); + std::uint32_t flagsOut = flagsIn; + + if (txFlags & tfMPTLock) + flagsOut |= lsfMPTLocked; + else if (txFlags & tfMPTUnlock) + flagsOut &= ~lsfMPTLocked; + + if (flagsIn != flagsOut) + sle->setFieldU32(sfFlags, flagsOut); + + view().update(sle); + + return tesSUCCESS; +} + +} // namespace ripple \ No newline at end of file diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h new file mode 100644 index 00000000000..36080d46667 --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_MPTOKENISSUANCESET_H_INCLUDED +#define RIPPLE_TX_MPTOKENISSUANCESET_H_INCLUDED + +#include + +namespace ripple { + +class MPTokenIssuanceSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit MPTokenIssuanceSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp index b884a791e78..258a96a412d 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp @@ -104,7 +104,8 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) return tecNFTOKEN_BUY_SELL_MISMATCH; // The two offers being brokered must be for the same asset: - if ((*bo)[sfAmount].issue() != (*so)[sfAmount].issue()) + if (get((*bo)[sfAmount]).issue() != + get((*so)[sfAmount]).issue()) return tecNFTOKEN_BUY_SELL_MISMATCH; // The two offers may not form a loop. A broker may not sell the @@ -115,7 +116,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) // Ensure that the buyer is willing to pay at least as much as the // seller is requesting: - if ((*so)[sfAmount] > (*bo)[sfAmount]) + if (get((*so)[sfAmount]) > get((*bo)[sfAmount])) return tecINSUFFICIENT_PAYMENT; // If the buyer specified a destination @@ -152,13 +153,14 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) // cut, if any). if (auto const brokerFee = ctx.tx[~sfNFTokenBrokerFee]) { - if (brokerFee->issue() != (*bo)[sfAmount].issue()) + if (brokerFee->issue() != get((*bo)[sfAmount]).issue()) return tecNFTOKEN_BUY_SELL_MISMATCH; - if (brokerFee >= (*bo)[sfAmount]) + if (brokerFee >= get((*bo)[sfAmount])) return tecINSUFFICIENT_PAYMENT; - if ((*so)[sfAmount] > (*bo)[sfAmount] - *brokerFee) + if (get((*so)[sfAmount]) > + get((*bo)[sfAmount]) - *brokerFee) return tecINSUFFICIENT_PAYMENT; } } @@ -191,7 +193,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) // // After this amendment, we allow an IOU issuer to buy an NFT with their // own currency - auto const needed = bo->at(sfAmount); + auto const needed = get(bo->at(sfAmount)); if (ctx.view.rules().enabled(fixNonFungibleTokensV1_2)) { if (accountFunds( @@ -234,7 +236,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) } // The account offering to buy must have funds: - auto const needed = so->at(sfAmount); + auto const needed = get(so->at(sfAmount)); if (!ctx.view.rules().enabled(fixNonFungibleTokensV1_2)) { if (accountHolds( @@ -280,7 +282,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) return tecINTERNAL; uint256 const& tokenID = offer->at(sfNFTokenID); - STAmount const& amount = offer->at(sfAmount); + STAmount const& amount = get(offer->at(sfAmount)); if (nft::getTransferFee(tokenID) != 0 && (nft::getFlags(tokenID) & nft::flagCreateTrustLines) == 0 && !amount.native()) @@ -387,7 +389,8 @@ NFTokenAcceptOffer::acceptOffer(std::shared_ptr const& offer) auto const nftokenID = (*offer)[sfNFTokenID]; - if (auto amount = offer->getFieldAmount(sfAmount); amount != beast::zero) + if (auto amount = get(offer->getFieldAmount(sfAmount)); + amount != beast::zero) { // Calculate the issuer's cut from this sale, if any: if (auto const fee = nft::getTransferFee(nftokenID); fee != 0) @@ -448,7 +451,7 @@ NFTokenAcceptOffer::doApply() auto const nftokenID = (*so)[sfNFTokenID]; // The amount is what the buyer of the NFT pays: - STAmount amount = (*bo)[sfAmount]; + STAmount amount = get((*bo)[sfAmount]); // Three different folks may be paid. The order of operations is // important. diff --git a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp index 43178d31b4a..c132df1ffda 100644 --- a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp @@ -46,7 +46,7 @@ NFTokenCreateOffer::preflight(PreflightContext const& ctx) // Use implementation shared with NFTokenMint if (NotTEC notTec = nft::tokenOfferCreatePreflight( ctx.tx[sfAccount], - ctx.tx[sfAmount], + get(ctx.tx[sfAmount]), ctx.tx[~sfDestination], ctx.tx[~sfExpiration], nftFlags, @@ -79,7 +79,7 @@ NFTokenCreateOffer::preclaim(PreclaimContext const& ctx) ctx.view, ctx.tx[sfAccount], nft::getIssuer(nftokenID), - ctx.tx[sfAmount], + get(ctx.tx[sfAmount]), ctx.tx[~sfDestination], nft::getFlags(nftokenID), nft::getTransferFee(nftokenID), @@ -95,7 +95,7 @@ NFTokenCreateOffer::doApply() return nft::tokenOfferCreateApply( view(), ctx_.tx[sfAccount], - ctx_.tx[sfAmount], + get(ctx_.tx[sfAmount]), ctx_.tx[~sfDestination], ctx_.tx[~sfExpiration], ctx_.tx.getSeqProxy(), diff --git a/src/xrpld/app/tx/detail/NFTokenMint.cpp b/src/xrpld/app/tx/detail/NFTokenMint.cpp index d5c3a8707c2..06ff1932f3f 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.cpp +++ b/src/xrpld/app/tx/detail/NFTokenMint.cpp @@ -105,7 +105,7 @@ NFTokenMint::preflight(PreflightContext const& ctx) // because a Mint is only allowed to create a sell offer. if (NotTEC notTec = nft::tokenOfferCreatePreflight( ctx.tx[sfAccount], - ctx.tx[sfAmount], + get(ctx.tx[sfAmount]), ctx.tx[~sfDestination], ctx.tx[~sfExpiration], extractNFTokenFlagsFromTxFlags(ctx.tx.getFlags()), @@ -195,7 +195,7 @@ NFTokenMint::preclaim(PreclaimContext const& ctx) ctx.view, ctx.tx[sfAccount], ctx.tx[~sfIssuer].value_or(ctx.tx[sfAccount]), - ctx.tx[sfAmount], + get(ctx.tx[sfAmount]), ctx.tx[~sfDestination], extractNFTokenFlagsFromTxFlags(ctx.tx.getFlags()), ctx.tx[~sfTransferFee].value_or(0), @@ -323,7 +323,7 @@ NFTokenMint::doApply() if (TER const ter = nft::tokenOfferCreateApply( view(), ctx_.tx[sfAccount], - ctx_.tx[sfAmount], + get(ctx_.tx[sfAmount]), ctx_.tx[~sfDestination], ctx_.tx[~sfExpiration], ctx_.tx.getSeqProxy(), diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.cpp b/src/xrpld/app/tx/detail/NFTokenUtils.cpp index 61ff8e200b3..c4e65543014 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.cpp +++ b/src/xrpld/app/tx/detail/NFTokenUtils.cpp @@ -1020,7 +1020,7 @@ tokenOfferCreateApply( auto offer = std::make_shared(offerID); (*offer)[sfOwner] = acctID; (*offer)[sfNFTokenID] = nftokenID; - (*offer)[sfAmount] = amount; + (*offer)[sfAmount] = STEitherAmount{amount}; (*offer)[sfFlags] = sleFlags; (*offer)[sfOwnerNode] = *ownerNode; (*offer)[sfNFTokenOfferNode] = *offerNode; diff --git a/src/xrpld/app/tx/detail/PayChan.cpp b/src/xrpld/app/tx/detail/PayChan.cpp index d17736c4738..e3a2e02e02d 100644 --- a/src/xrpld/app/tx/detail/PayChan.cpp +++ b/src/xrpld/app/tx/detail/PayChan.cpp @@ -149,9 +149,9 @@ closeChannel( if (!sle) return tefINTERNAL; - assert((*slep)[sfAmount] >= (*slep)[sfBalance]); - (*sle)[sfBalance] = - (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]; + assert(get((*slep)[sfAmount]) >= (*slep)[sfBalance]); + (*sle)[sfBalance] = (*sle)[sfBalance] + get((*slep)[sfAmount]) - + (*slep)[sfBalance]; adjustOwnerCount(view, sle, -1, j); view.update(sle); @@ -165,7 +165,7 @@ closeChannel( TxConsequences PayChanCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ctx.tx, get(ctx.tx[sfAmount]).xrp()}; } NotTEC @@ -177,7 +177,8 @@ PayChanCreate::preflight(PreflightContext const& ctx) if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; - if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) + if (!isXRP(ctx.tx[sfAmount]) || + (get(ctx.tx[sfAmount]) <= beast::zero)) return temBAD_AMOUNT; if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) @@ -206,7 +207,7 @@ PayChanCreate::preclaim(PreclaimContext const& ctx) if (balance < reserve) return tecINSUFFICIENT_RESERVE; - if (balance < reserve + ctx.tx[sfAmount]) + if (balance < reserve + get(ctx.tx[sfAmount])) return tecUNFUNDED; } @@ -262,7 +263,7 @@ PayChanCreate::doApply() // Funds held in this channel (*slep)[sfAmount] = ctx_.tx[sfAmount]; // Amount channel has already paid - (*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed(); + (*slep)[sfBalance] = get(ctx_.tx[sfAmount]).zeroed(); (*slep)[sfAccount] = account; (*slep)[sfDestination] = dst; (*slep)[sfSettleDelay] = ctx_.tx[sfSettleDelay]; @@ -295,7 +296,7 @@ PayChanCreate::doApply() } // Deduct owner's balance, increment owner count - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + (*sle)[sfBalance] = (*sle)[sfBalance] - get(ctx_.tx[sfAmount]); adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); ctx_.view().update(sle); @@ -307,7 +308,7 @@ PayChanCreate::doApply() TxConsequences PayChanFund::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ctx.tx, get(ctx.tx[sfAmount]).xrp()}; } NotTEC @@ -319,7 +320,8 @@ PayChanFund::preflight(PreflightContext const& ctx) if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; - if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) + if (!isXRP(ctx.tx[sfAmount]) || + (get(ctx.tx[sfAmount]) <= beast::zero)) return temBAD_AMOUNT; return preflight2(ctx); @@ -378,7 +380,7 @@ PayChanFund::doApply() if (balance < reserve) return tecINSUFFICIENT_RESERVE; - if (balance < reserve + ctx_.tx[sfAmount]) + if (balance < reserve + get(ctx_.tx[sfAmount])) return tecUNFUNDED; } @@ -389,10 +391,11 @@ PayChanFund::doApply() return tecNO_DST; } - (*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount]; + (*slep)[sfAmount] = STEitherAmount{ + get((*slep)[sfAmount]) + get(ctx_.tx[sfAmount])}; ctx_.view().update(slep); - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + (*sle)[sfBalance] = (*sle)[sfBalance] - get(ctx_.tx[sfAmount]); ctx_.view().update(sle); return tesSUCCESS; @@ -410,7 +413,7 @@ PayChanClaim::preflight(PreflightContext const& ctx) if (bal && (!isXRP(*bal) || *bal <= beast::zero)) return temBAD_AMOUNT; - auto const amt = ctx.tx[~sfAmount]; + auto const amt = get(ctx.tx[~sfAmount]); if (amt && (!isXRP(*amt) || *amt <= beast::zero)) return temBAD_AMOUNT; @@ -485,7 +488,8 @@ PayChanClaim::doApply() if (ctx_.tx[~sfBalance]) { auto const chanBalance = slep->getFieldAmount(sfBalance).xrp(); - auto const chanFunds = slep->getFieldAmount(sfAmount).xrp(); + auto const chanFunds = + get(slep->getFieldAmount(sfAmount)).xrp(); auto const reqBalance = ctx_.tx[sfBalance].xrp(); if (txAccount == dst && !ctx_.tx[~sfSignature]) @@ -549,7 +553,8 @@ PayChanClaim::doApply() if (ctx_.tx.getFlags() & tfClose) { // Channel will close immediately if dry or the receiver closes - if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount]) + if (dst == txAccount || + (*slep)[sfBalance] == get((*slep)[sfAmount])) return closeChannel( slep, ctx_.view(), k.key, ctx_.app.journal("View")); diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 309e9d4a498..42275ba4da6 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -28,12 +29,18 @@ namespace ripple { +template +static TxConsequences +makeTxConsequencesHelper(PreflightContext const& ctx); + +template <> TxConsequences -Payment::makeTxConsequences(PreflightContext const& ctx) +makeTxConsequencesHelper(PreflightContext const& ctx) { auto calculateMaxXRPSpend = [](STTx const& tx) -> XRPAmount { - STAmount const maxAmount = - tx.isFieldPresent(sfSendMax) ? tx[sfSendMax] : tx[sfAmount]; + auto const maxAmount = tx.isFieldPresent(sfSendMax) + ? tx[sfSendMax] + : get(tx[sfAmount]); // If there's no sfSendMax in XRP, and the sfAmount isn't // in XRP, then the transaction does not spend XRP. @@ -43,8 +50,20 @@ Payment::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)}; } +template <> +TxConsequences +makeTxConsequencesHelper(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx, beast::zero}; +} + +template +static NotTEC +preflightHelper(PreflightContext const& ctx); + +template <> NotTEC -Payment::preflight(PreflightContext const& ctx) +preflightHelper(PreflightContext const& ctx) { if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -67,7 +86,7 @@ Payment::preflight(PreflightContext const& ctx) bool const bPaths = tx.isFieldPresent(sfPaths); bool const bMax = tx.isFieldPresent(sfSendMax); - STAmount const saDstAmount(tx.getFieldAmount(sfAmount)); + STAmount const saDstAmount(get(tx.getFieldAmount(sfAmount))); STAmount maxSourceAmount; auto const account = tx.getAccountID(sfAccount); @@ -203,8 +222,91 @@ Payment::preflight(PreflightContext const& ctx) return preflight2(ctx); } +template <> +NotTEC +preflightHelper(PreflightContext const& ctx) +{ + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + if (ctx.tx.isFieldPresent(sfDeliverMin) || + ctx.tx.isFieldPresent(sfSendMax) || ctx.tx.isFieldPresent(sfPaths)) + return temMALFORMED; + + auto& tx = ctx.tx; + auto& j = ctx.j; + + std::uint32_t const uTxFlags = tx.getFlags(); + + if (uTxFlags & tfPaymentMask) + { + JLOG(j.trace()) << "Malformed transaction: " + << "Invalid flags set."; + return temINVALID_FLAG; + } + + STMPTAmount const saDstAmount( + get(tx.getFieldAmount(sfAmount))); + + auto const account = tx.getAccountID(sfAccount); + + auto const& uDstCurrency = saDstAmount.getCurrency(); + + auto const uDstAccountID = tx.getAccountID(sfDestination); + + if (!uDstAccountID) + { + JLOG(j.trace()) << "Malformed transaction: " + << "Payment destination account not specified."; + return temDST_NEEDED; + } + if (saDstAmount <= beast::zero) + { + JLOG(j.trace()) << "Malformed transaction: " + << "bad dst amount: " << saDstAmount.getFullText(); + return temBAD_AMOUNT; + } + if (noMPT() == uDstCurrency) + { + JLOG(j.trace()) << "Malformed transaction: " + << "Bad asset."; + return temBAD_CURRENCY; + } + if (account == uDstAccountID) + { + // You're signing yourself a payment. + JLOG(j.trace()) << "Malformed transaction: " + << "Redundant payment from " << to_string(account) + << " to self without path for " + << to_string(uDstCurrency); + return temREDUNDANT; + } + if (uTxFlags & (tfPartialPayment | tfLimitQuality | tfNoRippleDirect)) + { + JLOG(j.trace()) << "Malformed transaction: invalid MPT flags: " + << uTxFlags; + return temMALFORMED; + } + + return preflight2(ctx); +} + +template +static TER +preclaimHelper( + PreclaimContext const& ctx, + std::size_t maxPathSize, + std::size_t maxPathLength); + +template <> TER -Payment::preclaim(PreclaimContext const& ctx) +preclaimHelper( + PreclaimContext const& ctx, + std::size_t maxPathSize, + std::size_t maxPathLength) { // Ripple if source or destination is non-native or if there are paths. std::uint32_t const uTxFlags = ctx.tx.getFlags(); @@ -213,7 +315,7 @@ Payment::preclaim(PreclaimContext const& ctx) auto const sendMax = ctx.tx[~sfSendMax]; AccountID const uDstAccountID(ctx.tx[sfDestination]); - STAmount const saDstAmount(ctx.tx[sfAmount]); + STAmount const saDstAmount(get(ctx.tx[sfAmount])); auto const k = keylet::account(uDstAccountID); auto const sleDst = ctx.view.read(k); @@ -275,9 +377,9 @@ Payment::preclaim(PreclaimContext const& ctx) { STPathSet const& paths = ctx.tx.getFieldPathSet(sfPaths); - if (paths.size() > MaxPathSize || - std::any_of(paths.begin(), paths.end(), [](STPath const& path) { - return path.size() > MaxPathLength; + if (paths.size() > maxPathSize || + std::any_of(paths.begin(), paths.end(), [&](STPath const& path) { + return path.size() > maxPathLength; })) { return telBAD_PATH_COUNT; @@ -287,21 +389,72 @@ Payment::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +template <> TER -Payment::doApply() +preclaimHelper( + PreclaimContext const& ctx, + std::size_t, + std::size_t) { - auto const deliverMin = ctx_.tx[~sfDeliverMin]; + AccountID const uDstAccountID(ctx.tx[sfDestination]); + + auto const k = keylet::account(uDstAccountID); + auto const sleDst = ctx.view.read(k); + + if (!sleDst) + { + JLOG(ctx.j.trace()) + << "Delay transaction: Destination account does not exist."; + + // Another transaction could create the account and then this + // transaction would succeed. + return tecNO_DST; + } + else if ( + (sleDst->getFlags() & lsfRequireDestTag) && + !ctx.tx.isFieldPresent(sfDestinationTag)) + { + // The tag is basically account-specific information we don't + // understand, but we can require someone to fill it in. + + // We didn't make this test for a newly-formed account because there's + // no way for this field to be set. + JLOG(ctx.j.trace()) + << "Malformed transaction: DestinationTag required."; + + return tecDST_TAG_NEEDED; + } + + return tesSUCCESS; +} + +template +static TER +applyHelper( + ApplyContext& ctx, + XRPAmount const& priorBalance, + XRPAmount const& sourceBalance); + +template <> +TER +applyHelper( + ApplyContext& ctx, + XRPAmount const& priorBalance, + XRPAmount const& sourceBalance) +{ + AccountID const account = ctx.tx[sfAccount]; + auto const deliverMin = ctx.tx[~sfDeliverMin]; // Ripple if source or destination is non-native or if there are paths. - std::uint32_t const uTxFlags = ctx_.tx.getFlags(); + std::uint32_t const uTxFlags = ctx.tx.getFlags(); bool const partialPaymentAllowed = uTxFlags & tfPartialPayment; bool const limitQuality = uTxFlags & tfLimitQuality; bool const defaultPathsAllowed = !(uTxFlags & tfNoRippleDirect); - auto const paths = ctx_.tx.isFieldPresent(sfPaths); - auto const sendMax = ctx_.tx[~sfSendMax]; + auto const paths = ctx.tx.isFieldPresent(sfPaths); + auto const sendMax = ctx.tx[~sfSendMax]; - AccountID const uDstAccountID(ctx_.tx.getAccountID(sfDestination)); - STAmount const saDstAmount(ctx_.tx.getFieldAmount(sfAmount)); + AccountID const uDstAccountID(ctx.tx.getAccountID(sfDestination)); + STAmount const saDstAmount(get(ctx.tx.getFieldAmount(sfAmount))); STAmount maxSourceAmount; if (sendMax) maxSourceAmount = *sendMax; @@ -309,44 +462,47 @@ Payment::doApply() maxSourceAmount = saDstAmount; else maxSourceAmount = STAmount( - {saDstAmount.getCurrency(), account_}, + {saDstAmount.getCurrency(), account}, saDstAmount.mantissa(), saDstAmount.exponent(), saDstAmount < beast::zero); - JLOG(j_.trace()) << "maxSourceAmount=" << maxSourceAmount.getFullText() - << " saDstAmount=" << saDstAmount.getFullText(); + JLOG(ctx.journal.trace()) + << "maxSourceAmount=" << maxSourceAmount.getFullText() + << " saDstAmount=" << saDstAmount.getFullText(); // Open a ledger for editing. auto const k = keylet::account(uDstAccountID); - SLE::pointer sleDst = view().peek(k); + SLE::pointer sleDst = ctx.view().peek(k); if (!sleDst) { std::uint32_t const seqno{ - view().rules().enabled(featureDeletableAccounts) ? view().seq() - : 1}; + ctx.view().rules().enabled(featureDeletableAccounts) + ? ctx.view().seq() + : 1}; // Create the account. sleDst = std::make_shared(k); sleDst->setAccountID(sfAccount, uDstAccountID); sleDst->setFieldU32(sfSequence, seqno); - view().insert(sleDst); + ctx.view().insert(sleDst); } else { // Tell the engine that we are intending to change the destination // account. The source account gets always charged a fee so it's always // marked as modified. - view().update(sleDst); + ctx.view().update(sleDst); } // Determine whether the destination requires deposit authorization. bool const reqDepositAuth = sleDst->getFlags() & lsfDepositAuth && - view().rules().enabled(featureDepositAuth); + ctx.view().rules().enabled(featureDepositAuth); - bool const depositPreauth = view().rules().enabled(featureDepositPreauth); + bool const depositPreauth = + ctx.view().rules().enabled(featureDepositPreauth); bool const bRipple = paths || sendMax || !saDstAmount.native(); @@ -366,10 +522,10 @@ Payment::doApply() // authorization has two ways to get an IOU Payment in: // 1. If Account == Destination, or // 2. If Account is deposit preauthorized by destination. - if (uDstAccountID != account_) + if (uDstAccountID != account) { - if (!view().exists( - keylet::depositPreauth(uDstAccountID, account_))) + if (!ctx.view().exists( + keylet::depositPreauth(uDstAccountID, account))) return tecNO_PERMISSION; } } @@ -378,26 +534,26 @@ Payment::doApply() rcInput.partialPaymentAllowed = partialPaymentAllowed; rcInput.defaultPathsAllowed = defaultPathsAllowed; rcInput.limitQuality = limitQuality; - rcInput.isLedgerOpen = view().open(); + rcInput.isLedgerOpen = ctx.view().open(); path::RippleCalc::Output rc; { - PaymentSandbox pv(&view()); - JLOG(j_.debug()) << "Entering RippleCalc in payment: " - << ctx_.tx.getTransactionID(); + PaymentSandbox pv(&ctx.view()); + JLOG(ctx.journal.debug()) << "Entering RippleCalc in payment: " + << ctx.tx.getTransactionID(); rc = path::RippleCalc::rippleCalculate( pv, maxSourceAmount, saDstAmount, uDstAccountID, - account_, - ctx_.tx.getFieldPathSet(sfPaths), - ctx_.app.logs(), + account, + ctx.tx.getFieldPathSet(sfPaths), + ctx.app.logs(), &rcInput); // VFALCO NOTE We might not need to apply, depending // on the TER. But always applying *should* // be safe. - pv.apply(ctx_.rawView()); + pv.apply(ctx.rawView()); } // TODO: is this right? If the amount is the correct amount, was @@ -407,7 +563,7 @@ Payment::doApply() if (deliverMin && rc.actualAmountOut < *deliverMin) rc.setResult(tecPATH_PARTIAL); else - ctx_.deliver(rc.actualAmountOut); + ctx.deliver(rc.actualAmountOut); } auto terResult = rc.result(); @@ -425,7 +581,7 @@ Payment::doApply() // Direct XRP payment. - auto const sleSrc = view().peek(keylet::account(account_)); + auto const sleSrc = ctx.view().peek(keylet::account(account)); if (!sleSrc) return tefINTERNAL; @@ -434,21 +590,21 @@ Payment::doApply() auto const uOwnerCount = sleSrc->getFieldU32(sfOwnerCount); // This is the total reserve in drops. - auto const reserve = view().fees().accountReserve(uOwnerCount); + auto const reserve = ctx.view().fees().accountReserve(uOwnerCount); // mPriorBalance is the balance on the sending account BEFORE the // fees were charged. We want to make sure we have enough reserve // to send. Allow final spend to use reserve for fee. - auto const mmm = std::max(reserve, ctx_.tx.getFieldAmount(sfFee).xrp()); + auto const mmm = std::max(reserve, ctx.tx.getFieldAmount(sfFee).xrp()); - if (mPriorBalance < saDstAmount.xrp() + mmm) + if (priorBalance < saDstAmount.xrp() + mmm) { // Vote no. However the transaction might succeed, if applied in // a different order. - JLOG(j_.trace()) << "Delay transaction: Insufficient funds: " - << " " << to_string(mPriorBalance) << " / " - << to_string(saDstAmount.xrp() + mmm) << " (" - << to_string(reserve) << ")"; + JLOG(ctx.journal.trace()) << "Delay transaction: Insufficient funds: " + << " " << to_string(priorBalance) << " / " + << to_string(saDstAmount.xrp() + mmm) << " (" + << to_string(reserve) << ")"; return tecUNFUNDED_PAYMENT; } @@ -480,12 +636,13 @@ Payment::doApply() // We choose the base reserve as our bound because it is // a small number that seldom changes but is always sufficient // to get the account un-wedged. - if (uDstAccountID != account_) + if (uDstAccountID != account) { - if (!view().exists(keylet::depositPreauth(uDstAccountID, account_))) + if (!ctx.view().exists( + keylet::depositPreauth(uDstAccountID, account))) { // Get the base reserve. - XRPAmount const dstReserve{view().fees().accountReserve(0)}; + XRPAmount const dstReserve{ctx.view().fees().accountReserve(0)}; if (saDstAmount > dstReserve || sleDst->getFieldAmount(sfBalance) > dstReserve) @@ -495,7 +652,7 @@ Payment::doApply() } // Do the arithmetic for the transfer and make the ledger change. - sleSrc->setFieldAmount(sfBalance, mSourceBalance - saDstAmount); + sleSrc->setFieldAmount(sfBalance, sourceBalance - saDstAmount); sleDst->setFieldAmount( sfBalance, sleDst->getFieldAmount(sfBalance) + saDstAmount); @@ -506,4 +663,85 @@ Payment::doApply() return tesSUCCESS; } +template <> +TER +applyHelper(ApplyContext& ctx, XRPAmount const&, XRPAmount const&) +{ + auto const account = ctx.tx[sfAccount]; + + AccountID const uDstAccountID(ctx.tx.getAccountID(sfDestination)); + auto const saDstAmount(get(ctx.tx.getFieldAmount(sfAmount))); + + JLOG(ctx.journal.trace()) << " saDstAmount=" << saDstAmount.getFullText(); + + if (auto const ter = requireAuth(ctx.view(), saDstAmount.issue(), account); + ter != tesSUCCESS) + return ter; + + if (auto const ter = + requireAuth(ctx.view(), saDstAmount.issue(), uDstAccountID); + ter != tesSUCCESS) + return ter; + + if (auto const ter = canTransfer( + ctx.view(), saDstAmount.issue(), account, uDstAccountID); + ter != tesSUCCESS) + return ter; + + auto const& mpt = saDstAmount.issue(); + auto const& issuer = mpt.getIssuer(); + // If globally/individually locked then + // - can't send between holders + // - holder can send back to issuer + // - issuer can send to holder + if (account != issuer && uDstAccountID != issuer && + (isFrozen(ctx.view(), account, mpt) || + isFrozen(ctx.view(), uDstAccountID, mpt))) + return tecMPT_LOCKED; + + PaymentSandbox pv(&ctx.view()); + auto const res = + accountSend(pv, account, uDstAccountID, saDstAmount, ctx.journal); + pv.apply(ctx.rawView()); + return res; +} + +TxConsequences +Payment::makeTxConsequences(PreflightContext const& ctx) +{ + return std::visit( + [&](TDel const&) { + return makeTxConsequencesHelper(ctx); + }, + ctx.tx[sfAmount].getValue()); +} + +NotTEC +Payment::preflight(PreflightContext const& ctx) +{ + return std::visit( + [&](TDel const&) { return preflightHelper(ctx); }, + ctx.tx[sfAmount].getValue()); +} + +TER +Payment::preclaim(PreclaimContext const& ctx) +{ + return std::visit( + [&](TDel const&) { + return preclaimHelper(ctx, MaxPathSize, MaxPathLength); + }, + ctx.tx[sfAmount].getValue()); +} + +TER +Payment::doApply() +{ + return std::visit( + [&](TDel const&) { + return applyHelper(ctx_, mPriorBalance, mSourceBalance); + }, + ctx_.tx[sfAmount].getValue()); +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index 3a7fe9cca0d..954fc6543f1 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -537,7 +537,7 @@ SetTrust::doApply() else { // Zero balance in currency. - STAmount saBalance({currency, noAccount()}); + STAmount saBalance(Issue{currency, noAccount()}); auto const k = keylet::line(account_, uDstAccountID, currency); diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 6ae8be8a67f..a528943f4cf 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -843,7 +843,7 @@ Transactor::operator()() // fixSTAmountCanonicalize predate the rulesGuard and should be replaced. STAmountSO stAmountSO{view().rules().enabled(fixSTAmountCanonicalize)}; NumberSO stNumberSO{view().rules().enabled(fixUniversalNumber)}; - CurrentTransactionRulesGuard currentTransctionRulesGuard(view().rules()); + CurrentTransactionRulesGuard currentTransactionRulesGuard(view().rules()); #ifdef DEBUG { diff --git a/src/xrpld/app/tx/detail/XChainBridge.cpp b/src/xrpld/app/tx/detail/XChainBridge.cpp index f5633903567..3154d605ca1 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.cpp +++ b/src/xrpld/app/tx/detail/XChainBridge.cpp @@ -1676,7 +1676,7 @@ XChainClaim::preflight(PreflightContext const& ctx) return temINVALID_FLAG; STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge]; - auto const amount = ctx.tx[sfAmount]; + auto const amount = get(ctx.tx[sfAmount]); if (amount.signum() <= 0 || (amount.issue() != bridgeSpec.lockingChainIssue() && @@ -1693,7 +1693,7 @@ XChainClaim::preclaim(PreclaimContext const& ctx) { AccountID const account = ctx.tx[sfAccount]; STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge]; - STAmount const& thisChainAmount = ctx.tx[sfAmount]; + STAmount const& thisChainAmount = get(ctx.tx[sfAmount]); auto const claimID = ctx.tx[sfXChainClaimID]; auto const sleBridge = readBridge(ctx.view, bridgeSpec); @@ -1780,7 +1780,7 @@ XChainClaim::doApply() AccountID const account = ctx_.tx[sfAccount]; auto const dst = ctx_.tx[sfDestination]; STXChainBridge const bridgeSpec = ctx_.tx[sfXChainBridge]; - STAmount const& thisChainAmount = ctx_.tx[sfAmount]; + STAmount const& thisChainAmount = get(ctx_.tx[sfAmount]); auto const claimID = ctx_.tx[sfXChainClaimID]; auto const claimIDKeylet = keylet::xChainClaimID(bridgeSpec, claimID); @@ -1892,7 +1892,7 @@ TxConsequences XChainCommit::makeTxConsequences(PreflightContext const& ctx) { auto const maxSpend = [&] { - auto const amount = ctx.tx[sfAmount]; + auto const amount = get(ctx.tx[sfAmount]); if (amount.native() && amount.signum() > 0) return amount.xrp(); return XRPAmount{beast::zero}; @@ -1913,7 +1913,7 @@ XChainCommit::preflight(PreflightContext const& ctx) if (ctx.tx.getFlags() & tfUniversalMask) return temINVALID_FLAG; - auto const amount = ctx.tx[sfAmount]; + auto const amount = get(ctx.tx[sfAmount]); auto const bridgeSpec = ctx.tx[sfXChainBridge]; if (amount.signum() <= 0 || !isLegalNet(amount)) @@ -1959,12 +1959,14 @@ XChainCommit::preclaim(PreclaimContext const& ctx) if (isLockingChain) { - if (bridgeSpec.lockingChainIssue() != ctx.tx[sfAmount].issue()) + if (bridgeSpec.lockingChainIssue() != + get(ctx.tx[sfAmount]).issue()) return tecXCHAIN_BAD_TRANSFER_ISSUE; } else { - if (bridgeSpec.issuingChainIssue() != ctx.tx[sfAmount].issue()) + if (bridgeSpec.issuingChainIssue() != + get(ctx.tx[sfAmount]).issue()) return tecXCHAIN_BAD_TRANSFER_ISSUE; } @@ -1977,7 +1979,7 @@ XChainCommit::doApply() PaymentSandbox psb(&ctx_.view()); auto const account = ctx_.tx[sfAccount]; - auto const amount = ctx_.tx[sfAmount]; + auto const amount = get(ctx_.tx[sfAmount]); auto const bridgeSpec = ctx_.tx[sfXChainBridge]; if (!psb.read(keylet::account(account))) @@ -2182,7 +2184,7 @@ XChainCreateAccountCommit::preflight(PreflightContext const& ctx) if (ctx.tx.getFlags() & tfUniversalMask) return temINVALID_FLAG; - auto const amount = ctx.tx[sfAmount]; + auto const amount = get(ctx.tx[sfAmount]); if (amount.signum() <= 0 || !amount.native()) return temBAD_AMOUNT; @@ -2201,7 +2203,7 @@ TER XChainCreateAccountCommit::preclaim(PreclaimContext const& ctx) { STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge]; - STAmount const amount = ctx.tx[sfAmount]; + STAmount const amount = get(ctx.tx[sfAmount]); STAmount const reward = ctx.tx[sfSignatureReward]; auto const sleBridge = readBridge(ctx.view, bridgeSpec); @@ -2247,7 +2249,7 @@ XChainCreateAccountCommit::preclaim(PreclaimContext const& ctx) STXChainBridge::ChainType const dstChain = STXChainBridge::otherChain(srcChain); - if (bridgeSpec.issue(srcChain) != ctx.tx[sfAmount].issue()) + if (bridgeSpec.issue(srcChain) != get(ctx.tx[sfAmount]).issue()) return tecXCHAIN_BAD_TRANSFER_ISSUE; if (!isXRP(bridgeSpec.issue(dstChain))) @@ -2262,7 +2264,7 @@ XChainCreateAccountCommit::doApply() PaymentSandbox psb(&ctx_.view()); AccountID const account = ctx_.tx[sfAccount]; - STAmount const amount = ctx_.tx[sfAmount]; + STAmount const amount = get(ctx_.tx[sfAmount]); STAmount const reward = ctx_.tx[sfSignatureReward]; STXChainBridge const bridge = ctx_.tx[sfXChainBridge]; diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index cbeabb6fc9c..e33d673e022 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -39,6 +39,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -168,6 +172,14 @@ with_txn_type(TxType txnType, F&& f) return f.template operator()(); case ttORACLE_DELETE: return f.template operator()(); + case ttMPTOKEN_ISSUANCE_CREATE: + return f.template operator()(); + case ttMPTOKEN_ISSUANCE_DESTROY: + return f.template operator()(); + case ttMPTOKEN_AUTHORIZE: + return f.template operator()(); + case ttMPTOKEN_ISSUANCE_SET: + return f.template operator()(); default: throw UnknownTxnType(txnType); } diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 09f374d2c29..b413a775bea 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -78,9 +79,15 @@ hasExpired(ReadView const& view, std::optional const& exp); /** Controls the treatment of frozen account balances */ enum FreezeHandling { fhIGNORE_FREEZE, fhZERO_IF_FROZEN }; +/** Controls the treatment of unauthorized MPT balances */ +enum AuthHandling { ahIGNORE_AUTH, ahZERO_IF_UNAUTHORIZED }; + [[nodiscard]] bool isGlobalFrozen(ReadView const& view, AccountID const& issuer); +[[nodiscard]] bool +isGlobalFrozen(ReadView const& view, MPTIssue const& mpt); + [[nodiscard]] bool isIndividualFrozen( ReadView const& view, @@ -97,6 +104,12 @@ isIndividualFrozen( return isIndividualFrozen(view, account, issue.currency, issue.account); } +[[nodiscard]] inline bool +isIndividualFrozen( + ReadView const& view, + AccountID const& account, + MPTIssue const& mpt); + [[nodiscard]] bool isFrozen( ReadView const& view, @@ -110,6 +123,9 @@ isFrozen(ReadView const& view, AccountID const& account, Issue const& issue) return isFrozen(view, account, issue.currency, issue.account); } +[[nodiscard]] bool +isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mpt); + // Returns the amount an account can spend without going into debt. // // <-- saAmount: amount of currency held by account. May be negative. @@ -130,6 +146,15 @@ accountHolds( FreezeHandling zeroIfFrozen, beast::Journal j); +[[nodiscard]] STMPTAmount +accountHolds( + ReadView const& view, + AccountID const& account, + MPTIssue const& issue, + 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 @@ -209,6 +234,9 @@ forEachItemAfter( [[nodiscard]] Rate transferRate(ReadView const& view, AccountID const& issuer); +[[nodiscard]] Rate +transferRate(ReadView const& view, MPTID const& id); + /** Returns `true` if the directory is empty @param key The key of the directory */ @@ -419,6 +447,14 @@ rippleCredit( bool bCheckIssuer, beast::Journal j); +[[nodiscard]] TER +rippleCredit( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STMPTAmount saAmount, + beast::Journal j); + [[nodiscard]] TER accountSend( ApplyView& view, @@ -428,6 +464,15 @@ accountSend( beast::Journal j, WaiveTransferFee waiveFee = WaiveTransferFee::No); +[[nodiscard]] TER +accountSend( + ApplyView& view, + AccountID const& from, + AccountID const& to, + const STMPTAmount& saAmount, + beast::Journal j, + WaiveTransferFee waiveFee = WaiveTransferFee::No); + [[nodiscard]] TER issueIOU( ApplyView& view, @@ -458,6 +503,22 @@ transferXRP( */ [[nodiscard]] TER requireAuth(ReadView const& view, Issue const& issue, AccountID const& account); +[[nodiscard]] TER +requireAuth( + ReadView const& view, + MPTIssue const& mpt, + AccountID const& account); + +/** Check if the destination account is allowed + * to receive MPT. Return tecNO_AUTH if it doesn't + * and tesSUCCESS otherwise. + */ +[[nodiscard]] TER +canTransfer( + ReadView const& view, + MPTIssue const& mpt, + AccountID const& from, + AccountID const& to); /** Deleter function prototype. Returns the status of the entry deletion * (if should not be skipped) and if the entry should be skipped. The status diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 13ac07e5e74..22a8249758e 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -177,6 +177,14 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer) return false; } +bool +isGlobalFrozen(ReadView const& view, MPTIssue const& mpt) +{ + if (auto const sle = view.read(keylet::mptIssuance(mpt.getMptID()))) + return sle->getFlags() & lsfMPTLocked; + return false; +} + bool isIndividualFrozen( ReadView const& view, @@ -197,6 +205,17 @@ isIndividualFrozen( return false; } +bool +isIndividualFrozen( + ReadView const& view, + AccountID const& account, + MPTIssue const& mpt) +{ + if (auto const sle = view.read(keylet::mptoken(mpt.getMptID(), account))) + return sle->getFlags() & lsfMPTLocked; + return false; +} + // Can the specified account spend the specified currency issued by // the specified issuer or does the freeze flag prohibit it? bool @@ -222,6 +241,14 @@ isFrozen( return false; } +bool +isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mpt) +{ + if (isGlobalFrozen(view, mpt)) + return true; + return isIndividualFrozen(view, account, mpt); +} + STAmount accountHolds( ReadView const& view, @@ -278,6 +305,47 @@ accountHolds( view, account, issue.currency, issue.account, zeroIfFrozen, j); } +STMPTAmount +accountHolds( + ReadView const& view, + AccountID const& account, + MPTIssue const& issue, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j) +{ + STMPTAmount amount; + + auto const sleMpt = view.read(keylet::mptoken(issue.getMptID(), account)); + if (!sleMpt) + amount.clear(issue); + else if (zeroIfFrozen == fhZERO_IF_FROZEN && isFrozen(view, account, issue)) + amount.clear(issue); + else + { + auto const amt = sleMpt->getFieldU64(sfMPTAmount); + auto const locked = sleMpt->getFieldU64(sfLockedAmount); + if (amt > locked) + amount = STMPTAmount{issue, amt - locked}; + + // only if auth check is needed, as it needs to do an additional read + // operation + if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED) + { + auto const sleIssuance = + view.read(keylet::mptIssuance(issue.getMptID())); + + // if auth is enabled on the issuance and mpt is not authorized, + // clear amount + if (sleIssuance && sleIssuance->isFlag(lsfMPTRequireAuth) && + !sleMpt->isFlag(lsfMPTAuthorized)) + amount.clear(issue); + } + } + + return amount; +} + STAmount accountFunds( ReadView const& view, @@ -495,6 +563,18 @@ transferRate(ReadView const& view, AccountID const& issuer) return parityRate; } +Rate +transferRate(ReadView const& view, MPTID const& id) +{ + auto const sle = view.read(keylet::mptIssuance(id)); + + // fee is 0-50,000 (0-50%), rate is 1,000,000,000-2,000,000,000 + if (sle && sle->isFieldPresent(sfTransferFee)) + return Rate{1'000'000'000u + 10'000 * sle->getFieldU16(sfTransferFee)}; + + return parityRate; +} + bool areCompatible( ReadView const& validLedger, @@ -1256,6 +1336,81 @@ accountSend( return terResult; } +static TER +rippleSend( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STMPTAmount const& saAmount, + STMPTAmount& saActual, + beast::Journal j, + WaiveTransferFee waiveFee) +{ + assert(uSenderID != uReceiverID); + + // Safe to get MPT since rippleSend is only called by accountSend + auto const issuer = saAmount.getIssuer(); + + if (uSenderID == issuer || uReceiverID == issuer || issuer == noAccount()) + { + // Direct send: redeeming IOUs and/or sending own IOUs. + auto const ter = + rippleCredit(view, uSenderID, uReceiverID, saAmount, j); + if (ter != tesSUCCESS) + return ter; + saActual = saAmount; + return tesSUCCESS; + } + + // Sending 3rd party MPTs: transit. + if (auto const sle = + view.read(keylet::mptIssuance(saAmount.issue().getMptID()))) + { + saActual = (waiveFee == WaiveTransferFee::Yes) + ? saAmount + : multiply( + saAmount, transferRate(view, saAmount.issue().getMptID())); + + JLOG(j.debug()) << "rippleSend> " << to_string(uSenderID) << " - > " + << to_string(uReceiverID) + << " : deliver=" << saAmount.getFullText() + << " cost=" << saActual.getFullText(); + + if (auto const terResult = + rippleCredit(view, issuer, uReceiverID, saAmount, j); + terResult != tesSUCCESS) + return terResult; + else + return rippleCredit(view, uSenderID, issuer, saActual, j); + } + + return tecINTERNAL; +} + +TER +accountSend( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STMPTAmount const& saAmount, + beast::Journal j, + WaiveTransferFee waiveFee) +{ + assert(saAmount >= beast::zero); + + /* If we aren't sending anything or if the sender is the same as the + * receiver then we don't need to do anything. + */ + if (!saAmount || (uSenderID == uReceiverID)) + return tesSUCCESS; + + // STAmount saActual{saAmount.asset()}; + STMPTAmount saActual{}; + + return rippleSend( + view, uSenderID, uReceiverID, saAmount, saActual, j, waiveFee); +} + static bool updateTrustLine( ApplyView& view, @@ -1537,6 +1692,39 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account) return tesSUCCESS; } +TER +requireAuth(ReadView const& view, MPTIssue const& mpt, AccountID const& account) +{ + auto const mptID = keylet::mptIssuance(mpt.getMptID()); + if (auto const sle = view.read(mptID); + sle && sle->getFieldU32(sfFlags) & lsfMPTRequireAuth) + { + auto const mptokenID = keylet::mptoken(mptID.key, account); + if (auto const tokSle = view.read(mptokenID); tokSle && + //(sle->getFlags() & lsfMPTRequireAuth) && + !(tokSle->getFlags() & lsfMPTAuthorized)) + return TER{tecNO_AUTH}; + } + return tesSUCCESS; +} + +TER +canTransfer( + ReadView const& view, + MPTIssue const& mpt, + AccountID const& from, + AccountID const& to) +{ + auto const mptID = keylet::mptIssuance(mpt.getMptID()); + if (auto const sle = view.read(mptID); + sle && !(sle->getFieldU32(sfFlags) & lsfMPTCanTransfer)) + { + if (from != (*sle)[sfIssuer] && to != (*sle)[sfIssuer]) + return TER{tecNO_AUTH}; + } + return tesSUCCESS; +} + TER cleanupOnAccountDelete( ApplyView& view, @@ -1662,4 +1850,85 @@ deleteAMMTrustLine( return tesSUCCESS; } +TER +rippleCredit( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STMPTAmount saAmount, + beast::Journal j) +{ + auto const mptID = keylet::mptIssuance(saAmount.issue().getMptID()); + auto const issuer = saAmount.getIssuer(); + if (uSenderID == issuer) + { + if (auto sle = view.peek(mptID)) + { + sle->setFieldU64( + sfOutstandingAmount, + sle->getFieldU64(sfOutstandingAmount) + saAmount.value()); + + if (sle->getFieldU64(sfOutstandingAmount) > + (*sle)[~sfMaximumAmount].value_or(maxMPTokenAmount)) + return tecMPT_MAX_AMOUNT_EXCEEDED; + + view.update(sle); + } + else + return tecINTERNAL; + } + else + { + auto const mptokenID = keylet::mptoken(mptID.key, uSenderID); + if (auto sle = view.peek(mptokenID)) + { + auto const amt = sle->getFieldU64(sfMPTAmount); + auto const pay = saAmount.value(); + if (amt >= pay) + { + if (amt == pay) + sle->makeFieldAbsent(sfMPTAmount); + else + sle->setFieldU64(sfMPTAmount, amt - pay); + view.update(sle); + } + else + return tecINSUFFICIENT_FUNDS; + } + else + return tecNO_AUTH; + } + + if (uReceiverID == issuer) + { + if (auto sle = view.peek(mptID)) + { + auto const outstanding = sle->getFieldU64(sfOutstandingAmount); + auto const redeem = saAmount.value(); + if (outstanding >= redeem) + { + sle->setFieldU64(sfOutstandingAmount, outstanding - redeem); + view.update(sle); + } + else + return tecINSUFFICIENT_FUNDS; + } + else + return tecINTERNAL; + } + else + { + auto const mptokenID = keylet::mptoken(mptID.key, uReceiverID); + if (auto sle = view.peek(mptokenID)) + { + sle->setFieldU64( + sfMPTAmount, sle->getFieldU64(sfMPTAmount) + saAmount.value()); + view.update(sle); + } + else + return tecNO_AUTH; + } + return tesSUCCESS; +} + } // namespace ripple diff --git a/src/xrpld/rpc/DeliveredAmount.h b/src/xrpld/rpc/DeliveredAmount.h index 2ebadd38752..63e11b707a1 100644 --- a/src/xrpld/rpc/DeliveredAmount.h +++ b/src/xrpld/rpc/DeliveredAmount.h @@ -72,7 +72,7 @@ insertDeliveredAmount( std::shared_ptr const&, TxMeta const&); -std::optional +std::optional getDeliveredAmount( RPC::Context const& context, std::shared_ptr const& serializedTx, diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h b/src/xrpld/rpc/MPTokenIssuanceID.h new file mode 100644 index 00000000000..4f94e61fa86 --- /dev/null +++ b/src/xrpld/rpc/MPTokenIssuanceID.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + 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_RPC_MPTOKENISSUANCEID_H_INCLUDED +#define RIPPLE_RPC_MPTOKENISSUANCEID_H_INCLUDED + +#include +#include +#include +#include + +#include +#include + +namespace ripple { + +namespace RPC { + +bool +canHaveMPTokenIssuanceID( + std::shared_ptr const& serializedTx, + TxMeta const& transactionMeta); + +std::optional +getIDFromCreatedIssuance(TxMeta const& transactionMeta); + +void +insertMPTokenIssuanceID( + Json::Value& response, + std::shared_ptr const& transaction, + TxMeta const& transactionMeta); +/** @} */ + +} // namespace RPC +} // namespace ripple + +#endif diff --git a/src/xrpld/rpc/detail/DeliveredAmount.cpp b/src/xrpld/rpc/detail/DeliveredAmount.cpp index 7874997e24f..4396fb291c7 100644 --- a/src/xrpld/rpc/detail/DeliveredAmount.cpp +++ b/src/xrpld/rpc/detail/DeliveredAmount.cpp @@ -44,7 +44,7 @@ namespace RPC { std::optional */ template -std::optional +std::optional getDeliveredAmount( GetLedgerIndex const& getLedgerIndex, GetCloseTime const& getCloseTime, @@ -174,7 +174,7 @@ insertDeliveredAmount( } template -static std::optional +static std::optional getDeliveredAmount( RPC::Context const& context, std::shared_ptr const& serializedTx, @@ -195,7 +195,7 @@ getDeliveredAmount( return {}; } -std::optional +std::optional getDeliveredAmount( RPC::Context const& context, std::shared_ptr const& serializedTx, diff --git a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp new file mode 100644 index 00000000000..c2aa3eea02a --- /dev/null +++ b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp @@ -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 + +#include +#include +#include +#include + +namespace ripple { + +namespace RPC { + +bool +canHaveMPTokenIssuanceID( + std::shared_ptr const& serializedTx, + TxMeta const& transactionMeta) +{ + if (!serializedTx) + return false; + + TxType const tt = serializedTx->getTxnType(); + if (tt != ttMPTOKEN_ISSUANCE_CREATE) + return false; + + // if the transaction failed nothing could have been delivered. + if (transactionMeta.getResultTER() != tesSUCCESS) + return false; + + return true; +} + +std::optional +getIDFromCreatedIssuance(TxMeta const& transactionMeta) +{ + for (STObject const& node : transactionMeta.getNodes()) + { + if (node.getFieldU16(sfLedgerEntryType) != ltMPTOKEN_ISSUANCE || + node.getFName() != sfCreatedNode) + continue; + + auto const& mptNode = + node.peekAtField(sfNewFields).downcast(); + return getMptID( + mptNode.getAccountID(sfIssuer), mptNode.getFieldU32(sfSequence)); + } + + return std::nullopt; +} + +void +insertMPTokenIssuanceID( + Json::Value& response, + std::shared_ptr const& transaction, + TxMeta const& transactionMeta) +{ + if (!canHaveMPTokenIssuanceID(transaction, transactionMeta)) + return; + + std::optional result = getIDFromCreatedIssuance(transactionMeta); + if (result.has_value()) + response[jss::mpt_issuance_id] = to_string(result.value()); +} + +} // namespace RPC +} // namespace ripple diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index 71513ddcd5c..687c33c45a4 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -285,7 +285,13 @@ getAccountObjects( if (!typeFilter.has_value() || typeMatchesFilter(typeFilter.value(), sleNode->getType())) { - jvObjects.append(sleNode->getJson(JsonOptions::none)); + auto sleJson = sleNode->getJson(JsonOptions::none); + if (sleNode->getType() == ltMPTOKEN_ISSUANCE) + sleJson[jss::mpt_issuance_id] = to_string(getMptID( + sleNode->getAccountID(sfIssuer), + (*sleNode)[sfSequence])); + + jvObjects.append(sleJson); } if (++i == mlimit) @@ -934,7 +940,7 @@ chooseLedgerEntryType(Json::Value const& params) std::pair result{RPC::Status::OK, ltANY}; if (params.isMember(jss::type)) { - static constexpr std::array, 22> + static constexpr std::array, 24> types{ {{jss::account, ltACCOUNT_ROOT}, {jss::amendments, ltAMENDMENTS}, @@ -958,7 +964,9 @@ chooseLedgerEntryType(Json::Value const& params) {jss::ticket, ltTICKET}, {jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID}, {jss::xchain_owned_create_account_claim_id, - ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}}}; + ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, + {jss::mpt_issuance, ltMPTOKEN_ISSUANCE}, + {jss::mptoken, ltMPTOKEN}}}; auto const& p = params[jss::type]; if (!p.isString()) diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 1fee84c683b..77938b4924a 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -200,7 +200,7 @@ checkPayment( if (!tx_json.isMember(jss::Amount)) return RPC::missing_field_error("tx_json.Amount"); - STAmount amount; + STEitherAmount amount; if (!amountFromJsonNoThrow(amount, tx_json[jss::Amount])) return RPC::invalid_field_error("tx_json.Amount"); @@ -213,7 +213,8 @@ checkPayment( if (!dstAccountID) return RPC::invalid_field_error("tx_json.Destination"); - if ((doPath == false) && params.isMember(jss::build_path)) + if (((doPath == false) && params.isMember(jss::build_path)) || + (params.isMember(jss::build_path) && !amount.isIssue())) return RPC::make_error( rpcINVALID_PARAMS, "Field 'build_path' not allowed in this context."); @@ -235,7 +236,7 @@ checkPayment( else { // If no SendMax, default to Amount with sender as issuer. - sendMax = amount; + sendMax = get(amount); sendMax.setIssuer(srcAddressID); } @@ -259,7 +260,7 @@ checkPayment( *dstAccountID, sendMax.issue().currency, sendMax.issue().account, - amount, + get(amount), std::nullopt, app); if (pf.findPaths(app.config().PATH_SEARCH_OLD)) diff --git a/src/xrpld/rpc/detail/Tuning.h b/src/xrpld/rpc/detail/Tuning.h index 4f4a8be1bf7..d4557a9fcfa 100644 --- a/src/xrpld/rpc/detail/Tuning.h +++ b/src/xrpld/rpc/detail/Tuning.h @@ -57,6 +57,9 @@ static LimitRange constexpr accountNFTokens = {20, 100, 400}; /** Limits for the nft_buy_offers & nft_sell_offers commands. */ static LimitRange constexpr nftOffers = {50, 250, 500}; +/** Limits for the nft_buy_offers & nft_sell_offers commands. */ +static LimitRange constexpr mptHolders = {10, 200, 400}; + static int constexpr defaultAutoFillFeeMultiplier = 10; static int constexpr defaultAutoFillFeeDivisor = 1; static int constexpr maxPathfindsInProgress = 2; diff --git a/src/xrpld/rpc/handlers/AccountObjects.cpp b/src/xrpld/rpc/handlers/AccountObjects.cpp index c192fbf9071..63389753244 100644 --- a/src/xrpld/rpc/handlers/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/AccountObjects.cpp @@ -222,7 +222,9 @@ doAccountObjects(RPC::JsonContext& context) {jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID}, {jss::xchain_owned_create_account_claim_id, ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, - {jss::bridge, ltBRIDGE}}; + {jss::bridge, ltBRIDGE}, + {jss::mpt_issuance, ltMPTOKEN_ISSUANCE}, + {jss::mptoken, ltMPTOKEN}}; typeFilter.emplace(); typeFilter->reserve(std::size(deletionBlockers)); diff --git a/src/xrpld/rpc/handlers/AccountTx.cpp b/src/xrpld/rpc/handlers/AccountTx.cpp index 3b9165eecf1..e4d78a4a03c 100644 --- a/src/xrpld/rpc/handlers/AccountTx.cpp +++ b/src/xrpld/rpc/handlers/AccountTx.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -361,6 +362,8 @@ populateJsonResponse( insertDeliveredAmount( jvObj[jss::meta], context, txn, *txnMeta); insertNFTSyntheticInJson(jvObj, sttx, *txnMeta); + RPC::insertMPTokenIssuanceID( + jvObj[jss::meta], sttx, *txnMeta); } else assert(false && "Missing transaction medatata"); diff --git a/src/xrpld/rpc/handlers/Handlers.h b/src/xrpld/rpc/handlers/Handlers.h index 0085f51465a..b1f65cdea57 100644 --- a/src/xrpld/rpc/handlers/Handlers.h +++ b/src/xrpld/rpc/handlers/Handlers.h @@ -51,6 +51,8 @@ doBlackList(RPC::JsonContext&); Json::Value doCanDelete(RPC::JsonContext&); Json::Value +doMPTHolders(RPC::JsonContext&); +Json::Value doChannelAuthorize(RPC::JsonContext&); Json::Value doChannelVerify(RPC::JsonContext&); diff --git a/src/xrpld/rpc/handlers/LedgerData.cpp b/src/xrpld/rpc/handlers/LedgerData.cpp index ad26b83b43b..80d321f0372 100644 --- a/src/xrpld/rpc/handlers/LedgerData.cpp +++ b/src/xrpld/rpc/handlers/LedgerData.cpp @@ -124,6 +124,10 @@ doLedgerData(RPC::JsonContext& context) Json::Value& entry = nodes.append(sle->getJson(JsonOptions::none)); entry[jss::index] = to_string(sle->key()); + + if (sle->getType() == ltMPTOKEN_ISSUANCE) + entry[jss::mpt_issuance_id] = to_string(getMptID( + sle->getAccountID(sfIssuer), (*sle)[sfSequence])); } } } diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index f461cd3100b..b8f5ffe104a 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -644,6 +644,73 @@ doLedgerEntry(RPC::JsonContext& context) uNodeIndex = keylet::oracle(*account, *documentID).key; } } + else if (context.params.isMember(jss::mpt_issuance)) + { + expectedType = ltMPTOKEN_ISSUANCE; + auto const unparsedMPTIssuanceID = + context.params[jss::mpt_issuance]; + if (unparsedMPTIssuanceID.isString()) + { + MPTID mptIssuanceID; + if (!mptIssuanceID.parseHex(unparsedMPTIssuanceID.asString())) + { + uNodeIndex = beast::zero; + jvResult[jss::error] = "malformedRequest"; + } + else + uNodeIndex = keylet::mptIssuance(mptIssuanceID).key; + } + else + { + jvResult[jss::error] = "malformedRequest"; + } + } + else if (context.params.isMember(jss::mptoken)) + { + expectedType = ltMPTOKEN; + if (!context.params[jss::mptoken].isObject()) + { + if (!uNodeIndex.parseHex( + context.params[jss::mptoken].asString())) + { + uNodeIndex = beast::zero; + jvResult[jss::error] = "malformedRequest"; + } + } + else if ( + !context.params[jss::mptoken].isMember(jss::mpt_issuance_id) || + !context.params[jss::mptoken].isMember(jss::account)) + { + jvResult[jss::error] = "malformedRequest"; + } + else + { + try + { + auto const mptIssuanceIdStr = + context.params[jss::mptoken][jss::mpt_issuance_id] + .asString(); + + MPTID mptIssuanceID; + if (!mptIssuanceID.parseHex(mptIssuanceIdStr)) + Throw( + "Cannot parse mpt_issuance_id"); + + auto const account = parseBase58( + context.params[jss::mptoken][jss::account].asString()); + + if (!account || account->isZero()) + jvResult[jss::error] = "malformedAddress"; + else + uNodeIndex = + keylet::mptoken(mptIssuanceID, *account).key; + } + catch (std::runtime_error const&) + { + jvResult[jss::error] = "malformedRequest"; + } + } + } else { if (context.params.isMember("params") && diff --git a/src/xrpld/rpc/handlers/Tx.cpp b/src/xrpld/rpc/handlers/Tx.cpp index e32d926e566..e57f8b3852d 100644 --- a/src/xrpld/rpc/handlers/Tx.cpp +++ b/src/xrpld/rpc/handlers/Tx.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -384,6 +385,7 @@ populateJsonResponse( insertDeliveredAmount( response[jss::meta], context, result.txn, *meta); insertNFTSyntheticInJson(response, sttx, *meta); + RPC::insertMPTokenIssuanceID(response[jss::meta], sttx, *meta); } } response[jss::validated] = result.validated;