From 6d6fda2c048dac27071c2748add497e61dc00e74 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Fri, 28 Jun 2024 19:22:55 -0400 Subject: [PATCH 01/58] Introduce MPT support (XLS-33d): New Transactions: - MPTokenIssuanceCreate - MPTokenIssuanceDestory - MPTokenIssuanceSet - MPTokenAuthorize Modified Transactions: - Payment - Clawback New Objects: - MPTokenIssuance - MPTokenAuthorize API updates: - ledger_entry - account_objects - ledger_data Read full spec: https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0033d-multi-purpose-tokens --------- Co-authored-by: Shawn Xie Co-authored-by: Howard Hinnant --- Builds/levelization/results/loops.txt | 2 +- include/xrpl/basics/MPTAmount.h | 185 ++ include/xrpl/basics/Number.h | 7 + include/xrpl/basics/base_uint.h | 2 + include/xrpl/protocol/AmountConversions.h | 8 +- include/xrpl/protocol/Asset.h | 149 ++ include/xrpl/protocol/Feature.h | 3 +- include/xrpl/protocol/Indexes.h | 27 + include/xrpl/protocol/Issue.h | 17 +- include/xrpl/protocol/LedgerFormats.h | 25 + include/xrpl/protocol/MPTIssue.h | 77 + include/xrpl/protocol/Protocol.h | 6 + include/xrpl/protocol/SField.h | 11 + include/xrpl/protocol/SOTemplate.h | 31 +- include/xrpl/protocol/STAmount.h | 246 ++- include/xrpl/protocol/STBitString.h | 8 + include/xrpl/protocol/STObject.h | 2 + include/xrpl/protocol/Serializer.h | 6 + include/xrpl/protocol/TER.h | 10 +- include/xrpl/protocol/TxFlags.h | 31 + include/xrpl/protocol/TxFormats.h | 11 + include/xrpl/protocol/UintTypes.h | 3 + include/xrpl/protocol/jss.h | 234 +-- src/libxrpl/basics/MPTAmount.cpp | 85 + src/libxrpl/basics/Number.cpp | 5 + src/libxrpl/protocol/Asset.cpp | 93 + src/libxrpl/protocol/Feature.cpp | 1 + src/libxrpl/protocol/Indexes.cpp | 37 + src/libxrpl/protocol/Issue.cpp | 10 + src/libxrpl/protocol/LedgerFormats.cpp | 30 + src/libxrpl/protocol/MPTIssue.cpp | 60 + src/libxrpl/protocol/Quality.cpp | 10 +- src/libxrpl/protocol/Rate2.cpp | 4 +- src/libxrpl/protocol/SField.cpp | 10 + src/libxrpl/protocol/STAmount.cpp | 472 ++--- src/libxrpl/protocol/STInteger.cpp | 24 +- src/libxrpl/protocol/STObject.cpp | 6 + src/libxrpl/protocol/STParsedJSON.cpp | 34 +- src/libxrpl/protocol/STTx.cpp | 44 +- src/libxrpl/protocol/STVar.cpp | 6 + src/libxrpl/protocol/TER.cpp | 6 + src/libxrpl/protocol/TxFormats.cpp | 38 +- src/test/app/Clawback_test.cpp | 1 + src/test/app/Flow_test.cpp | 4 - src/test/app/MPToken_test.cpp | 1695 +++++++++++++++++ src/test/app/SetAuth_test.cpp | 4 +- src/test/app/TrustAndBalance_test.cpp | 1 - src/test/jtx.h | 1 + src/test/jtx/Env.h | 6 + src/test/jtx/amount.h | 79 +- src/test/jtx/impl/mpt.cpp | 398 ++++ src/test/jtx/impl/trust.cpp | 8 +- src/test/jtx/mpt.h | 264 +++ src/test/jtx/trust.h | 5 +- src/test/ledger/PaymentSandbox_test.cpp | 2 - src/test/protocol/Quality_test.cpp | 2 +- src/test/protocol/STAmount_test.cpp | 2 - src/test/protocol/STTx_test.cpp | 32 +- src/xrpld/app/ledger/detail/LedgerToJson.cpp | 13 + src/xrpld/app/misc/NetworkOPs.cpp | 3 + src/xrpld/app/paths/Credit.cpp | 4 +- src/xrpld/app/paths/PathRequest.cpp | 2 +- src/xrpld/app/paths/Pathfinder.cpp | 7 +- src/xrpld/app/tx/detail/Clawback.cpp | 162 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 204 +- src/xrpld/app/tx/detail/InvariantCheck.h | 28 +- src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 251 +++ src/xrpld/app/tx/detail/MPTokenAuthorize.h | 63 + .../app/tx/detail/MPTokenIssuanceCreate.cpp | 142 ++ .../app/tx/detail/MPTokenIssuanceCreate.h | 60 + .../app/tx/detail/MPTokenIssuanceDestroy.cpp | 82 + .../app/tx/detail/MPTokenIssuanceDestroy.h | 48 + .../app/tx/detail/MPTokenIssuanceSet.cpp | 118 ++ src/xrpld/app/tx/detail/MPTokenIssuanceSet.h | 48 + src/xrpld/app/tx/detail/Payment.cpp | 301 ++- src/xrpld/app/tx/detail/SetTrust.cpp | 2 +- src/xrpld/app/tx/detail/applySteps.cpp | 12 + src/xrpld/ledger/View.h | 65 +- src/xrpld/ledger/detail/View.cpp | 298 ++- src/xrpld/rpc/MPTokenIssuanceID.h | 53 + src/xrpld/rpc/detail/MPTokenIssuanceID.cpp | 83 + src/xrpld/rpc/detail/RPCHelpers.cpp | 14 +- src/xrpld/rpc/detail/TransactionSign.cpp | 3 +- src/xrpld/rpc/detail/Tuning.h | 3 + src/xrpld/rpc/handlers/AccountObjects.cpp | 4 +- src/xrpld/rpc/handlers/AccountTx.cpp | 3 + src/xrpld/rpc/handlers/Handlers.h | 2 + src/xrpld/rpc/handlers/LedgerData.cpp | 4 + src/xrpld/rpc/handlers/LedgerEntry.cpp | 67 + src/xrpld/rpc/handlers/Tx.cpp | 2 + 90 files changed, 6180 insertions(+), 551 deletions(-) create mode 100644 include/xrpl/basics/MPTAmount.h create mode 100644 include/xrpl/protocol/Asset.h create mode 100644 include/xrpl/protocol/MPTIssue.h create mode 100644 src/libxrpl/basics/MPTAmount.cpp create mode 100644 src/libxrpl/protocol/Asset.cpp create mode 100644 src/libxrpl/protocol/MPTIssue.cpp create mode 100644 src/test/app/MPToken_test.cpp create mode 100644 src/test/jtx/impl/mpt.cpp create mode 100644 src/test/jtx/mpt.h create mode 100644 src/xrpld/app/tx/detail/MPTokenAuthorize.cpp create mode 100644 src/xrpld/app/tx/detail/MPTokenAuthorize.h create mode 100644 src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp create mode 100644 src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h create mode 100644 src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp create mode 100644 src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h create mode 100644 src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp create mode 100644 src/xrpld/app/tx/detail/MPTokenIssuanceSet.h create mode 100644 src/xrpld/rpc/MPTokenIssuanceID.h create mode 100644 src/xrpld/rpc/detail/MPTokenIssuanceID.cpp 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..5f8f3dbbe76 --- /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("MPT 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/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h index 0348e3c975d..270d009b916 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -33,13 +33,7 @@ toSTAmount(IOUAmount const& iou, Issue const& iss) { bool const isNeg = iou.signum() < 0; std::uint64_t const umant = isNeg ? -iou.mantissa() : iou.mantissa(); - return STAmount( - iss, - umant, - iou.exponent(), - /*native*/ false, - isNeg, - STAmount::unchecked()); + return STAmount(iss, umant, iou.exponent(), isNeg, STAmount::unchecked()); } inline STAmount diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h new file mode 100644 index 00000000000..760b2439557 --- /dev/null +++ b/include/xrpl/protocol/Asset.h @@ -0,0 +1,149 @@ +//------------------------------------------------------------------------------ +/* + 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_ASSET_H_INCLUDED +#define RIPPLE_PROTOCOL_ASSET_H_INCLUDED + +#include +#include +#include + +namespace ripple { + +template +concept ValidIssueType = + std::is_same_v || std::is_same_v; + +class Asset +{ +private: + using value_type = std::variant; + value_type issue_; + +public: + Asset() = default; + + Asset(Issue const& issue); + + Asset(MPTIssue const& mpt); + + Asset(MPTID const& mpt); + + explicit operator Issue() const; + + explicit operator MPTIssue() const; + + AccountID const& + getIssuer() const; + + template + constexpr TIss const& + get() const; + + template + TIss& + get(); + + template + constexpr bool + holds() const; + + std::string + getText() const; + + constexpr value_type const& + value() const; + + void + setJson(Json::Value& jv) const; + + friend constexpr bool + operator==(Asset const& lhs, Asset const& rhs); + + friend constexpr bool + operator!=(Asset const& lhs, Asset const& rhs); +}; + +template +constexpr bool +Asset::holds() const +{ + return std::holds_alternative(issue_); +} + +template +constexpr TIss const& +Asset::get() const +{ + if (!std::holds_alternative(issue_)) + Throw("Asset is not a requested issue"); + return std::get(issue_); +} + +template +TIss& +Asset::get() +{ + if (!std::holds_alternative(issue_)) + Throw("Asset is not a requested issue"); + return std::get(issue_); +} + +constexpr Asset::value_type const& +Asset::value() const +{ + return issue_; +} + +constexpr bool +operator==(Asset const& lhs, Asset const& rhs) +{ + return std::visit( + [&]( + TLhs const& issLhs, TRhs const& issRhs) { + if constexpr (std::is_same_v) + return issLhs == issRhs; + else + return false; + }, + lhs.issue_, + rhs.issue_); +} + +constexpr bool +operator!=(Asset const& lhs, Asset const& rhs) +{ + return !(lhs == rhs); +} + +inline bool +isXRP(Asset const& asset) +{ + return asset.holds() && isXRP(asset.get()); +} + +std::string +to_string(Asset const& asset); + +bool +validJSONAsset(Json::Value const& jv); + +} // namespace ripple + +#endif // RIPPLE_PROTOCOL_ASSET_H_INCLUDED 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..0c046378ea0 100644 --- a/include/xrpl/protocol/Issue.h +++ b/include/xrpl/protocol/Issue.h @@ -38,13 +38,12 @@ class Issue Currency currency{}; AccountID account{}; - Issue() - { - } + Issue() = default; - Issue(Currency const& c, AccountID const& a) : currency(c), account(a) - { - } + Issue(Currency const& c, AccountID const& a); + + AccountID const& + getIssuer() const; std::string getText() const; @@ -116,6 +115,12 @@ noIssue() return issue; } +inline bool +isXRP(Issue const& issue) +{ + return issue == xrpIssue(); +} + } // namespace ripple #endif 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..5892e2b91c8 --- /dev/null +++ b/include/xrpl/protocol/MPTIssue.h @@ -0,0 +1,77 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_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 const& + 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); + +std::string +to_string(MPTIssue const& mpt); + +} // namespace ripple + +#endif // RIPPLE_PROTOCOL_MPTISSUE_H_INCLUDED diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 8d8a71dfef8..f706b6a3bbb 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 = 0x7FFF'FFFF'FFFF'FFFFull; + /** A ledger index. */ using LedgerIndex = std::uint32_t; diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 7f54201a4b8..b7391d2971b 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -373,6 +373,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 +468,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 +482,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; @@ -564,6 +572,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 +596,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; @@ -651,6 +661,7 @@ extern SField const sfXChainClaimProofSig; extern SField const sfXChainCreateAccountProofSig; extern SField const sfXChainClaimAttestationCollectionElement; extern SField const sfXChainCreateAccountAttestationCollectionElement; +extern SField const MPToken; // array of objects (common) // ARRAY/1 is reserved for end of array diff --git a/include/xrpl/protocol/SOTemplate.h b/include/xrpl/protocol/SOTemplate.h index c0fcfb64358..51479353a95 100644 --- a/include/xrpl/protocol/SOTemplate.h +++ b/include/xrpl/protocol/SOTemplate.h @@ -39,6 +39,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 +50,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 +66,21 @@ class SOElement } } +public: + SOElement(SField const& fieldName, SOEStyle style) + : sField_(fieldName), style_(style), supportMpt_(soeMPTNone) + { + init(fieldName); + } + SOElement( + TypedField const& fieldName, + SOEStyle style, + SOETxMPTAmount supportMpt = soeMPTNotSupported) + : sField_(fieldName), style_(style), supportMpt_(supportMpt) + { + init(fieldName); + } + SField const& sField() const { @@ -73,6 +92,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..2f037a1c285 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -23,9 +23,10 @@ #include #include #include +#include #include #include -#include +#include #include #include #include @@ -33,6 +34,10 @@ namespace ripple { +template +concept AssetType = std::is_same_v || + std::is_convertible_v || std::is_convertible_v; + // Internal form: // 1: If amount is zero, then value is zero and offset is -100 // 2: Otherwise: @@ -51,10 +56,9 @@ class STAmount final : public STBase, public CountedObject using rep = std::pair; private: - Issue mIssue; + Asset mAsset; mantissa_type mValue; exponent_type mOffset; - bool mIsNative; // A shorthand for isXRP(mIssue). bool mIsNegative; public: @@ -70,8 +74,10 @@ class STAmount final : public STBase, public CountedObject // Max native value on network. static const std::uint64_t cMaxNativeN = 100000000000000000ull; - static const std::uint64_t cNotNative = 0x8000000000000000ull; - static const std::uint64_t cPosNative = 0x4000000000000000ull; + static const std::uint64_t cIssuedCurrency = 0x8000000000000000ull; + static const std::uint64_t cPositive = 0x4000000000000000ull; + static const std::uint64_t cMPToken = 0x2000000000000000ull; + static const std::uint64_t cValueMask = ~(cPositive | cMPToken); static std::uint64_t const uRateOne; @@ -84,31 +90,31 @@ class STAmount final : public STBase, public CountedObject }; // Do not call canonicalize + template STAmount( SField const& name, - Issue const& issue, + A const& asset, mantissa_type mantissa, exponent_type exponent, - bool native, bool negative, unchecked); + template STAmount( - Issue const& issue, + A const& asset, mantissa_type mantissa, exponent_type exponent, - bool native, bool negative, unchecked); // Call canonicalize + template STAmount( SField const& name, - Issue const& issue, - mantissa_type mantissa, - exponent_type exponent, - bool native, - bool negative); + A const& asset, + mantissa_type mantissa = 0, + exponent_type exponent = 0, + bool negative = false); STAmount(SField const& name, std::int64_t mantissa); @@ -117,37 +123,42 @@ class STAmount final : public STBase, public CountedObject 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); + template STAmount( - Issue const& issue, + A const& asset, std::uint64_t mantissa = 0, int exponent = 0, - bool negative = false); + bool negative = false) + : mAsset(asset) + , mValue(mantissa) + , mOffset(exponent) + , mIsNegative(negative) + { + canonicalize(); + } // VFALCO Is this needed when we have the previous signature? + template STAmount( - Issue const& issue, + A const& asset, std::uint32_t mantissa, int exponent = 0, bool negative = false); - STAmount(Issue const& issue, std::int64_t mantissa, int exponent = 0); + template + STAmount(A const& asset, std::int64_t mantissa, int exponent = 0); - STAmount(Issue const& issue, int mantissa, int exponent = 0); + template + STAmount(A const& asset, int mantissa, int exponent = 0); // Legacy support for new-style amounts STAmount(IOUAmount const& amount, Issue const& issue); STAmount(XRPAmount const& amount); + STAmount(MPTAmount const& amount, MPTIssue const& issue); operator Number() const; //-------------------------------------------------------------------------- @@ -162,12 +173,23 @@ class STAmount final : public STBase, public CountedObject bool native() const noexcept; + template + constexpr bool + holds() const noexcept; + bool negative() const noexcept; std::uint64_t mantissa() const noexcept; + Asset const& + asset() const; + + template + constexpr TIss const& + get() const; + Issue const& issue() const; @@ -223,17 +245,14 @@ class STAmount final : public STBase, public CountedObject // Zero while copying currency and issuer. void - clear(STAmount const& saTmpl); - - void - clear(Issue const& issue); + clear(Asset const& asset); void setIssuer(AccountID const& uIssuer); - /** Set the Issue for this amount and update mIsNative. */ + /** Set the Issue for this amount. */ void - setIssue(Issue const& issue); + setIssue(Asset const& asset); //-------------------------------------------------------------------------- // @@ -265,6 +284,8 @@ class STAmount final : public STBase, public CountedObject xrp() const; IOUAmount iou() const; + MPTAmount + mpt() const; private: static std::unique_ptr @@ -289,6 +310,99 @@ class STAmount final : public STBase, public CountedObject operator+(STAmount const& v1, STAmount const& v2); }; +template +STAmount::STAmount( + SField const& name, + A const& asset, + mantissa_type mantissa, + exponent_type exponent, + bool negative, + unchecked) + : STBase(name) + , mAsset(asset) + , mValue(mantissa) + , mOffset(exponent) + , mIsNegative(negative) +{ +} + +template +STAmount::STAmount( + A const& asset, + mantissa_type mantissa, + exponent_type exponent, + bool negative, + unchecked) + : mAsset(asset), mValue(mantissa), mOffset(exponent), mIsNegative(negative) +{ +} + +template +STAmount::STAmount( + SField const& name, + A const& asset, + std::uint64_t mantissa, + int exponent, + bool negative) + : STBase(name) + , mAsset(asset) + , mValue(mantissa) + , mOffset(exponent) + , mIsNegative(negative) +{ + assert(mValue <= std::numeric_limits::max()); + canonicalize(); +} + +template +STAmount::STAmount(A const& asset, std::int64_t mantissa, int exponent) + : mAsset(asset), mOffset(exponent) +{ + set(mantissa); + canonicalize(); +} + +template +STAmount::STAmount( + A const& asset, + std::uint32_t mantissa, + int exponent, + bool negative) + : STAmount(asset, safe_cast(mantissa), exponent, negative) +{ +} + +template +STAmount::STAmount(A const& asset, int mantissa, int exponent) + : STAmount(asset, safe_cast(mantissa), exponent) +{ +} + +// Legacy support for new-style amounts +inline STAmount::STAmount(IOUAmount const& amount, Issue const& issue) + : mAsset(issue) + , mOffset(amount.exponent()) + , mIsNegative(amount < beast::zero) +{ + if (mIsNegative) + mValue = static_cast(-amount.mantissa()); + else + mValue = static_cast(amount.mantissa()); + + canonicalize(); +} + +inline STAmount::STAmount(MPTAmount const& amount, MPTIssue const& issue) + : mAsset(issue), mOffset(0), mIsNegative(amount < beast::zero) +{ + if (mIsNegative) + mValue = unsafe_cast(-amount.value()); + else + mValue = unsafe_cast(amount.value()); + + canonicalize(); +} + //------------------------------------------------------------------------------ // // Creation @@ -300,7 +414,7 @@ STAmount amountFromQuality(std::uint64_t rate); STAmount -amountFromString(Issue const& issue, std::string const& amount); +amountFromString(Asset const& issue, std::string const& amount); STAmount amountFromJson(SField const& name, Json::Value const& v); @@ -331,7 +445,14 @@ STAmount::exponent() const noexcept inline bool STAmount::native() const noexcept { - return mIsNative; + return isXRP(mAsset); +} + +template +constexpr bool +STAmount::holds() const noexcept +{ + return mAsset.holds(); } inline bool @@ -346,22 +467,35 @@ STAmount::mantissa() const noexcept return mValue; } +inline Asset const& +STAmount::asset() const +{ + return mAsset; +} + +template +constexpr TIss const& +STAmount::get() const +{ + return mAsset.get(); +} + inline Issue const& STAmount::issue() const { - return mIssue; + return get(); } inline Currency const& STAmount::getCurrency() const { - return mIssue.currency; + return mAsset.get().currency; } inline AccountID const& STAmount::getIssuer() const { - return mIssue.account; + return mAsset.getIssuer(); } inline int @@ -373,7 +507,9 @@ STAmount::signum() const noexcept inline STAmount STAmount::zeroed() const { - return STAmount(mIssue); + if (mAsset.holds()) + return STAmount(mAsset.get()); + return STAmount(mAsset.get()); } inline STAmount::operator bool() const noexcept @@ -383,8 +519,10 @@ inline STAmount::operator bool() const noexcept inline STAmount::operator Number() const { - if (mIsNative) + if (native()) return xrp(); + if (mAsset.holds()) + return mpt(); return iou(); } @@ -413,30 +551,22 @@ STAmount::clear() { // The -100 is used to allow 0 to sort less than a small positive values // which have a negative exponent. - mOffset = mIsNative ? 0 : -100; + mOffset = native() ? 0 : -100; mValue = 0; mIsNegative = false; } -// Zero while copying currency and issuer. -inline void -STAmount::clear(STAmount const& saTmpl) -{ - clear(saTmpl.mIssue); -} - inline void -STAmount::clear(Issue const& issue) +STAmount::clear(Asset const& asset) { - setIssue(issue); + setIssue(asset); clear(); } inline void STAmount::setIssuer(AccountID const& uIssuer) { - mIssue.account = uIssuer; - setIssue(mIssue); + mAsset.get().account = uIssuer; } inline STAmount const& @@ -501,17 +631,17 @@ STAmount operator-(STAmount const& v1, STAmount const& v2); STAmount -divide(STAmount const& v1, STAmount const& v2, Issue const& issue); +divide(STAmount const& v1, STAmount const& v2, Asset const& asset); STAmount -multiply(STAmount const& v1, STAmount const& v2, Issue const& issue); +multiply(STAmount const& v1, STAmount const& v2, Asset const& asset); // multiply rounding result in specified direction STAmount mulRound( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp); // multiply following the rounding directions more precisely. @@ -519,7 +649,7 @@ STAmount mulRoundStrict( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp); // divide rounding result in specified direction @@ -527,7 +657,7 @@ STAmount divRound( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp); // divide following the rounding directions more precisely. @@ -535,7 +665,7 @@ STAmount divRoundStrict( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp); // Someone is offering X for Y, what is the rate? @@ -549,7 +679,7 @@ getRate(STAmount const& offerOut, STAmount const& offerIn); inline bool isXRP(STAmount const& amount) { - return isXRP(amount.issue().currency); + return isXRP(amount.asset()); } // Since `canonicalize` does not have access to a ledger, this is needed to put 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/STObject.h b/include/xrpl/protocol/STObject.h index b3cef83de5f..e55351cbc24 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -226,6 +226,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 diff --git a/include/xrpl/protocol/Serializer.h b/include/xrpl/protocol/Serializer.h index b85e8eb013d..d8d0b9222e3 100644 --- a/include/xrpl/protocol/Serializer.h +++ b/include/xrpl/protocol/Serializer.h @@ -373,6 +373,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..b4dcd6ff875 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,12 @@ 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, + tecMPT_ISSUANCE_NOT_FOUND = 196 }; //------------------------------------------------------------------------------ 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/UintTypes.h b/include/xrpl/protocol/UintTypes.h index a0a8069f669..74f3917faaf 100644 --- a/include/xrpl/protocol/UintTypes.h +++ b/include/xrpl/protocol/UintTypes.h @@ -58,6 +58,9 @@ 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. */ +using MPTID = base_uint<192>; + /** XRP currency. */ Currency const& xrpCurrency(); diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index e3eda80b44f..a5d3788379f 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(BidMax); // in: AMM Bid +JSS(BidMin); // in: AMM Bid +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(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/Asset.cpp b/src/libxrpl/protocol/Asset.cpp new file mode 100644 index 00000000000..d458644d948 --- /dev/null +++ b/src/libxrpl/protocol/Asset.cpp @@ -0,0 +1,93 @@ +//------------------------------------------------------------------------------ +/* + 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 { + +Asset::Asset(Issue const& issue) : issue_(issue) +{ +} + +Asset::Asset(MPTIssue const& mpt) : issue_(mpt) +{ +} + +Asset::Asset(MPTID const& mpt) : issue_(MPTIssue{mpt}) +{ +} + +Asset::operator Issue() const +{ + return get(); +} + +Asset::operator MPTIssue() const +{ + return get(); +} + +AccountID const& +Asset::getIssuer() const +{ + if (holds()) + return get().getIssuer(); + return get().getIssuer(); +} + +std::string +Asset::getText() const +{ + if (holds()) + return get().getText(); + return to_string(get().getMptID()); +} + +void +Asset::setJson(Json::Value& jv) const +{ + if (holds()) + jv[jss::mpt_issuance_id] = to_string(get().getMptID()); + else + { + jv[jss::currency] = to_string(get().currency); + if (!isXRP(get().currency)) + jv[jss::issuer] = toBase58(get().account); + } +} + +std::string +to_string(Asset const& asset) +{ + if (asset.holds()) + return to_string(asset.get()); + return to_string(asset.get().getMptID()); +} + +bool +validJSONAsset(Json::Value const& jv) +{ + if (jv.isMember(jss::mpt_issuance_id)) + return !(jv.isMember(jss::currency) || jv.isMember(jss::issuer)); + return jv.isMember(jss::currency); +} + +} // namespace ripple \ No newline at end of file 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..8aff535fde7 100644 --- a/src/libxrpl/protocol/Issue.cpp +++ b/src/libxrpl/protocol/Issue.cpp @@ -26,6 +26,16 @@ namespace ripple { +Issue::Issue(Currency const& c, AccountID const& a) : currency(c), account(a) +{ +} + +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..f46e6d9b2d8 --- /dev/null +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -0,0 +1,60 @@ +//------------------------------------------------------------------------------ +/* + 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 const& +MPTIssue::getIssuer() const +{ + // copy from id skipping the sequence + AccountID const* account = reinterpret_cast( + mptID_.data() + sizeof(std::uint32_t)); + + 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; +} + +std::string +to_string(MPTIssue const& mptIssue) +{ + return to_string(mptIssue.getMptID()); +} + +} // namespace ripple diff --git a/src/libxrpl/protocol/Quality.cpp b/src/libxrpl/protocol/Quality.cpp index 38b641328b0..c6464eba9d2 100644 --- a/src/libxrpl/protocol/Quality.cpp +++ b/src/libxrpl/protocol/Quality.cpp @@ -65,7 +65,7 @@ Quality::operator--(int) } template + *DivRoundFunc)(STAmount const&, STAmount const&, Asset const&, bool)> static Amounts ceil_in_impl( Amounts const& amount, @@ -77,7 +77,7 @@ ceil_in_impl( { Amounts result( limit, - DivRoundFunc(limit, quality.rate(), amount.out.issue(), roundUp)); + DivRoundFunc(limit, quality.rate(), amount.out.asset(), roundUp)); // Clamp out if (result.out > amount.out) result.out = amount.out; @@ -104,7 +104,7 @@ Quality::ceil_in_strict( } template + *MulRoundFunc)(STAmount const&, STAmount const&, Asset const&, bool)> static Amounts ceil_out_impl( Amounts const& amount, @@ -115,7 +115,7 @@ ceil_out_impl( if (amount.out > limit) { Amounts result( - MulRoundFunc(limit, quality.rate(), amount.in.issue(), roundUp), + MulRoundFunc(limit, quality.rate(), amount.in.asset(), roundUp), limit); // Clamp in if (result.in > amount.in) @@ -151,7 +151,7 @@ composed_quality(Quality const& lhs, Quality const& rhs) STAmount const rhs_rate(rhs.rate()); assert(rhs_rate != beast::zero); - STAmount const rate(mulRound(lhs_rate, rhs_rate, lhs_rate.issue(), true)); + STAmount const rate(mulRound(lhs_rate, rhs_rate, lhs_rate.asset(), true)); std::uint64_t const stored_exponent(rate.exponent() + 100); std::uint64_t const stored_mantissa(rate.mantissa()); diff --git a/src/libxrpl/protocol/Rate2.cpp b/src/libxrpl/protocol/Rate2.cpp index d85a49a5958..01a3e7deca5 100644 --- a/src/libxrpl/protocol/Rate2.cpp +++ b/src/libxrpl/protocol/Rate2.cpp @@ -51,7 +51,7 @@ multiply(STAmount const& amount, Rate const& rate) if (rate == parityRate) return amount; - return multiply(amount, detail::as_amount(rate), amount.issue()); + return multiply(amount, detail::as_amount(rate), amount.asset()); } STAmount @@ -62,7 +62,7 @@ multiplyRound(STAmount const& amount, Rate const& rate, bool roundUp) if (rate == parityRate) return amount; - return mulRound(amount, detail::as_amount(rate), amount.issue(), roundUp); + return mulRound(amount, detail::as_amount(rate), amount.asset(), roundUp); } STAmount diff --git a/src/libxrpl/protocol/SField.cpp b/src/libxrpl/protocol/SField.cpp index f8eb2d6f877..dc4bfb69a21 100644 --- a/src/libxrpl/protocol/SField.cpp +++ b/src/libxrpl/protocol/SField.cpp @@ -98,6 +98,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 +194,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 +208,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); @@ -307,6 +315,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 +328,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..735e3853c9d 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -78,26 +79,56 @@ getSNValue(STAmount const& amount) return ret; } +static std::int64_t +getMPTValue(STAmount const& amount) +{ + if (!amount.holds()) + Throw("amount is not MPT!"); + + auto ret = static_cast(amount.mantissa()); + + assert(static_cast(ret) == amount.mantissa()); + + if (amount.negative()) + ret = -ret; + + return ret; +} + static bool areComparable(STAmount const& v1, STAmount const& v2) { - return v1.native() == v2.native() && - v1.issue().currency == v2.issue().currency; + if (v1.holds() && v2.holds()) + return v1.native() == v2.native() && + v1.get().currency == v2.get().currency; + if (v1.holds() && v2.holds()) + return v1.get() == v2.get(); + return false; } STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name) { std::uint64_t value = sit.get64(); - // native - if ((value & cNotNative) == 0) + // native or MPT + if ((value & cIssuedCurrency) == 0) { + if ((value & cMPToken) != 0) + { + // is MPT + mOffset = 0; + mIsNegative = (value & cPositive) == 0; + mValue = (value << 8) | sit.get8(); + mAsset = sit.get192(); + return; + } + // else is XRP + mAsset = xrpIssue(); // positive - if ((value & cPosNative) != 0) + if ((value & cPositive) != 0) { - mValue = value & ~cPosNative; + mValue = value & cValueMask; mOffset = 0; - mIsNative = true; mIsNegative = false; return; } @@ -106,9 +137,8 @@ STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name) if (value == 0) Throw("negative zero is not canonical"); - mValue = value; + mValue = value & cValueMask; mOffset = 0; - mIsNative = true; mIsNegative = true; return; } @@ -140,7 +170,7 @@ STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name) Throw("invalid currency value"); } - mIssue = issue; + mAsset = issue; mValue = value; mOffset = offset; mIsNegative = isNegative; @@ -151,97 +181,32 @@ STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name) if (offset != 512) Throw("invalid currency value"); - mIssue = issue; + mAsset = issue; mValue = 0; mOffset = 0; mIsNegative = false; 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, - exponent_type exponent, - bool native, - bool negative, - unchecked) - : mIssue(issue) - , mValue(mantissa) - , mOffset(exponent) - , mIsNative(native) - , mIsNegative(negative) -{ -} - -STAmount::STAmount( - SField const& name, - Issue const& issue, - mantissa_type mantissa, - exponent_type exponent, - bool native, - bool negative) - : STBase(name) - , mIssue(issue) - , mValue(mantissa) - , mOffset(exponent) - , mIsNative(native) - , mIsNegative(negative) -{ - canonicalize(); -} - STAmount::STAmount(SField const& name, std::int64_t mantissa) - : STBase(name), mOffset(0), mIsNative(true) + : STBase(name), mAsset(xrpIssue()), mOffset(0) { set(mantissa); } STAmount::STAmount(SField const& name, std::uint64_t mantissa, bool negative) : STBase(name) + , mAsset(xrpIssue()) , 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) + , mAsset(from.mAsset) , mValue(from.mValue) , mOffset(from.mOffset) , mIsNegative(from.mIsNegative) @@ -253,62 +218,16 @@ STAmount::STAmount(SField const& name, STAmount const& from) //------------------------------------------------------------------------------ STAmount::STAmount(std::uint64_t mantissa, bool negative) - : mValue(mantissa) + : mAsset(xrpIssue()) + , mValue(mantissa) , mOffset(0) - , mIsNative(true) , mIsNegative(mantissa != 0 && negative) { assert(mValue <= std::numeric_limits::max()); } -STAmount::STAmount( - Issue const& issue, - std::uint64_t mantissa, - int exponent, - bool negative) - : mIssue(issue), mValue(mantissa), mOffset(exponent), mIsNegative(negative) -{ - canonicalize(); -} - -STAmount::STAmount(Issue const& issue, std::int64_t mantissa, int exponent) - : mIssue(issue), mOffset(exponent) -{ - set(mantissa); - canonicalize(); -} - -STAmount::STAmount( - Issue const& issue, - std::uint32_t mantissa, - int exponent, - bool negative) - : STAmount(issue, safe_cast(mantissa), exponent, negative) -{ -} - -STAmount::STAmount(Issue const& issue, int mantissa, int exponent) - : STAmount(issue, safe_cast(mantissa), exponent) -{ -} - -// Legacy support for new-style amounts -STAmount::STAmount(IOUAmount const& amount, Issue const& issue) - : mIssue(issue) - , mOffset(amount.exponent()) - , mIsNative(false) - , mIsNegative(amount < beast::zero) -{ - if (mIsNegative) - mValue = static_cast(-amount.mantissa()); - else - mValue = static_cast(amount.mantissa()); - - canonicalize(); -} - STAmount::STAmount(XRPAmount const& amount) - : mOffset(0), mIsNative(true), mIsNegative(amount < beast::zero) + : mAsset(xrpIssue()), mOffset(0), mIsNegative(amount < beast::zero) { if (mIsNegative) mValue = unsafe_cast(-amount.drops()); @@ -344,7 +263,7 @@ STAmount::move(std::size_t n, void* buf) XRPAmount STAmount::xrp() const { - if (!mIsNative) + if (!native()) Throw( "Cannot return non-native STAmount as XRPAmount"); @@ -359,7 +278,7 @@ STAmount::xrp() const IOUAmount STAmount::iou() const { - if (mIsNative) + if (native() || holds()) Throw("Cannot return native STAmount as IOUAmount"); auto mantissa = static_cast(mValue); @@ -371,10 +290,24 @@ STAmount::iou() const return {mantissa, exponent}; } +MPTAmount +STAmount::mpt() const +{ + if (!holds()) + Throw("Cannot return STAmount as MPTAmount"); + + auto value = static_cast(mValue); + + if (mIsNegative) + value = -value; + + return MPTAmount{value}; +} + STAmount& STAmount::operator=(IOUAmount const& iou) { - assert(mIsNative == false); + assert(native() == false); mOffset = iou.exponent(); mIsNegative = iou < beast::zero; if (mIsNegative) @@ -418,7 +351,7 @@ operator+(STAmount const& v1, STAmount const& v2) // Result must be in terms of v1 currency and issuer. return { v1.getFName(), - v1.issue(), + v1.asset(), v2.mantissa(), v2.exponent(), v2.negative()}; @@ -426,6 +359,8 @@ operator+(STAmount const& v1, STAmount const& v2) if (v1.native()) return {v1.getFName(), getSNValue(v1) + getSNValue(v2)}; + if (v1.holds()) + return {v1.mAsset, v1.mpt().value() + v2.mpt().value()}; if (getSTNumberSwitchover()) { @@ -462,18 +397,18 @@ 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.getFName(), v1.asset()}; if (fv >= 0) return STAmount{ v1.getFName(), - v1.issue(), + v1.asset(), static_cast(fv), ov1, false}; return STAmount{ - v1.getFName(), v1.issue(), static_cast(-fv), ov1, true}; + v1.getFName(), v1.asset(), static_cast(-fv), ov1, true}; } STAmount @@ -487,10 +422,9 @@ operator-(STAmount const& v1, STAmount const& v2) std::uint64_t const STAmount::uRateOne = getRate(STAmount(1), STAmount(1)); void -STAmount::setIssue(Issue const& issue) +STAmount::setIssue(Asset const& asset) { - mIssue = issue; - mIsNative = isXRP(*this); + mAsset = asset; } // Convert an offer into an index amount so they sort by rate. @@ -529,13 +463,12 @@ STAmount::setJson(Json::Value& elem) const { elem = Json::objectValue; - if (!mIsNative) + if (!native()) { // It is an error for currency or issuer not to be specified for valid // json. elem[jss::value] = getText(); - elem[jss::currency] = to_string(mIssue.currency); - elem[jss::issuer] = to_string(mIssue.account); + mAsset.setJson(elem); } else { @@ -561,7 +494,7 @@ STAmount::getFullText() const std::string ret; ret.reserve(64); - ret = getText() + "/" + mIssue.getText(); + ret = getText() + "/" + mAsset.getText(); return ret; } @@ -581,7 +514,7 @@ STAmount::getText() const bool const scientific( (mOffset != 0) && ((mOffset < -25) || (mOffset > -5))); - if (mIsNative || scientific) + if (native() || mAsset.holds() || scientific) { ret.append(raw_value); @@ -660,19 +593,28 @@ Json::Value STAmount::getJson(JsonOptions) const void STAmount::add(Serializer& s) const { - if (mIsNative) + if (native()) { assert(mOffset == 0); if (!mIsNegative) - s.add64(mValue | cPosNative); + s.add64(mValue | cPositive); else s.add64(mValue); } + else if (mAsset.holds()) + { + auto u8 = static_cast(cMPToken >> 56); + if (!mIsNegative) + u8 |= static_cast(cPositive >> 56); + s.add8(u8); + s.add64(mValue); + s.addBitString(mAsset.get().getMptID()); + } else { if (*this == beast::zero) - s.add64(cNotNative); + s.add64(cIssuedCurrency); else if (mIsNegative) // 512 = not native s.add64( mValue | @@ -682,9 +624,8 @@ STAmount::add(Serializer& s) const mValue | (static_cast(mOffset + 512 + 256 + 97) << (64 - 10))); - - s.addBitString(mIssue.currency); - s.addBitString(mIssue.account); + s.addBitString(mAsset.get().currency); + s.addBitString(mAsset.get().account); } } @@ -698,7 +639,7 @@ STAmount::isEquivalent(const STBase& t) const bool STAmount::isDefault() const { - return (mValue == 0) && mIsNative; + return (mValue == 0) && native(); } //------------------------------------------------------------------------------ @@ -722,11 +663,8 @@ STAmount::isDefault() const void STAmount::canonicalize() { - if (isXRP(*this)) + if (isXRP(*this) || mAsset.holds()) { - // native currency amounts should always have an offset of zero - mIsNative = true; - // log(2^64,10) ~ 19.2 if (mValue == 0 || mOffset <= -20) { @@ -748,9 +686,18 @@ STAmount::canonicalize() { Number num( mIsNegative ? -mValue : mValue, mOffset, Number::unchecked{}); - XRPAmount xrp{num}; - mIsNegative = xrp.drops() < 0; - mValue = mIsNegative ? -xrp.drops() : xrp.drops(); + if (native()) + { + XRPAmount xrp{num}; + mIsNegative = xrp.drops() < 0; + mValue = mIsNegative ? -xrp.drops() : xrp.drops(); + } + else + { + MPTAmount c{num}; + mIsNegative = c.value() < 0; + mValue = mIsNegative ? -c.value() : c.value(); + } mOffset = 0; } else @@ -767,23 +714,32 @@ STAmount::canonicalize() { // N.B. do not move the overflow check to after the // multiplication - if (mValue > cMaxNativeN) - Throw( - "Native currency amount out of range"); + if (isXRP(*this)) + { + if (mValue > cMaxNativeN) + Throw( + "Native currency amount out of range"); + } + else if (mValue > maxMPTokenAmount) + Throw("MPT amount out of range"); } mValue *= 10; --mOffset; } } - if (mValue > cMaxNativeN) - Throw("Native currency amount out of range"); + if (isXRP(*this)) + { + if (mValue > cMaxNativeN) + Throw( + "Native currency amount out of range"); + } + else if (mValue > maxMPTokenAmount) + Throw("MPT amount out of range"); return; } - mIsNative = false; - if (getSTNumberSwitchover()) { *this = iou(); @@ -859,7 +815,7 @@ amountFromQuality(std::uint64_t rate) } STAmount -amountFromString(Issue const& issue, std::string const& amount) +amountFromString(Asset const& asset, std::string const& amount) { static boost::regex const reNumber( "^" // the beginning of the string @@ -891,9 +847,10 @@ amountFromString(Issue const& issue, std::string const& amount) bool negative = (match[1].matched && (match[1] == "-")); - // Can't specify XRP using fractional representation - if (isXRP(issue) && match[3].matched) - Throw("XRP must be specified in integral drops."); + // Can't specify XRP or MPT using fractional representation + if ((isXRP(asset) || asset.holds()) && match[3].matched) + Throw( + "XRP and MPT must be specified as integral amount."); std::uint64_t mantissa; int exponent; @@ -920,7 +877,7 @@ amountFromString(Issue const& issue, std::string const& amount) exponent += beast::lexicalCastThrow(std::string(match[7])); } - return {issue, mantissa, exponent, negative}; + return {asset, mantissa, exponent, negative}; } STAmount @@ -929,11 +886,12 @@ amountFromJson(SField const& name, Json::Value const& v) STAmount::mantissa_type mantissa = 0; STAmount::exponent_type exponent = 0; bool negative = false; - Issue issue; + Asset asset; Json::Value value; - Json::Value currency; + Json::Value currencyOrMPTID; Json::Value issuer; + bool isMPT = false; if (v.isNull()) { @@ -942,14 +900,25 @@ amountFromJson(SField const& name, Json::Value const& v) } else if (v.isObject()) { + if (!validJSONAsset(v)) + Throw("Invalid Asset's Json specification"); + value = v[jss::value]; - currency = v[jss::currency]; - issuer = v[jss::issuer]; + 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); - currency = v.get(Json::UInt(1), Json::nullValue); + currencyOrMPTID = v.get(Json::UInt(1), Json::nullValue); issuer = v.get(Json::UInt(2), Json::nullValue); } else if (v.isString()) @@ -964,7 +933,7 @@ amountFromJson(SField const& name, Json::Value const& v) value = elements[0]; if (elements.size() > 1) - currency = elements[1]; + currencyOrMPTID = elements[1]; if (elements.size() > 2) issuer = elements[2]; @@ -974,26 +943,38 @@ amountFromJson(SField const& name, Json::Value const& v) value = v; } - bool const native = !currency.isString() || currency.asString().empty() || - (currency.asString() == systemCurrencyCode()); + 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(); + asset = 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 (isMPT) + { + // sequence (32 bits) + account (160 bits) + uint192 u; + if (!u.parseHex(currencyOrMPTID.asString())) + Throw("invalid MPTokenIssuanceID"); + asset = u; + } + else + { + Issue issue; + if (!to_currency(issue.currency, currencyOrMPTID.asString())) + Throw("invalid currency"); + if (!issuer.isString() || + !to_issuer(issue.account, issuer.asString())) + Throw("invalid issuer"); + if (isXRP(issue)) + Throw("invalid issuer"); + asset = issue; + } } if (value.isInt()) @@ -1014,7 +995,7 @@ amountFromJson(SField const& name, Json::Value const& v) } else if (value.isString()) { - auto const ret = amountFromString(issue, value.asString()); + auto const ret = amountFromString(asset, value.asString()); mantissa = ret.mantissa(); exponent = ret.exponent(); @@ -1025,7 +1006,7 @@ amountFromJson(SField const& name, Json::Value const& v) Throw("invalid amount type"); } - return {name, issue, mantissa, exponent, native, negative}; + return {name, asset, mantissa, exponent, negative}; } bool @@ -1099,10 +1080,9 @@ operator-(STAmount const& value) return value; return STAmount( value.getFName(), - value.issue(), + value.asset(), value.mantissa(), value.exponent(), - value.native(), !value.negative(), STAmount::unchecked{}); } @@ -1161,20 +1141,20 @@ muldiv_round( } STAmount -divide(STAmount const& num, STAmount const& den, Issue const& issue) +divide(STAmount const& num, STAmount const& den, Asset const& asset) { if (den == beast::zero) Throw("division by zero"); if (num == beast::zero) - return {issue}; + return {asset}; std::uint64_t numVal = num.mantissa(); std::uint64_t denVal = den.mantissa(); int numOffset = num.exponent(); int denOffset = den.exponent(); - if (num.native()) + if (num.native() || num.holds()) { while (numVal < STAmount::cMinValue) { @@ -1184,7 +1164,7 @@ divide(STAmount const& num, STAmount const& den, Issue const& issue) } } - if (den.native()) + if (den.native() || den.holds()) { while (denVal < STAmount::cMinValue) { @@ -1199,19 +1179,19 @@ divide(STAmount const& num, STAmount const& den, Issue const& issue) // 10^32 to 10^33) followed by a division, so the result // is in the range of 10^16 to 10^15. return STAmount( - issue, + asset, muldiv(numVal, tenTo17, denVal) + 5, numOffset - denOffset - 17, num.negative() != den.negative()); } STAmount -multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) +multiply(STAmount const& v1, STAmount const& v2, Asset const& asset) { if (v1 == beast::zero || v2 == beast::zero) - return STAmount(issue); + return STAmount(asset); - if (v1.native() && v2.native() && isXRP(issue)) + if (v1.native() && v2.native() && isXRP(asset)) { std::uint64_t const minV = getSNValue(v1) < getSNValue(v2) ? getSNValue(v1) : getSNValue(v2); @@ -1226,16 +1206,36 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) return STAmount(v1.getFName(), minV * maxV); } + if (v1.holds() && v2.holds() && asset.holds()) + { + std::uint64_t const minV = getMPTValue(v1) < getMPTValue(v2) + ? getMPTValue(v1) + : getMPTValue(v2); + std::uint64_t const maxV = getMPTValue(v1) < getMPTValue(v2) + ? getMPTValue(v2) + : getMPTValue(v1); + + if (minV > 3000000000ull) // sqrt(cMaxNative) + Throw("Asset value overflow"); + + if (((maxV >> 32) * minV) > 2095475792ull) // cMaxNative / 2^32 + Throw("Asset value overflow"); + + return STAmount(asset, minV * maxV); + } if (getSTNumberSwitchover()) - return {IOUAmount{Number{v1} * Number{v2}}, issue}; + { + auto const r = Number{v1} * Number{v2}; + return STAmount{asset, r.mantissa(), r.exponent()}; + } std::uint64_t value1 = v1.mantissa(); std::uint64_t value2 = v2.mantissa(); int offset1 = v1.exponent(); int offset2 = v2.exponent(); - if (v1.native()) + if (v1.native() || v1.holds()) { while (value1 < STAmount::cMinValue) { @@ -1244,7 +1244,7 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) } } - if (v2.native()) + if (v2.native() || v2.holds()) { while (value2 < STAmount::cMinValue) { @@ -1258,7 +1258,7 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) // range. Dividing their product by 10^14 maintains the // precision, by scaling the result to 10^16 to 10^18. return STAmount( - issue, + asset, muldiv(value1, value2, tenTo14) + 7, offset1 + offset2 + 14, v1.negative() != v2.negative()); @@ -1395,14 +1395,15 @@ static STAmount mulRoundImpl( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp) { if (v1 == beast::zero || v2 == beast::zero) - return {issue}; + return {asset}; - bool const xrp = isXRP(issue); + bool const xrp = isXRP(asset); + // TODO MPT if (v1.native() && v2.native() && xrp) { std::uint64_t minV = @@ -1419,10 +1420,28 @@ mulRoundImpl( return STAmount(v1.getFName(), minV * maxV); } + if (v1.holds() && v2.holds() && asset.holds()) + { + std::uint64_t minV = (getMPTValue(v1) < getMPTValue(v2)) + ? getMPTValue(v1) + : getMPTValue(v2); + std::uint64_t maxV = (getMPTValue(v1) < getMPTValue(v2)) + ? getMPTValue(v2) + : getMPTValue(v1); + + if (minV > 3000000000ull) // sqrt(cMaxNative) + Throw("Asset value overflow"); + + if (((maxV >> 32) * minV) > 2095475792ull) // cMaxNative / 2^32 + Throw("Asset value overflow"); + + return STAmount(asset, minV * maxV); + } + std::uint64_t value1 = v1.mantissa(), value2 = v2.mantissa(); int offset1 = v1.exponent(), offset2 = v2.exponent(); - if (v1.native()) + if (v1.native() || v1.holds()) { while (value1 < STAmount::cMinValue) { @@ -1431,7 +1450,7 @@ mulRoundImpl( } } - if (v2.native()) + if (v2.native() || v2.holds()) { while (value2 < STAmount::cMinValue) { @@ -1462,7 +1481,7 @@ mulRoundImpl( // If appropriate, tell Number to round down. This gives the desired // result from STAmount::canonicalize. MightSaveRound const savedRound(Number::towards_zero); - return STAmount(issue, amount, offset, resultNegative); + return STAmount(asset, amount, offset, resultNegative); }(); if (roundUp && !resultNegative && !result) @@ -1479,7 +1498,7 @@ mulRoundImpl( amount = STAmount::cMinValue; offset = STAmount::cMinOffset; } - return STAmount(issue, amount, offset, resultNegative); + return STAmount(asset, amount, offset, resultNegative); } return result; } @@ -1488,22 +1507,22 @@ STAmount mulRound( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp) { return mulRoundImpl( - v1, v2, issue, roundUp); + v1, v2, asset, roundUp); } STAmount mulRoundStrict( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp) { return mulRoundImpl( - v1, v2, issue, roundUp); + v1, v2, asset, roundUp); } // We might need to use NumberRoundModeGuard. Allow the caller @@ -1513,19 +1532,19 @@ static STAmount divRoundImpl( STAmount const& num, STAmount const& den, - Issue const& issue, + Asset const& asset, bool roundUp) { if (den == beast::zero) Throw("division by zero"); if (num == beast::zero) - return {issue}; + return {asset}; std::uint64_t numVal = num.mantissa(), denVal = den.mantissa(); int numOffset = num.exponent(), denOffset = den.exponent(); - if (num.native()) + if (num.native() || num.holds()) { while (numVal < STAmount::cMinValue) { @@ -1534,7 +1553,7 @@ divRoundImpl( } } - if (den.native()) + if (den.native() || den.holds()) { while (denVal < STAmount::cMinValue) { @@ -1559,7 +1578,8 @@ divRoundImpl( int offset = numOffset - denOffset - 17; if (resultNegative != roundUp) - canonicalizeRound(isXRP(issue), amount, offset, roundUp); + canonicalizeRound( + isXRP(asset) || asset.holds(), amount, offset, roundUp); STAmount result = [&]() { // If appropriate, tell Number the rounding mode we are using. @@ -1568,12 +1588,12 @@ divRoundImpl( using enum Number::rounding_mode; MightSaveRound const savedRound( roundUp ^ resultNegative ? upward : downward); - return STAmount(issue, amount, offset, resultNegative); + return STAmount(asset, amount, offset, resultNegative); }(); if (roundUp && !resultNegative && !result) { - if (isXRP(issue)) + if (isXRP(asset) || asset.holds()) { // return the smallest value above zero amount = 1; @@ -1585,7 +1605,7 @@ divRoundImpl( amount = STAmount::cMinValue; offset = STAmount::cMinOffset; } - return STAmount(issue, amount, offset, resultNegative); + return STAmount(asset, amount, offset, resultNegative); } return result; } @@ -1594,20 +1614,20 @@ STAmount divRound( STAmount const& num, STAmount const& den, - Issue const& issue, + Asset const& asset, bool roundUp) { - return divRoundImpl(num, den, issue, roundUp); + return divRoundImpl(num, den, asset, roundUp); } STAmount divRoundStrict( STAmount const& num, STAmount const& den, - Issue const& issue, + Asset const& asset, bool roundUp) { - return divRoundImpl(num, den, issue, roundUp); + return divRoundImpl(num, den, asset, roundUp); } } // namespace ripple diff --git a/src/libxrpl/protocol/STInteger.cpp b/src/libxrpl/protocol/STInteger.cpp index 7b7420006f9..148a05d2ab6 100644 --- a/src/libxrpl/protocol/STInteger.cpp +++ b/src/libxrpl/protocol/STInteger.cpp @@ -194,11 +194,25 @@ STUInt64::getText() const template <> Json::Value STUInt64::getJson(JsonOptions) const { - std::string str(16, 0); - auto ret = std::to_chars(str.data(), str.data() + str.size(), value_, 16); - assert(ret.ec == std::errc()); - str.resize(std::distance(str.data(), ret.ptr)); - return str; + auto convertToString = [](uint64_t const value, int const base) { + assert(base == 10 || base == 16); + std::string str( + base == 10 ? 20 : 16, 0); // Allocate space depending on base + auto ret = + std::to_chars(str.data(), str.data() + str.size(), value, base); + assert(ret.ec == std::errc()); + str.resize(std::distance(str.data(), ret.ptr)); + return str; + }; + + if (auto const& fName = getFName(); fName == sfMaximumAmount || + fName == sfOutstandingAmount || fName == sfLockedAmount || + fName == sfMPTAmount) + { + return convertToString(value_, 10); // Convert to base 10 + } + + return convertToString(value_, 16); // Convert to base 16 } } // namespace ripple diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index bde83ec31a1..7e62fc25bd6 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 { diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index dec5e87eaee..7e6b8ff5975 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -398,8 +398,16 @@ parseLeaf( std::uint64_t val; + bool const useBase10 = field == sfMaximumAmount || + field == sfOutstandingAmount || + field == sfLockedAmount || field == sfMPTAmount; + + // if the field is amount, serialize as base 10 auto [p, ec] = std::from_chars( - str.data(), str.data() + str.size(), val, 16); + str.data(), + str.data() + str.size(), + val, + useBase10 ? 10 : 16); if (ec != std::errc() || (p != str.data() + str.size())) Throw("invalid data"); @@ -454,6 +462,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()) { diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 149186d43ce..cb0d0d1495e 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -143,9 +143,14 @@ STTx::getMentionedAccounts() const } else if (auto samt = dynamic_cast(&it)) { - auto const& issuer = samt->getIssuer(); - if (!isXRP(issuer)) - list.insert(issuer); + if (samt->holds()) + { + auto const& issuer = samt->getIssuer(); + if (!isXRP(issuer)) + list.insert(issuer); + } + else + list.insert(samt->getIssuer()); } } @@ -543,6 +548,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).holds()) + { + if (e.supportMPT() == soeMPTNotSupported) + return true; + } + } + } + } + return false; +} + bool passesLocalChecks(STObject const& st, std::string& reason) { @@ -560,6 +591,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..0cb52b5d24e 100644 --- a/src/libxrpl/protocol/STVar.cpp +++ b/src/libxrpl/protocol/STVar.cpp @@ -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; @@ -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/TER.cpp b/src/libxrpl/protocol/TER.cpp index 917bbf26a9f..1803d836625 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,10 @@ 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(tecMPT_ISSUANCE_NOT_FOUND, "The MPTokenIssuance object is not found"), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), @@ -197,6 +202,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/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..4d1397eab83 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -1023,14 +1023,12 @@ struct Flow_test : public beast::unit_test::suite 9000000000000000ll, -17, false, - false, STAmount::unchecked{}}; STAmount tinyAmt3{ USD.issue(), 9000000000000003ll, -17, false, - false, STAmount::unchecked{}}; env(offer(gw, drops(9000000000), tinyAmt3)); @@ -1058,14 +1056,12 @@ struct Flow_test : public beast::unit_test::suite 9000000000000000ll, -17, false, - false, STAmount::unchecked{}}; STAmount tinyAmt3{ USD.issue(), 9000000000000003ll, -17, false, - false, STAmount::unchecked{}}; env(pay(gw, alice, tinyAmt1)); diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp new file mode 100644 index 00000000000..6ed9c66fb9b --- /dev/null +++ b/src/test/app/MPToken_test.cpp @@ -0,0 +1,1695 @@ +//------------------------------------------------------------------------------ +/* + 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 = "18446744073709551600", // FFFFFFFFFFFFFFF0 + .assetScale = 0, + .transferFee = 0, + .metadata = "test", + .err = temMALFORMED}); + mptAlice.create( + {.maxAmt = "9223372036854775808", // 8000000000000000 + .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 = "9223372036854775807", // 7FFFFFFFFFFFFFFF + .assetScale = 1, + .transferFee = 10, + .metadata = "123", + .ownerCount = 1, + .flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | + tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback}); + + // Get the hash for the most recent transaction. + std::string const txHash{ + env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; + + Json::Value const result = env.rpc("tx", txHash)[jss::result]; + BEAST_EXPECT( + result[sfMaximumAmount.getJsonName()] == "9223372036854775807"); + } + } + + 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 + 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); + STAmount 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); + STAmount 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); + STAmount 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."); + } + + // Issuer fails trying to send fund after issuance was destroyed + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &bob}); + + // alice destroys issuance + mptAlice.destroy({.ownerCount = 0}); + + // alice tries to send bob fund after issuance is destroy, should + // fail. + mptAlice.pay(alice, bob, 100, tecMPT_ISSUANCE_NOT_FOUND); + } + + // Issuer fails trying to send to some who doesn't own MPT for a + // issuance that was destroyed + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + // alice destroys issuance + mptAlice.destroy({.ownerCount = 0}); + + // alice tries to send bob who doesn't own the MPT after issuance is + // destroyed, it should fail + mptAlice.pay(alice, bob, 100, tecMPT_ISSUANCE_NOT_FOUND); + } + + // Issuers issues maximum amount of MPT to a holder, the holder should + // be able to transfer the max amount to someone else + { + Env env{*this, features}; + Account const alice("alice"); + Account const carol("bob"); + Account const bob("carol"); + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create( + {.maxAmt = "100", .ownerCount = 1, .flags = tfMPTCanTransfer}); + + mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = &carol}); + + mptAlice.pay(alice, bob, 100); + + // transfer max amount to another holder + mptAlice.pay(bob, carol, 100); + } + } + + 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)); + STAmount 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/SetAuth_test.cpp b/src/test/app/SetAuth_test.cpp index adb909314d3..3dd8ab590a4 100644 --- a/src/test/app/SetAuth_test.cpp +++ b/src/test/app/SetAuth_test.cpp @@ -38,8 +38,8 @@ struct SetAuth_test : public beast::unit_test::suite using namespace jtx; Json::Value jv; jv[jss::Account] = account.human(); - jv[jss::LimitAmount] = - STAmount({to_currency(currency), dest}).getJson(JsonOptions::none); + jv[jss::LimitAmount] = STAmount(Issue{to_currency(currency), dest}) + .getJson(JsonOptions::none); jv[jss::TransactionType] = jss::TrustSet; jv[jss::Flags] = tfSetfAuth; return jv; diff --git a/src/test/app/TrustAndBalance_test.cpp b/src/test/app/TrustAndBalance_test.cpp index bf7c8629b69..b438d797276 100644 --- a/src/test/app/TrustAndBalance_test.cpp +++ b/src/test/app/TrustAndBalance_test.cpp @@ -400,7 +400,6 @@ class TrustAndBalance_test : public beast::unit_test::suite carol["USD"].issue(), 6500000000000000ull, -14, - false, true, STAmount::unchecked{}))); env.require(balance(carol, gw["USD"](35))); 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..122e21726f5 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -305,15 +305,19 @@ class IOU return {currency, account.id()}; } - /** Implicit conversion to Issue. + /** Implicit conversion to Issue or Asset. This allows passing an IOU - value where an Issue is expected. + value where an Issue or Asset is expected. */ operator Issue() const { return issue(); } + operator Asset() const + { + return issue(); + } template < class T, @@ -351,6 +355,77 @@ operator<<(std::ostream& os, IOU const& iou); //------------------------------------------------------------------------------ +/** Converts to MPT Issue or STAmount. + + Examples: + MPT Converts to the underlying Issue + MPT(10) Returns STAmount 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 MPTIssue is expected. + */ + operator ripple::MPTIssue() const + { + return mpt(); + } + + template + requires(sizeof(T) >= sizeof(int) && std::is_arithmetic_v) PrettyAmount + operator()(T v) const + { + // VFALCO NOTE Should throw if the + // representation of v is not exact. + return {amountFromString(mpt(), std::to_string(v)), name}; + } + + PrettyAmount operator()(epsilon_t) const; + PrettyAmount operator()(detail::epsilon_multiple) const; + + // VFALCO TODO + // STAmount operator()(char const* s) const; + + /** Returns None-of-Issue */ +#if 0 + None operator()(none_t) const + { + return {Issue{}}; + } +#endif + + friend BookSpec + operator~(MPT const& mpt) + { + assert(false); + Throw("MPT is not supported"); + return BookSpec{beast::zero, noCurrency()}; + } +}; + +std::ostream& +operator<<(std::ostream& os, MPT const& mpt); + +//------------------------------------------------------------------------------ + struct any_t { inline AnyAmount diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp new file mode 100644 index 00000000000..7cd60afed67 --- /dev/null +++ b/src/test/jtx/impl/mpt.cpp @@ -0,0 +1,398 @@ +//------------------------------------------------------------------------------ +/* + 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 { + +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); + if (arg.maxAmt) + jv[sfMaximumAmount.jsonName] = *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 + { + STAmount const saAmount = {*id_, amount}; + STAmount const saActual = + multiply(saAmount, transferRate(*env_.current(), *id_)); + // Sender pays the transfer fee if any + env_.require(mptpay(*this, src, srcAmt - saActual.mpt().value())); + env_.require(mptpay(*this, dest, destAmt + amount)); + // Outstanding amount is reduced by the transfer fee if any + env_.require(mptpay( + *this, issuer_, outstnAmt - (saActual - saAmount).mpt().value())); + } +} + +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))); +} + +PrettyAmount +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/trust.cpp b/src/test/jtx/impl/trust.cpp index 641a0f79f28..320c7d05c7c 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, + STAmount 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/mpt.h b/src/test/jtx/mpt.h new file mode 100644 index 00000000000..e66433260eb --- /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); + + PrettyAmount + 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/trust.h b/src/test/jtx/trust.h index f9fddf4871a..0d02c6e76c4 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, + STAmount const& amount, + std::optional const& mptHolder = std::nullopt); } // namespace jtx } // namespace test diff --git a/src/test/ledger/PaymentSandbox_test.cpp b/src/test/ledger/PaymentSandbox_test.cpp index e3ede19b4b6..dd9b5c5d88b 100644 --- a/src/test/ledger/PaymentSandbox_test.cpp +++ b/src/test/ledger/PaymentSandbox_test.cpp @@ -316,14 +316,12 @@ class PaymentSandbox_test : public beast::unit_test::suite STAmount::cMinValue, STAmount::cMinOffset + 1, false, - false, STAmount::unchecked{}); STAmount hugeAmt( issue, STAmount::cMaxValue, STAmount::cMaxOffset - 1, false, - false, STAmount::unchecked{}); ApplyViewImpl av(&*env.current(), tapNONE); diff --git a/src/test/protocol/Quality_test.cpp b/src/test/protocol/Quality_test.cpp index 741a341d980..64cf0c71b3a 100644 --- a/src/test/protocol/Quality_test.cpp +++ b/src/test/protocol/Quality_test.cpp @@ -29,7 +29,7 @@ class Quality_test : public beast::unit_test::suite // Create a raw, non-integral amount from mantissa and exponent STAmount static raw(std::uint64_t mantissa, int exponent) { - return STAmount({Currency(3), AccountID(3)}, mantissa, exponent); + return STAmount(Issue{Currency(3), AccountID(3)}, mantissa, exponent); } template diff --git a/src/test/protocol/STAmount_test.cpp b/src/test/protocol/STAmount_test.cpp index e48d0500ba6..b512c42a643 100644 --- a/src/test/protocol/STAmount_test.cpp +++ b/src/test/protocol/STAmount_test.cpp @@ -62,7 +62,6 @@ class STAmount_test : public beast::unit_test::suite amount.issue(), mantissa, amount.exponent(), - amount.native(), amount.negative(), STAmount::unchecked{}}; } @@ -82,7 +81,6 @@ class STAmount_test : public beast::unit_test::suite amount.issue(), mantissa, amount.exponent(), - amount.native(), amount.negative(), STAmount::unchecked{}}; } 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/xrpld/app/ledger/detail/LedgerToJson.cpp b/src/xrpld/app/ledger/detail/LedgerToJson.cpp index 9824b31d794..3f6869df1d8 100644 --- a/src/xrpld/app/ledger/detail/LedgerToJson.cpp +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -156,6 +157,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()) @@ -187,6 +194,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 208aab05aa1..7868807c52a 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -2946,6 +2947,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/paths/Credit.cpp b/src/xrpld/app/paths/Credit.cpp index c11f628a11d..b3870937367 100644 --- a/src/xrpld/app/paths/Credit.cpp +++ b/src/xrpld/app/paths/Credit.cpp @@ -31,7 +31,7 @@ creditLimit( AccountID const& issuer, Currency const& currency) { - STAmount result({currency, account}); + STAmount result(Issue{currency, account}); auto sleRippleState = view.read(keylet::line(account, issuer, currency)); @@ -64,7 +64,7 @@ creditBalance( AccountID const& issuer, Currency const& currency) { - STAmount result({currency, account}); + STAmount result(Issue{currency, account}); auto sleRippleState = view.read(keylet::line(account, issuer, currency)); diff --git a/src/xrpld/app/paths/PathRequest.cpp b/src/xrpld/app/paths/PathRequest.cpp index 4cd9f7d71f7..bb6a104bca2 100644 --- a/src/xrpld/app/paths/PathRequest.cpp +++ b/src/xrpld/app/paths/PathRequest.cpp @@ -562,7 +562,7 @@ PathRequest::findPaths( }(); STAmount saMaxAmount = saSendMax.value_or( - STAmount({issue.currency, sourceAccount}, 1u, 0, true)); + STAmount(Issue{issue.currency, sourceAccount}, 1u, 0, true)); JLOG(m_journal.debug()) << iIdentifier << " Paths found, calling rippleCalc"; diff --git a/src/xrpld/app/paths/Pathfinder.cpp b/src/xrpld/app/paths/Pathfinder.cpp index 885a8ae9b47..a5fe2afe949 100644 --- a/src/xrpld/app/paths/Pathfinder.cpp +++ b/src/xrpld/app/paths/Pathfinder.cpp @@ -176,9 +176,10 @@ Pathfinder::Pathfinder( , mSrcCurrency(uSrcCurrency) , mSrcIssuer(uSrcIssuer) , mSrcAmount(srcAmount.value_or(STAmount( - {uSrcCurrency, - uSrcIssuer.value_or( - isXRP(uSrcCurrency) ? xrpAccount() : uSrcAccount)}, + Issue{ + uSrcCurrency, + uSrcIssuer.value_or( + isXRP(uSrcCurrency) ? xrpAccount() : uSrcAccount)}, 1u, 0, true))) diff --git a/src/xrpld/app/tx/detail/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index 15d76526094..c11dfbd8c11 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,6 +45,9 @@ 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]; @@ -51,8 +60,46 @@ 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 = 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.mpt() > 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]; @@ -110,11 +157,59 @@ Clawback::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +template <> TER -Clawback::doApply() +preclaimHelper(PreclaimContext const& ctx) +{ + AccountID const issuer = ctx.tx[sfAccount]; + auto const clawAmount = 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.get().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.get(), + 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 = ctx.tx[sfAmount]; AccountID const holder = clawAmount.getIssuer(); // cannot be reference // Replace the `issuer` field with issuer's account @@ -124,20 +219,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 = ctx.tx[sfAmount]; + AccountID const holder = ctx.tx[sfMPTokenHolder]; + + // Get the spendable balance. Must use `accountHolds`. + STAmount const spendableAmount = accountHolds( + ctx.view(), + holder, + clawAmount.get(), + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + ctx.journal); + + return rippleMPTCredit( + 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].asset().value()); +} + +TER +Clawback::preclaim(PreclaimContext const& ctx) +{ + return std::visit( + [&](T const&) { return preclaimHelper(ctx); }, + ctx.tx[sfAmount].asset().value()); +} + +TER +Clawback::doApply() +{ + return std::visit( + [&](T const&) { return applyHelper(ctx_); }, + ctx_.tx[sfAmount].asset().value()); } } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index f855ad8578c..625f8c7b284 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -478,6 +478,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 +884,9 @@ ValidClawback::visitEntry( { if (before && before->getType() == ltRIPPLE_STATE) trustlinesChanged++; + + if (before && before->getType() == ltMPTOKEN) + mptokensChanged++; } bool @@ -904,18 +909,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 = 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 +940,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/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 309e9d4a498..6f68789357e 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 @@ -43,8 +44,13 @@ Payment::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)}; } +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; @@ -78,7 +84,7 @@ Payment::preflight(PreflightContext const& ctx) maxSourceAmount = saDstAmount; else maxSourceAmount = STAmount( - {saDstAmount.getCurrency(), account}, + Issue{saDstAmount.getCurrency(), account}, saDstAmount.mantissa(), saDstAmount.exponent(), saDstAmount < beast::zero); @@ -203,8 +209,83 @@ 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; + } + + STAmount const saDstAmount(tx.getFieldAmount(sfAmount)); + + auto const account = tx.getAccountID(sfAccount); + + auto const& uDstMptID = saDstAmount.get().getMptID(); + + 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 (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(uDstMptID); + 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(); @@ -275,9 +356,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 +368,69 @@ Payment::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +template <> TER -Payment::doApply() +preclaimHelper(PreclaimContext const& ctx, std::size_t, std::size_t) +{ + 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) { - auto const deliverMin = ctx_.tx[~sfDeliverMin]; + 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(ctx.tx.getFieldAmount(sfAmount)); STAmount maxSourceAmount; if (sendMax) maxSourceAmount = *sendMax; @@ -309,44 +438,47 @@ Payment::doApply() maxSourceAmount = saDstAmount; else maxSourceAmount = STAmount( - {saDstAmount.getCurrency(), account_}, + Issue{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 +498,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 +510,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 +539,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 +557,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 +566,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 +612,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 +628,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 +639,78 @@ 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(ctx.tx.getFieldAmount(sfAmount)); + + JLOG(ctx.journal.trace()) << " saDstAmount=" << saDstAmount.getFullText(); + + if (auto const ter = + requireAuth(ctx.view(), saDstAmount.get(), account); + ter != tesSUCCESS) + return ter; + + if (auto const ter = + requireAuth(ctx.view(), saDstAmount.get(), uDstAccountID); + ter != tesSUCCESS) + return ter; + + if (auto const ter = canTransfer( + ctx.view(), saDstAmount.get(), account, uDstAccountID); + ter != tesSUCCESS) + return ter; + + auto const& mpt = saDstAmount.get(); + 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 = + accountSendMPT(pv, account, uDstAccountID, saDstAmount, ctx.journal); + pv.apply(ctx.rawView()); + return res; +} + +NotTEC +Payment::preflight(PreflightContext const& ctx) +{ + return std::visit( + [&](TDelIss const&) { + return preflightHelper(ctx); + }, + ctx.tx[sfAmount].asset().value()); +} + +TER +Payment::preclaim(PreclaimContext const& ctx) +{ + return std::visit( + [&](TDelIss const&) { + return preclaimHelper(ctx, MaxPathSize, MaxPathLength); + }, + ctx.tx[sfAmount].asset().value()); +} + +TER +Payment::doApply() +{ + return std::visit( + [&](TDelIss const&) { + return applyHelper(ctx_, mPriorBalance, mSourceBalance); + }, + ctx_.tx[sfAmount].asset().value()); +} + } // 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/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..da6d2caabc8 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -26,6 +26,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]] STAmount +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 */ @@ -415,16 +443,33 @@ rippleCredit( ApplyView& view, AccountID const& uSenderID, AccountID const& uReceiverID, - const STAmount& saAmount, + STAmount const& saAmount, bool bCheckIssuer, beast::Journal j); +[[nodiscard]] TER +rippleMPTCredit( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STAmount const& saAmount, + beast::Journal j); + [[nodiscard]] TER accountSend( ApplyView& view, AccountID const& from, AccountID const& to, - const STAmount& saAmount, + STAmount const& saAmount, + beast::Journal j, + WaiveTransferFee waiveFee = WaiveTransferFee::No); + +[[nodiscard]] TER +accountSendMPT( + ApplyView& view, + AccountID const& from, + AccountID const& to, + STAmount const& saAmount, beast::Journal j, WaiveTransferFee waiveFee = WaiveTransferFee::No); @@ -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..67f442a9349 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, @@ -241,13 +268,13 @@ accountHolds( auto const sle = view.read(keylet::line(account, issuer, currency)); if (!sle) { - amount.clear({currency, issuer}); + amount.clear(Issue{currency, issuer}); } else if ( (zeroIfFrozen == fhZERO_IF_FROZEN) && isFrozen(view, account, currency, issuer)) { - amount.clear(Issue(currency, issuer)); + amount.clear(Issue{currency, issuer}); } else { @@ -278,6 +305,47 @@ accountHolds( view, account, issue.currency, issue.account, zeroIfFrozen, j); } +STAmount +accountHolds( + ReadView const& view, + AccountID const& account, + MPTIssue const& issue, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j) +{ + STAmount 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 = STAmount{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, @@ -820,9 +900,8 @@ trustCreate( bSetHigh ? sfHighLimit : sfLowLimit, saLimit); sleRippleState->setFieldAmount( bSetHigh ? sfLowLimit : sfHighLimit, - STAmount( - {saBalance.getCurrency(), - bSetDst ? uSrcAccountID : uDstAccountID})); + STAmount(Issue{ + saBalance.getCurrency(), bSetDst ? uSrcAccountID : uDstAccountID})); if (uQualityIn) sleRippleState->setFieldU32( @@ -1055,7 +1134,7 @@ rippleCredit( return tesSUCCESS; } - STAmount const saReceiverLimit({currency, uReceiverID}); + STAmount const saReceiverLimit(Issue{currency, uReceiverID}); STAmount saBalance{saAmount}; saBalance.setIssuer(noAccount()); @@ -1156,7 +1235,7 @@ accountSend( } else { - assert(saAmount >= beast::zero); + assert(saAmount >= beast::zero && !saAmount.holds()); } /* If we aren't sending anything or if the sender is the same as the @@ -1256,6 +1335,96 @@ accountSend( return terResult; } +static TER +rippleSendMPT( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STAmount const& saAmount, + STAmount& saActual, + beast::Journal j, + WaiveTransferFee waiveFee) +{ + assert(uSenderID != uReceiverID); + + // Safe to get MPT since rippleSendMPT is only called by accountSendMPT + auto const issuer = saAmount.getIssuer(); + + if (uSenderID == issuer || uReceiverID == issuer || issuer == noAccount()) + { + // if sender is issuer, check that the new OutstandingAmount will not + // exceed MaximumAmount + if (uSenderID == issuer) + { + auto const mptID = + keylet::mptIssuance(saAmount.get().getMptID()); + auto const sle = view.peek(mptID); + if (!sle) + return tecMPT_ISSUANCE_NOT_FOUND; + + if (sle->getFieldU64(sfOutstandingAmount) + saAmount.mpt().value() > + (*sle)[~sfMaximumAmount].value_or(maxMPTokenAmount)) + return tecMPT_MAX_AMOUNT_EXCEEDED; + } + + // Direct send: redeeming IOUs and/or sending own IOUs. + auto const ter = + rippleMPTCredit(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.get().getMptID()))) + { + saActual = (waiveFee == WaiveTransferFee::Yes) + ? saAmount + : multiply( + saAmount, + transferRate(view, saAmount.get().getMptID())); + + JLOG(j.debug()) << "rippleSend> " << to_string(uSenderID) << " - > " + << to_string(uReceiverID) + << " : deliver=" << saAmount.getFullText() + << " cost=" << saActual.getFullText(); + + if (auto const terResult = + rippleMPTCredit(view, issuer, uReceiverID, saAmount, j); + terResult != tesSUCCESS) + return terResult; + + return rippleMPTCredit(view, uSenderID, issuer, saActual, j); + } + + return tecINTERNAL; +} + +TER +accountSendMPT( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STAmount const& saAmount, + beast::Journal j, + WaiveTransferFee waiveFee) +{ + assert(saAmount >= beast::zero && saAmount.holds()); + + /* 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()}; + + return rippleSendMPT( + view, uSenderID, uReceiverID, saAmount, saActual, j, waiveFee); +} + static bool updateTrustLine( ApplyView& view, @@ -1377,7 +1546,7 @@ issueIOU( // NIKB TODO: The limit uses the receiver's account as the issuer and // this is unnecessarily inefficient as copying which could be avoided // is now required. Consider available options. - STAmount const limit({issue.currency, account}); + STAmount const limit(Issue{issue.currency, account}); STAmount final_balance = amount; final_balance.setIssuer(noAccount()); @@ -1537,6 +1706,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 +1864,84 @@ deleteAMMTrustLine( return tesSUCCESS; } +TER +rippleMPTCredit( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STAmount const& saAmount, + beast::Journal j) +{ + auto const mptID = keylet::mptIssuance(saAmount.get().getMptID()); + auto const issuer = saAmount.getIssuer(); + if (!view.exists(mptID)) + return tecMPT_ISSUANCE_NOT_FOUND; + if (uSenderID == issuer) + { + if (auto sle = view.peek(mptID)) + { + sle->setFieldU64( + sfOutstandingAmount, + sle->getFieldU64(sfOutstandingAmount) + saAmount.mpt().value()); + + 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.mpt().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.mpt().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.mpt().value()); + view.update(sle); + } + else + return tecNO_AUTH; + } + return tesSUCCESS; +} + } // namespace ripple diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h b/src/xrpld/rpc/MPTokenIssuanceID.h new file mode 100644 index 00000000000..f7f45fded3b --- /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/MPTokenIssuanceID.cpp b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp new file mode 100644 index 00000000000..8de1e8f3344 --- /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 fa66fecfbba..87ec9ff03f4 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) @@ -915,7 +921,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}, @@ -939,7 +945,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 65ee50c0891..401f808f56a 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -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.holds())) return RPC::make_error( rpcINVALID_PARAMS, "Field 'build_path' not allowed in this context."); 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 a85abd86682..887694daf21 100644 --- a/src/xrpld/rpc/handlers/AccountTx.cpp +++ b/src/xrpld/rpc/handlers/AccountTx.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -349,6 +350,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..30b1113cd05 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()) + { + uint192 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(); + + uint192 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 ba103d186fc..98af3a809bf 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 @@ -265,6 +266,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; From f84e9ea2959eee97213b5ca45e4bc0eef6ebc84a Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Tue, 1 Oct 2024 13:25:16 -0400 Subject: [PATCH 02/58] Apply suggestions from code review Co-authored-by: Ed Hennis --- include/xrpl/basics/MPTAmount.h | 4 ++-- include/xrpl/protocol/MPTIssue.h | 2 +- include/xrpl/protocol/SOTemplate.h | 4 ++-- src/libxrpl/protocol/STAmount.cpp | 18 +++++++----------- src/test/jtx/impl/mpt.cpp | 2 +- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/include/xrpl/basics/MPTAmount.h b/include/xrpl/basics/MPTAmount.h index 5f8f3dbbe76..722b6e72e73 100644 --- a/include/xrpl/basics/MPTAmount.h +++ b/include/xrpl/basics/MPTAmount.h @@ -17,8 +17,8 @@ */ //============================================================================== -#ifndef RIPPLE_BASICS_INTEGRALAMOUNT_H_INCLUDED -#define RIPPLE_BASICS_INTEGRALAMOUNT_H_INCLUDED +#ifndef RIPPLE_BASICS_MPTAMOUNT_H_INCLUDED +#define RIPPLE_BASICS_MPTAMOUNT_H_INCLUDED #include #include diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index 5892e2b91c8..af85a21c121 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -57,7 +57,7 @@ operator==(MPTIssue const& lhs, MPTIssue const& rhs) constexpr bool operator!=(MPTIssue const& lhs, MPTIssue const& rhs) { - return !(lhs.mptID_ == rhs.mptID_); + return !(lhs == rhs); } inline bool diff --git a/include/xrpl/protocol/SOTemplate.h b/include/xrpl/protocol/SOTemplate.h index 51479353a95..95cd35fead2 100644 --- a/include/xrpl/protocol/SOTemplate.h +++ b/include/xrpl/protocol/SOTemplate.h @@ -50,7 +50,7 @@ class SOElement // Use std::reference_wrapper so SOElement can be stored in a std::vector. std::reference_wrapper sField_; SOEStyle style_; - SOETxMPTAmount supportMpt_; + SOETxMPTAmount supportMpt_ = soeMPTNone; private: void @@ -68,7 +68,7 @@ class SOElement public: SOElement(SField const& fieldName, SOEStyle style) - : sField_(fieldName), style_(style), supportMpt_(soeMPTNone) + : sField_(fieldName), style_(style) { init(fieldName); } diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 735e3853c9d..8ef983c9a0c 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -663,7 +663,7 @@ STAmount::isDefault() const void STAmount::canonicalize() { - if (isXRP(*this) || mAsset.holds()) + if (native() || mAsset.holds()) { // log(2^64,10) ~ 19.2 if (mValue == 0 || mOffset <= -20) @@ -686,18 +686,14 @@ STAmount::canonicalize() { Number num( mIsNegative ? -mValue : mValue, mOffset, Number::unchecked{}); + auto set = [&](auto const& val) { + mIsNegative = val.value() < 0; + mValue = mIsNegative ? -val.value() : val.value(); + }; if (native()) - { - XRPAmount xrp{num}; - mIsNegative = xrp.drops() < 0; - mValue = mIsNegative ? -xrp.drops() : xrp.drops(); - } + set(XRPAmount{num}); else - { - MPTAmount c{num}; - mIsNegative = c.value() < 0; - mValue = mIsNegative ? -c.value() : c.value(); - } + set(MPTAmount{num}); mOffset = 0; } else diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 7cd60afed67..8b1606bbbdb 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -49,7 +49,7 @@ MPTTester::makeHolders(std::vector const& holders) std::unordered_map accounts; for (auto const& h : holders) { - assert(h && holders_.find(h->human()) == accounts.cend()); + assert(h && accounts.find(h->human()) == accounts.cend()); accounts.emplace(h->human(), h); } return accounts; From 18515dd8e6c99b29a7a405026e02c42f86e07997 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Tue, 1 Oct 2024 14:33:10 -0400 Subject: [PATCH 03/58] Address reviewer's feedback --- include/xrpl/basics/MPTAmount.h | 22 +--------- include/xrpl/basics/XRPAmount.h | 4 ++ include/xrpl/protocol/Asset.h | 10 ++++- include/xrpl/protocol/Indexes.h | 12 ++--- include/xrpl/protocol/Issue.h | 9 +++- include/xrpl/protocol/MPTIssue.h | 12 +++-- include/xrpl/protocol/STAmount.h | 11 ++--- include/xrpl/protocol/UintTypes.h | 4 +- src/libxrpl/basics/MPTAmount.cpp | 17 -------- src/libxrpl/protocol/Asset.cpp | 4 +- src/libxrpl/protocol/Indexes.cpp | 9 ++-- src/libxrpl/protocol/Issue.cpp | 10 ----- src/libxrpl/protocol/MPTIssue.cpp | 6 +-- src/libxrpl/protocol/STAmount.cpp | 10 +++-- src/libxrpl/protocol/STTx.cpp | 11 ++--- src/libxrpl/protocol/TER.cpp | 2 +- src/test/app/MPToken_test.cpp | 28 ++++++------ src/test/jtx/amount.h | 10 ++--- src/test/jtx/impl/mpt.cpp | 2 +- src/xrpld/ledger/View.h | 17 +++++--- src/xrpld/ledger/detail/View.cpp | 51 +++++++++++++--------- src/xrpld/rpc/detail/MPTokenIssuanceID.cpp | 2 +- src/xrpld/rpc/detail/RPCHelpers.cpp | 2 +- src/xrpld/rpc/handlers/LedgerData.cpp | 2 +- 24 files changed, 129 insertions(+), 138 deletions(-) diff --git a/include/xrpl/basics/MPTAmount.h b/include/xrpl/basics/MPTAmount.h index 722b6e72e73..06487194a47 100644 --- a/include/xrpl/basics/MPTAmount.h +++ b/include/xrpl/basics/MPTAmount.h @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 Ripple Labs Inc. + 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 @@ -81,9 +81,6 @@ class MPTAmount : private boost::totally_ordered, 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. @@ -131,21 +128,6 @@ 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) { @@ -182,4 +164,4 @@ mulRatio( } // namespace ripple -#endif // RIPPLE_BASICS_INTEGRALAMOUNT_H_INCLUDED +#endif // RIPPLE_BASICS_MPTAMOUNT_H_INCLUDED diff --git a/include/xrpl/basics/XRPAmount.h b/include/xrpl/basics/XRPAmount.h index 1d3b32f169b..e0c381b562e 100644 --- a/include/xrpl/basics/XRPAmount.h +++ b/include/xrpl/basics/XRPAmount.h @@ -205,6 +205,10 @@ class XRPAmount : private boost::totally_ordered, return dropsAs().value_or(defaultValue.drops()); } + /* Clips a 64-bit value to a 32-bit JSON number. It is only used + * in contexts that don't expect the value to ever approach + * the 32-bit limits (i.e. fees and reserves). + */ Json::Value jsonClipped() const { diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index 760b2439557..c42fdac3203 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -30,6 +30,12 @@ template concept ValidIssueType = std::is_same_v || std::is_same_v; +/* Asset is a variant of Issue (XRP and IOU) and MPTIssue (MPT). + * It enables handling of different issues when either one is expected. + * For instance, it extends STAmount class to support either issue + * in a general way. It handles specifics such arithmetics and serialization + * depending on specific issue type held by Asset. + */ class Asset { private: @@ -41,9 +47,9 @@ class Asset Asset(Issue const& issue); - Asset(MPTIssue const& mpt); + Asset(MPTIssue const& mptIssue); - Asset(MPTID const& mpt); + Asset(MPTID const& issuanceID); explicit operator Issue() const; diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index e9f4100008e..63fa34ada37 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -291,21 +291,21 @@ Keylet mptIssuance(AccountID const& issuer, std::uint32_t seq) noexcept; Keylet -mptIssuance(MPTID const& mpt) noexcept; +mptIssuance(MPTID const& issuanceID) noexcept; inline Keylet -mptIssuance(uint256 const& issuance) +mptIssuance(uint256 const& issuanceKey) { - return {ltMPTOKEN_ISSUANCE, issuance}; + return {ltMPTOKEN_ISSUANCE, issuanceKey}; } Keylet mptoken(MPTID const& issuanceID, AccountID const& holder) noexcept; inline Keylet -mptoken(uint256 const& mptokenKey) +mptoken(uint256 const& issuanceKey) { - return {ltMPTOKEN, mptokenKey}; + return {ltMPTOKEN, issuanceKey}; } Keylet @@ -352,7 +352,7 @@ std::array, 6> const directAccountKeylets{ {&keylet::did, jss::DID, true}}}; MPTID -getMptID(AccountID const& account, std::uint32_t sequence); +makeMptID(AccountID const& account, std::uint32_t sequence); } // namespace ripple diff --git a/include/xrpl/protocol/Issue.h b/include/xrpl/protocol/Issue.h index 0c046378ea0..c533ff678ec 100644 --- a/include/xrpl/protocol/Issue.h +++ b/include/xrpl/protocol/Issue.h @@ -40,10 +40,15 @@ class Issue Issue() = default; - Issue(Currency const& c, AccountID const& a); + Issue(Currency const& c, AccountID const& a) : currency(c), account(a) + { + } AccountID const& - getIssuer() const; + getIssuer() const + { + return account; + } std::string getText() const; diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index af85a21c121..bac648ae2ab 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -25,6 +25,12 @@ namespace ripple { +/* MPTIssue represents a Multi Purpose Token (MPT) and enables handling of + * either Issue or MPTIssue tokens by Asset and STAmount. MPT is identified + * by MPTID, which is a 192-bit concatenation of a 32-bit account sequence + * number at the time of MPT creation and a 160-bit account id. + * The sequence number is stored in big endian order. + */ class MPTIssue { private: @@ -33,7 +39,7 @@ class MPTIssue public: MPTIssue() = default; - MPTIssue(MPTID const& id); + explicit MPTIssue(MPTID const& issuanceID); AccountID const& getIssuer() const; @@ -67,10 +73,10 @@ isXRP(MPTID const&) } Json::Value -to_json(MPTIssue const& issue); +to_json(MPTIssue const& mptIssue); std::string -to_string(MPTIssue const& mpt); +to_string(MPTIssue const& mptIssue); } // namespace ripple diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 2f037a1c285..449cd952775 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -35,8 +35,9 @@ namespace ripple { template -concept AssetType = std::is_same_v || - std::is_convertible_v || std::is_convertible_v; +concept AssetType = + std::is_same_v || std::is_convertible_v || + std::is_convertible_v || std::is_convertible_v; // Internal form: // 1: If amount is zero, then value is zero and offset is -100 @@ -158,7 +159,7 @@ class STAmount final : public STBase, public CountedObject // Legacy support for new-style amounts STAmount(IOUAmount const& amount, Issue const& issue); STAmount(XRPAmount const& amount); - STAmount(MPTAmount const& amount, MPTIssue const& issue); + STAmount(MPTAmount const& amount, MPTIssue const& mptIssue); operator Number() const; //-------------------------------------------------------------------------- @@ -392,8 +393,8 @@ inline STAmount::STAmount(IOUAmount const& amount, Issue const& issue) canonicalize(); } -inline STAmount::STAmount(MPTAmount const& amount, MPTIssue const& issue) - : mAsset(issue), mOffset(0), mIsNegative(amount < beast::zero) +inline STAmount::STAmount(MPTAmount const& amount, MPTIssue const& mptIssue) + : mAsset(mptIssue), mOffset(0), mIsNegative(amount < beast::zero) { if (mIsNegative) mValue = unsafe_cast(-amount.value()); diff --git a/include/xrpl/protocol/UintTypes.h b/include/xrpl/protocol/UintTypes.h index 74f3917faaf..a8fefab91ce 100644 --- a/include/xrpl/protocol/UintTypes.h +++ b/include/xrpl/protocol/UintTypes.h @@ -58,7 +58,9 @@ 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. */ +/** MPTID is a 192-bit representing MPT Issuance ID, + * which is a concatenation of a 32-bit sequence (big endian) + * and a 160-bit account */ using MPTID = base_uint<192>; /** XRP currency. */ diff --git a/src/libxrpl/basics/MPTAmount.cpp b/src/libxrpl/basics/MPTAmount.cpp index 6c9a50e4730..0481da67711 100644 --- a/src/libxrpl/basics/MPTAmount.cpp +++ b/src/libxrpl/basics/MPTAmount.cpp @@ -59,23 +59,6 @@ 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() { diff --git a/src/libxrpl/protocol/Asset.cpp b/src/libxrpl/protocol/Asset.cpp index d458644d948..97725e52f4c 100644 --- a/src/libxrpl/protocol/Asset.cpp +++ b/src/libxrpl/protocol/Asset.cpp @@ -27,11 +27,11 @@ Asset::Asset(Issue const& issue) : issue_(issue) { } -Asset::Asset(MPTIssue const& mpt) : issue_(mpt) +Asset::Asset(MPTIssue const& mptIssue) : issue_(mptIssue) { } -Asset::Asset(MPTID const& mpt) : issue_(MPTIssue{mpt}) +Asset::Asset(MPTID const& issuanceID) : issue_(MPTIssue{issuanceID}) { } diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 8014d8d4dcd..a1f79752b38 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -138,7 +138,7 @@ getTicketIndex(AccountID const& account, SeqProxy ticketSeq) } MPTID -getMptID(AccountID const& account, std::uint32_t sequence) +makeMptID(AccountID const& account, std::uint32_t sequence) { MPTID u; sequence = boost::endian::native_to_big(sequence); @@ -466,14 +466,15 @@ oracle(AccountID const& account, std::uint32_t const& documentID) noexcept Keylet mptIssuance(AccountID const& issuer, std::uint32_t seq) noexcept { - return mptIssuance(getMptID(issuer, seq)); + return mptIssuance(makeMptID(issuer, seq)); } Keylet -mptIssuance(MPTID const& id) noexcept +mptIssuance(MPTID const& issuanceID) noexcept { return { - ltMPTOKEN_ISSUANCE, indexHash(LedgerNameSpace::MPTOKEN_ISSUANCE, id)}; + ltMPTOKEN_ISSUANCE, + indexHash(LedgerNameSpace::MPTOKEN_ISSUANCE, issuanceID)}; } Keylet diff --git a/src/libxrpl/protocol/Issue.cpp b/src/libxrpl/protocol/Issue.cpp index 8aff535fde7..70d2c013d7b 100644 --- a/src/libxrpl/protocol/Issue.cpp +++ b/src/libxrpl/protocol/Issue.cpp @@ -26,16 +26,6 @@ namespace ripple { -Issue::Issue(Currency const& c, AccountID const& a) : currency(c), account(a) -{ -} - -AccountID const& -Issue::getIssuer() const -{ - return account; -} - std::string Issue::getText() const { diff --git a/src/libxrpl/protocol/MPTIssue.cpp b/src/libxrpl/protocol/MPTIssue.cpp index f46e6d9b2d8..c4a4947e0ca 100644 --- a/src/libxrpl/protocol/MPTIssue.cpp +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -23,7 +23,7 @@ namespace ripple { -MPTIssue::MPTIssue(MPTID const& id) : mptID_(id) +MPTIssue::MPTIssue(MPTID const& issuanceID) : mptID_(issuanceID) { } @@ -44,10 +44,10 @@ MPTIssue::getMptID() const } Json::Value -to_json(MPTIssue const& issue) +to_json(MPTIssue const& mptIssue) { Json::Value jv; - jv[jss::mpt_issuance_id] = to_string(issue.getMptID()); + jv[jss::mpt_issuance_id] = to_string(mptIssue.getMptID()); return jv; } diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 8ef983c9a0c..8dd08463664 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -665,6 +665,7 @@ STAmount::canonicalize() { if (native() || mAsset.holds()) { + // native and MPT currency amounts should always have an offset of zero // log(2^64,10) ~ 19.2 if (mValue == 0 || mOffset <= -20) { @@ -677,9 +678,12 @@ STAmount::canonicalize() if (getSTAmountCanonicalizeSwitchover()) { // log(cMaxNativeN, 10) == 17 - if (mOffset > 17) + if (native() && mOffset > 17) Throw( "Native currency amount out of range"); + // log(maxMPTokenAmount, 10) ~ 18.96 + if (mAsset.holds() && mOffset > 18) + Throw("MPT amount out of range"); } if (getSTNumberSwitchover() && getSTAmountCanonicalizeSwitchover()) @@ -710,7 +714,7 @@ STAmount::canonicalize() { // N.B. do not move the overflow check to after the // multiplication - if (isXRP(*this)) + if (native()) { if (mValue > cMaxNativeN) Throw( @@ -724,7 +728,7 @@ STAmount::canonicalize() } } - if (isXRP(*this)) + if (native()) { if (mValue > cMaxNativeN) Throw( diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index cb0d0d1495e..cbb49e26b49 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -143,14 +143,9 @@ STTx::getMentionedAccounts() const } else if (auto samt = dynamic_cast(&it)) { - if (samt->holds()) - { - auto const& issuer = samt->getIssuer(); - if (!isXRP(issuer)) - list.insert(issuer); - } - else - list.insert(samt->getIssuer()); + auto const& issuer = samt->getIssuer(); + if (!isXRP(issuer)) + list.insert(issuer); } } diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 1803d836625..45da1f30e56 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -116,7 +116,7 @@ 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(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(tecMPT_ISSUANCE_NOT_FOUND, "The MPTokenIssuance object is not found"), diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 6ed9c66fb9b..75ab42a96c3 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -152,7 +152,7 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features - featureMPTokensV1}; MPTTester mptAlice(env, alice); - auto const id = getMptID(alice, env.seq(alice)); + auto const id = makeMptID(alice, env.seq(alice)); mptAlice.destroy({.id = id, .ownerCount = 0, .err = temDISABLED}); env.enableFeature(featureMPTokensV1); @@ -167,7 +167,7 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {&bob}}); mptAlice.destroy( - {.id = getMptID(alice.id(), env.seq(alice)), + {.id = makeMptID(alice.id(), env.seq(alice)), .ownerCount = 0, .err = tecOBJECT_NOT_FOUND}); @@ -224,7 +224,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.authorize( {.account = &bob, - .id = getMptID(alice, env.seq(alice)), + .id = makeMptID(alice, env.seq(alice)), .err = temDISABLED}); } @@ -249,7 +249,7 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {&bob}}); - auto const id = getMptID(alice, env.seq(alice)); + auto const id = makeMptID(alice, env.seq(alice)); mptAlice.authorize( {.holder = &bob, .id = id, .err = tecOBJECT_NOT_FOUND}); @@ -474,7 +474,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.set( {.account = &bob, - .id = getMptID(alice, env.seq(alice)), + .id = makeMptID(alice, env.seq(alice)), .err = temDISABLED}); env.enableFeature(featureMPTokensV1); @@ -551,7 +551,7 @@ class MPToken_test : public beast::unit_test::suite // alice trying to set when the mptissuance doesn't exist yet mptAlice.set( - {.id = getMptID(alice.id(), env.seq(alice)), + {.id = makeMptID(alice.id(), env.seq(alice)), .flags = tfMPTLock, .err = tecOBJECT_NOT_FOUND}); @@ -845,7 +845,7 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; env.fund(XRP(1'000), alice, bob); - STAmount mpt{MPTIssue{getMptID(alice.id(), 1)}, UINT64_C(100)}; + STAmount mpt{MPTIssue{makeMptID(alice.id(), 1)}, UINT64_C(100)}; Json::Value jv; jv[jss::secret] = alice.name(); jv[jss::tx_json] = pay(alice, bob, mpt); @@ -924,7 +924,7 @@ class MPToken_test : public beast::unit_test::suite env.fund(XRP(1'000), alice); env.fund(XRP(1'000), bob); - STAmount mpt{MPTIssue{getMptID(alice.id(), 1)}, UINT64_C(100)}; + STAmount mpt{MPTIssue{makeMptID(alice.id(), 1)}, UINT64_C(100)}; env(pay(alice, bob, mpt), ter(temDISABLED)); } @@ -938,7 +938,7 @@ class MPToken_test : public beast::unit_test::suite env.fund(XRP(1'000), alice); env.fund(XRP(1'000), carol); - STAmount mpt{MPTIssue{getMptID(alice.id(), 1)}, UINT64_C(100)}; + STAmount mpt{MPTIssue{makeMptID(alice.id(), 1)}, UINT64_C(100)}; Json::Value jv; jv[jss::secret] = alice.name(); @@ -1082,7 +1082,7 @@ class MPToken_test : public beast::unit_test::suite Account const alice("alice"); auto const USD = alice["USD"]; Account const carol("carol"); - MPTIssue issue(getMptID(alice.id(), 1)); + MPTIssue issue(makeMptID(alice.id(), 1)); STAmount mpt{issue, UINT64_C(100)}; auto const jvb = bridge(alice, USD, alice, USD); for (auto const& feature : {features, features - featureMPTokensV1}) @@ -1406,7 +1406,7 @@ class MPToken_test : public beast::unit_test::suite auto const USD = alice["USD"]; auto const mpt = ripple::test::jtx::MPT( - alice.name(), getMptID(alice.id(), env.seq(alice))); + alice.name(), makeMptID(alice.id(), env.seq(alice))); env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED)); env.close(); @@ -1429,7 +1429,7 @@ class MPToken_test : public beast::unit_test::suite auto const USD = alice["USD"]; auto const mpt = ripple::test::jtx::MPT( - alice.name(), getMptID(alice.id(), env.seq(alice))); + alice.name(), makeMptID(alice.id(), env.seq(alice))); // clawing back IOU from a MPT holder fails env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED)); @@ -1488,7 +1488,7 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {&bob}}); auto const fakeMpt = ripple::test::jtx::MPT( - alice.name(), getMptID(alice.id(), env.seq(alice))); + alice.name(), makeMptID(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)); @@ -1527,7 +1527,7 @@ class MPToken_test : public beast::unit_test::suite env.close(); auto const mpt = ripple::test::jtx::MPT( - alice.name(), getMptID(alice.id(), env.seq(alice))); + alice.name(), makeMptID(alice.id(), env.seq(alice))); Json::Value jv = claw(alice, mpt(1), bob); jv[jss::Amount][jss::value] = to_string(maxMPTokenAmount + 1); diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index 122e21726f5..c73ac2bcf09 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -366,17 +366,17 @@ class MPT { public: std::string name; - ripple::MPTID mptID; + ripple::MPTID issuanceID; - MPT(std::string const& n, ripple::MPTID const& mptID_) - : name(n), mptID(mptID_) + MPT(std::string const& n, ripple::MPTID const& issuanceID_) + : name(n), issuanceID(issuanceID_) { } ripple::MPTID const& mpt() const { - return mptID; + return issuanceID; } /** Implicit conversion to MPTIssue. @@ -386,7 +386,7 @@ class MPT */ operator ripple::MPTIssue() const { - return mpt(); + return MPTIssue{issuanceID}; } template diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 8b1606bbbdb..3bd69850054 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -85,7 +85,7 @@ MPTTester::create(const MPTCreate& arg) { if (issuanceKey_) Throw("MPT can't be reused"); - id_ = getMptID(issuer_.id(), env_.seq(issuer_)); + id_ = makeMptID(issuer_.id(), env_.seq(issuer_)); issuanceKey_ = keylet::mptIssuance(*id_).key; Json::Value jv; jv[sfAccount.jsonName] = issuer_.human(); diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index da6d2caabc8..472276d8857 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -86,7 +86,7 @@ enum AuthHandling { ahIGNORE_AUTH, ahZERO_IF_UNAUTHORIZED }; isGlobalFrozen(ReadView const& view, AccountID const& issuer); [[nodiscard]] bool -isGlobalFrozen(ReadView const& view, MPTIssue const& mpt); +isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue); [[nodiscard]] bool isIndividualFrozen( @@ -108,7 +108,7 @@ isIndividualFrozen( isIndividualFrozen( ReadView const& view, AccountID const& account, - MPTIssue const& mpt); + MPTIssue const& mptIssue); [[nodiscard]] bool isFrozen( @@ -124,7 +124,10 @@ isFrozen(ReadView const& view, AccountID const& account, Issue const& issue) } [[nodiscard]] bool -isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mpt); +isFrozen( + ReadView const& view, + AccountID const& account, + MPTIssue const& mptIssue); // Returns the amount an account can spend without going into debt. // @@ -150,7 +153,7 @@ accountHolds( accountHolds( ReadView const& view, AccountID const& account, - MPTIssue const& issue, + MPTIssue const& mptIssue, FreezeHandling zeroIfFrozen, AuthHandling zeroIfUnauthorized, beast::Journal j); @@ -235,7 +238,7 @@ forEachItemAfter( transferRate(ReadView const& view, AccountID const& issuer); [[nodiscard]] Rate -transferRate(ReadView const& view, MPTID const& id); +transferRate(ReadView const& view, MPTID const& issuanceID); /** Returns `true` if the directory is empty @param key The key of the directory @@ -506,7 +509,7 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account); [[nodiscard]] TER requireAuth( ReadView const& view, - MPTIssue const& mpt, + MPTIssue const& mptIssue, AccountID const& account); /** Check if the destination account is allowed @@ -516,7 +519,7 @@ requireAuth( [[nodiscard]] TER canTransfer( ReadView const& view, - MPTIssue const& mpt, + MPTIssue const& mptIssue, AccountID const& from, AccountID const& to); diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 67f442a9349..7710be2a12a 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -178,9 +178,9 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer) } bool -isGlobalFrozen(ReadView const& view, MPTIssue const& mpt) +isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue) { - if (auto const sle = view.read(keylet::mptIssuance(mpt.getMptID()))) + if (auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID()))) return sle->getFlags() & lsfMPTLocked; return false; } @@ -209,9 +209,10 @@ bool isIndividualFrozen( ReadView const& view, AccountID const& account, - MPTIssue const& mpt) + MPTIssue const& mptIssue) { - if (auto const sle = view.read(keylet::mptoken(mpt.getMptID(), account))) + if (auto const sle = + view.read(keylet::mptoken(mptIssue.getMptID(), account))) return sle->getFlags() & lsfMPTLocked; return false; } @@ -242,11 +243,14 @@ isFrozen( } bool -isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mpt) +isFrozen( + ReadView const& view, + AccountID const& account, + MPTIssue const& mptIssue) { - if (isGlobalFrozen(view, mpt)) + if (isGlobalFrozen(view, mptIssue)) return true; - return isIndividualFrozen(view, account, mpt); + return isIndividualFrozen(view, account, mptIssue); } STAmount @@ -309,37 +313,39 @@ STAmount accountHolds( ReadView const& view, AccountID const& account, - MPTIssue const& issue, + MPTIssue const& mptIssue, FreezeHandling zeroIfFrozen, AuthHandling zeroIfUnauthorized, beast::Journal j) { STAmount amount; - auto const sleMpt = view.read(keylet::mptoken(issue.getMptID(), account)); + auto const sleMpt = + view.read(keylet::mptoken(mptIssue.getMptID(), account)); if (!sleMpt) - amount.clear(issue); - else if (zeroIfFrozen == fhZERO_IF_FROZEN && isFrozen(view, account, issue)) - amount.clear(issue); + amount.clear(mptIssue); + else if ( + zeroIfFrozen == fhZERO_IF_FROZEN && isFrozen(view, account, mptIssue)) + amount.clear(mptIssue); else { auto const amt = sleMpt->getFieldU64(sfMPTAmount); auto const locked = sleMpt->getFieldU64(sfLockedAmount); if (amt > locked) - amount = STAmount{issue, amt - locked}; + amount = STAmount{mptIssue, 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())); + view.read(keylet::mptIssuance(mptIssue.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); + amount.clear(mptIssue); } } @@ -564,9 +570,9 @@ transferRate(ReadView const& view, AccountID const& issuer) } Rate -transferRate(ReadView const& view, MPTID const& id) +transferRate(ReadView const& view, MPTID const& issuanceID) { - auto const sle = view.read(keylet::mptIssuance(id)); + auto const sle = view.read(keylet::mptIssuance(issuanceID)); // fee is 0-50,000 (0-50%), rate is 1,000,000,000-2,000,000,000 if (sle && sle->isFieldPresent(sfTransferFee)) @@ -1707,9 +1713,12 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account) } TER -requireAuth(ReadView const& view, MPTIssue const& mpt, AccountID const& account) +requireAuth( + ReadView const& view, + MPTIssue const& mptIssue, + AccountID const& account) { - auto const mptID = keylet::mptIssuance(mpt.getMptID()); + auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); if (auto const sle = view.read(mptID); sle && sle->getFieldU32(sfFlags) & lsfMPTRequireAuth) { @@ -1725,11 +1734,11 @@ requireAuth(ReadView const& view, MPTIssue const& mpt, AccountID const& account) TER canTransfer( ReadView const& view, - MPTIssue const& mpt, + MPTIssue const& mptIssue, AccountID const& from, AccountID const& to) { - auto const mptID = keylet::mptIssuance(mpt.getMptID()); + auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); if (auto const sle = view.read(mptID); sle && !(sle->getFieldU32(sfFlags) & lsfMPTCanTransfer)) { diff --git a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp index 8de1e8f3344..4c6a0308693 100644 --- a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp +++ b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp @@ -58,7 +58,7 @@ getIDFromCreatedIssuance(TxMeta const& transactionMeta) auto const& mptNode = node.peekAtField(sfNewFields).downcast(); - return getMptID( + return makeMptID( mptNode.getAccountID(sfIssuer), mptNode.getFieldU32(sfSequence)); } diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index 87ec9ff03f4..99b9e21fed4 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -287,7 +287,7 @@ getAccountObjects( { auto sleJson = sleNode->getJson(JsonOptions::none); if (sleNode->getType() == ltMPTOKEN_ISSUANCE) - sleJson[jss::mpt_issuance_id] = to_string(getMptID( + sleJson[jss::mpt_issuance_id] = to_string(makeMptID( sleNode->getAccountID(sfIssuer), (*sleNode)[sfSequence])); diff --git a/src/xrpld/rpc/handlers/LedgerData.cpp b/src/xrpld/rpc/handlers/LedgerData.cpp index 80d321f0372..16f35100c8f 100644 --- a/src/xrpld/rpc/handlers/LedgerData.cpp +++ b/src/xrpld/rpc/handlers/LedgerData.cpp @@ -126,7 +126,7 @@ doLedgerData(RPC::JsonContext& context) entry[jss::index] = to_string(sle->key()); if (sle->getType() == ltMPTOKEN_ISSUANCE) - entry[jss::mpt_issuance_id] = to_string(getMptID( + entry[jss::mpt_issuance_id] = to_string(makeMptID( sle->getAccountID(sfIssuer), (*sle)[sfSequence])); } } From aef142661e249c38471265170e256ecde6dfc3dd Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Tue, 1 Oct 2024 19:51:30 -0400 Subject: [PATCH 04/58] Allow MPT SendMax in Payment tx. Update MPT Payment errors to be consistent with IOU Payment. --- src/libxrpl/protocol/TxFormats.cpp | 2 +- src/test/app/MPToken_test.cpp | 54 +++++++++++++++++++---------- src/test/jtx/impl/mpt.cpp | 6 ++++ src/test/jtx/mpt.h | 3 ++ src/xrpld/app/tx/detail/Payment.cpp | 47 ++++++++++++++++++------- 5 files changed, 79 insertions(+), 33 deletions(-) diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index 92e8ff3b690..01d097aa17f 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -163,7 +163,7 @@ TxFormats::TxFormats() { {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, - {sfSendMax, soeOPTIONAL}, + {sfSendMax, soeOPTIONAL, soeMPTSupported}, {sfPaths, soeDEFAULT}, {sfInvoiceID, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 75ab42a96c3..df37651d8a4 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -742,10 +742,10 @@ class MPToken_test : public beast::unit_test::suite mptAlice.pay(alice, bob, 100); // Pay to another holder - mptAlice.pay(bob, carol, 101, tecINSUFFICIENT_FUNDS); + mptAlice.pay(bob, carol, 101, tecPATH_PARTIAL); // Pay to the issuer - mptAlice.pay(bob, alice, 101, tecINSUFFICIENT_FUNDS); + mptAlice.pay(bob, alice, 101, tecPATH_PARTIAL); } // MPT is locked @@ -805,7 +805,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.pay(alice, bob, 100); // issuer tries to exceed max amount - mptAlice.pay(alice, bob, 1, tecMPT_MAX_AMOUNT_EXCEEDED); + mptAlice.pay(alice, bob, 1, tecPATH_PARTIAL); } // Issuer fails trying to send more than the default maximum @@ -823,7 +823,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.pay(alice, bob, maxMPTokenAmount); // issuer tries to exceed max amount - mptAlice.pay(alice, bob, 1, tecMPT_MAX_AMOUNT_EXCEEDED); + mptAlice.pay(alice, bob, 1, tecPATH_PARTIAL); } // Can't pay negative amount @@ -876,14 +876,25 @@ class MPToken_test : public beast::unit_test::suite mptAlice.pay(alice, bob, 2'000); // Payment between the holder and the issuer, no transfer fee. - mptAlice.pay(alice, bob, 1'000); + mptAlice.pay(bob, alice, 1'000); // Payment between the holders. The sender doesn't have // enough funds to cover the transfer fee. - mptAlice.pay(bob, carol, 1'000); + mptAlice.pay(bob, carol, 1'000, tecPATH_PARTIAL); - // Payment between the holders. The sender pays 10% transfer fee. - mptAlice.pay(bob, carol, 100); + // Payment between the holders. The sender has enough funds + // but SendMax is not included. + mptAlice.pay(bob, carol, 100, tecPATH_PARTIAL); + + auto const MPT = mptAlice["MPT"]; + // SendMax doesn't cover the fee + env(pay(bob, carol, MPT(100)), + sendmax(MPT(109)), + ter(tecPATH_PARTIAL)); + + // Payment succeeds if SendMax is included. + env(pay(bob, carol, MPT(100)), sendmax(MPT(110))); + env(pay(bob, carol, MPT(100)), sendmax(MPT(115))); } // Test that non-issuer cannot send to each other if MPTCanTransfer @@ -962,12 +973,21 @@ class MPToken_test : public beast::unit_test::suite // sendMax and DeliverMin are valid XRP amount, // but is invalid combination with MPT amount - env(pay(alice, carol, mptAlice.mpt(100)), + auto const MPT = mptAlice["MPT"]; + env(pay(alice, carol, MPT(100)), sendmax(XRP(100)), ter(temMALFORMED)); - env(pay(alice, carol, mptAlice.mpt(100)), + env(pay(alice, carol, MPT(100)), delivermin(XRP(100)), ter(temMALFORMED)); + // sendMax MPT is invalid with IOU or XRP + auto const USD = alice["USD"]; + env(pay(alice, carol, USD(100)), + sendmax(MPT(100)), + ter(temMALFORMED)); + env(pay(alice, carol, XRP(100)), + sendmax(MPT(100)), + ter(temMALFORMED)); } // build_path is invalid if MPT @@ -979,12 +999,13 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); + auto const MPT = mptAlice["MPT"]; mptAlice.authorize({.account = &carol}); Json::Value payment; payment[jss::secret] = alice.name(); - payment[jss::tx_json] = pay(alice, carol, mptAlice.mpt(100)); + payment[jss::tx_json] = pay(alice, carol, MPT(100)); payment[jss::build_path] = true; auto jrr = env.rpc("json", "submit", to_string(payment)); @@ -1237,20 +1258,15 @@ class MPToken_test : public beast::unit_test::suite 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); + jv[jss::DeliverMin] = mpt.getJson(JsonOptions::none); test(jv); - }; - payment(sfSendMax); - payment(sfDeliverMin); + } // NFTokenCreateOffer { Json::Value jv; diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 3bd69850054..6af502ee2cc 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -393,6 +393,12 @@ MPTTester::getFlags(ripple::test::jtx::AccountP holder) const return flags; } +MPT +MPTTester::operator[](const std::string& name) +{ + return MPT(name, issuanceID()); +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index e66433260eb..0e61b4ee2b6 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -215,6 +215,9 @@ class MPTTester std::int64_t getAmount(Account const& account) const; + MPT + operator[](std::string const& name); + private: using SLEP = std::shared_ptr; bool diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 6f68789357e..08742ff0bbf 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include @@ -89,6 +88,9 @@ preflightHelper(PreflightContext const& ctx) saDstAmount.exponent(), saDstAmount < beast::zero); + if (!maxSourceAmount.holds()) + return temMALFORMED; + auto const& uSrcCurrency = maxSourceAmount.getCurrency(); auto const& uDstCurrency = saDstAmount.getCurrency(); @@ -219,8 +221,11 @@ preflightHelper(PreflightContext const& ctx) if (!ctx.rules.enabled(featureMPTokensV1)) return temDISABLED; - if (ctx.tx.isFieldPresent(sfDeliverMin) || - ctx.tx.isFieldPresent(sfSendMax) || ctx.tx.isFieldPresent(sfPaths)) + if (ctx.tx.isFieldPresent(sfDeliverMin) || ctx.tx.isFieldPresent(sfPaths)) + return temMALFORMED; + + if (auto const sendMax = ctx.tx[~sfSendMax]; + sendMax && !sendMax->holds()) return temMALFORMED; auto& tx = ctx.tx; @@ -667,19 +672,35 @@ applyHelper(ApplyContext& ctx, XRPAmount const&, XRPAmount const&) auto const& mpt = saDstAmount.get(); 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; + + if (account != issuer && uDstAccountID != issuer) + { + // If globally/individually locked then + // - can't send between holders + // - holder can send back to issuer + // - issuer can send to holder + if (isFrozen(ctx.view(), account, mpt) || + isFrozen(ctx.view(), uDstAccountID, mpt)) + return tecMPT_LOCKED; + + auto const sendMax(ctx.tx[~sfSendMax]); + // If the transfer fee is included then SendMax has to be included + // and be large enough to cover the fee. The payment can still fail if + // the sender has insufficient funds. + if (auto const rate = transferRate(ctx.view(), mpt.getMptID()); + rate != parityRate && + (!sendMax || multiply(saDstAmount, rate) > *sendMax)) + return tecPATH_PARTIAL; + } PaymentSandbox pv(&ctx.view()); - auto const res = + auto res = accountSendMPT(pv, account, uDstAccountID, saDstAmount, ctx.journal); - pv.apply(ctx.rawView()); + if (res == tesSUCCESS) + pv.apply(ctx.rawView()); + else if (res == tecINSUFFICIENT_FUNDS || res == tecMPT_MAX_AMOUNT_EXCEEDED) + res = tecPATH_PARTIAL; + return res; } From 9136a89a3d69fb45afb78bbf002c84ea5d21c319 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Wed, 2 Oct 2024 07:47:28 -0400 Subject: [PATCH 05/58] Apply suggestions from code review Co-authored-by: Ed Hennis --- include/xrpl/protocol/STAmount.h | 9 ++++----- include/xrpl/protocol/UintTypes.h | 2 +- src/xrpld/ledger/detail/View.cpp | 8 ++++++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 449cd952775..a8efb05cd7b 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -351,6 +351,7 @@ STAmount::STAmount( , mOffset(exponent) , mIsNegative(negative) { + // mValue is uint64, but needs to fit in the range of int64 assert(mValue <= std::numeric_limits::max()); canonicalize(); } @@ -386,9 +387,9 @@ inline STAmount::STAmount(IOUAmount const& amount, Issue const& issue) , mIsNegative(amount < beast::zero) { if (mIsNegative) - mValue = static_cast(-amount.mantissa()); + mValue = unsafe_cast(-amount.mantissa()); else - mValue = static_cast(amount.mantissa()); + mValue = unsafe_cast(amount.mantissa()); canonicalize(); } @@ -508,9 +509,7 @@ STAmount::signum() const noexcept inline STAmount STAmount::zeroed() const { - if (mAsset.holds()) - return STAmount(mAsset.get()); - return STAmount(mAsset.get()); + return STAmount(mAsset); } inline STAmount::operator bool() const noexcept diff --git a/include/xrpl/protocol/UintTypes.h b/include/xrpl/protocol/UintTypes.h index a8fefab91ce..cf676189bad 100644 --- a/include/xrpl/protocol/UintTypes.h +++ b/include/xrpl/protocol/UintTypes.h @@ -58,7 +58,7 @@ using Currency = base_uint<160, detail::CurrencyTag>; /** NodeID is a 160-bit hash representing one node. */ using NodeID = base_uint<160, detail::NodeIDTag>; -/** MPTID is a 192-bit representing MPT Issuance ID, +/** MPTID is a 192-bit value representing MPT Issuance ID, * which is a concatenation of a 32-bit sequence (big endian) * and a 160-bit account */ using MPTID = base_uint<192>; diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 7710be2a12a..6e1f4536fcc 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -1368,8 +1368,12 @@ rippleSendMPT( if (!sle) return tecMPT_ISSUANCE_NOT_FOUND; - if (sle->getFieldU64(sfOutstandingAmount) + saAmount.mpt().value() > - (*sle)[~sfMaximumAmount].value_or(maxMPTokenAmount)) + auto const sendAmount = saAmount.mpt().value(); + auto const maximumAmount = + sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount); + if (sendAmount > maximumAmount || + sle->getFieldU64(sfOutstandingAmount) > + maximumAmount - sendAmount) return tecMPT_MAX_AMOUNT_EXCEEDED; } From 16a029a3c7c3543322119a1488ccc92e1551465c Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Wed, 2 Oct 2024 08:39:54 -0400 Subject: [PATCH 06/58] Address reviewer's feedback --- include/xrpl/protocol/Issue.h | 3 +++ include/xrpl/protocol/MPTIssue.h | 6 ++++++ src/libxrpl/protocol/Asset.cpp | 16 ++++++---------- src/libxrpl/protocol/Issue.cpp | 12 +++++++++--- src/libxrpl/protocol/MPTIssue.cpp | 14 +++++++++++++- src/libxrpl/protocol/STAmount.cpp | 2 +- 6 files changed, 38 insertions(+), 15 deletions(-) diff --git a/include/xrpl/protocol/Issue.h b/include/xrpl/protocol/Issue.h index c533ff678ec..aa5f5174485 100644 --- a/include/xrpl/protocol/Issue.h +++ b/include/xrpl/protocol/Issue.h @@ -52,6 +52,9 @@ class Issue std::string getText() const; + + void + setJson(Json::Value& jv) const; }; bool diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index bac648ae2ab..d5bcc7f0751 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -47,6 +47,12 @@ class MPTIssue MPTID const& getMptID() const; + std::string + getText() const; + + void + setJson(Json::Value& jv) const; + friend constexpr bool operator==(MPTIssue const& lhs, MPTIssue const& rhs); diff --git a/src/libxrpl/protocol/Asset.cpp b/src/libxrpl/protocol/Asset.cpp index 97725e52f4c..4d0ce4c4750 100644 --- a/src/libxrpl/protocol/Asset.cpp +++ b/src/libxrpl/protocol/Asset.cpp @@ -58,20 +58,16 @@ Asset::getText() const { if (holds()) return get().getText(); - return to_string(get().getMptID()); + return get().getText(); } void Asset::setJson(Json::Value& jv) const { - if (holds()) - jv[jss::mpt_issuance_id] = to_string(get().getMptID()); + if (holds()) + get().setJson(jv); else - { - jv[jss::currency] = to_string(get().currency); - if (!isXRP(get().currency)) - jv[jss::issuer] = toBase58(get().account); - } + get().setJson(jv); } std::string @@ -79,7 +75,7 @@ to_string(Asset const& asset) { if (asset.holds()) return to_string(asset.get()); - return to_string(asset.get().getMptID()); + return to_string(asset.get()); } bool @@ -90,4 +86,4 @@ validJSONAsset(Json::Value const& jv) return jv.isMember(jss::currency); } -} // namespace ripple \ No newline at end of file +} // namespace ripple diff --git a/src/libxrpl/protocol/Issue.cpp b/src/libxrpl/protocol/Issue.cpp index 70d2c013d7b..59ca3b9f395 100644 --- a/src/libxrpl/protocol/Issue.cpp +++ b/src/libxrpl/protocol/Issue.cpp @@ -49,6 +49,14 @@ Issue::getText() const return ret; } +void +Issue::setJson(Json::Value& jv) const +{ + jv[jss::currency] = to_string(currency); + if (!isXRP(currency)) + jv[jss::issuer] = toBase58(account); +} + bool isConsistent(Issue const& ac) { @@ -68,9 +76,7 @@ Json::Value to_json(Issue const& is) { Json::Value jv; - jv[jss::currency] = to_string(is.currency); - if (!isXRP(is.currency)) - jv[jss::issuer] = toBase58(is.account); + is.setJson(jv); return jv; } diff --git a/src/libxrpl/protocol/MPTIssue.cpp b/src/libxrpl/protocol/MPTIssue.cpp index c4a4947e0ca..951b9d139c3 100644 --- a/src/libxrpl/protocol/MPTIssue.cpp +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -43,11 +43,23 @@ MPTIssue::getMptID() const return mptID_; } +std::string +MPTIssue::getText() const +{ + return to_string(mptID_); +} + +void +MPTIssue::setJson(Json::Value& jv) const +{ + jv[jss::mpt_issuance_id] = to_string(mptID_); +} + Json::Value to_json(MPTIssue const& mptIssue) { Json::Value jv; - jv[jss::mpt_issuance_id] = to_string(mptIssue.getMptID()); + mptIssue.setJson(jv); return jv; } diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 8dd08463664..a05c68a9532 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -21,7 +21,7 @@ #include #include #include -#include +#include #include #include #include From 2beb2d9a7d6016d4047b312b8cbe5bce20907d46 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 3 Oct 2024 08:45:10 -0400 Subject: [PATCH 07/58] Manually align jss and disable clang-format --- include/xrpl/protocol/jss.h | 1046 ++++++++++++++++++----------------- 1 file changed, 524 insertions(+), 522 deletions(-) diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index a5d3788379f..1716be7e7ec 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -41,63 +41,64 @@ 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(BidMax); // in: AMM Bid -JSS(BidMin); // in: AMM Bid -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(MPToken); // ledger type. -JSS(MPTokenIssuance); // ledger type. +// clang-format off +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(BidMax); // in: AMM Bid +JSS(BidMin); // in: AMM Bid +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(MPToken); // ledger type. +JSS(MPTokenIssuance); // ledger type. JSS(MPTokenIssuanceCreate); // transaction type. JSS(MPTokenIssuanceDestroy); // transaction type. JSS(MPTokenAuthorize); // transaction type. @@ -158,133 +159,133 @@ 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. -JSS(XChainClaim); // transaction type. -JSS(XChainCommit); // transaction type. -JSS(XChainCreateBridge); // transaction type. -JSS(XChainCreateClaimID); // transaction type. -JSS(XChainModifyBridge); // transaction type. -JSS(XChainOwnedClaimID); // ledger type. -JSS(XChainOwnedCreateAccountClaimID); // ledger type. -JSS(aborted); // out: InboundLedger -JSS(accepted); // out: LedgerToJson, OwnerInfo, SubmitTransaction -JSS(account); // in/out: many -JSS(accountState); // out: LedgerToJson -JSS(accountTreeHash); // out: ledger/Ledger.cpp -JSS(account_data); // out: AccountInfo -JSS(account_flags); // out: AccountInfo -JSS(account_hash); // out: LedgerToJson -JSS(account_id); // out: WalletPropose -JSS(account_nfts); // out: AccountNFTs -JSS(account_objects); // out: AccountObjects -JSS(account_root); // in: LedgerEntry -JSS(account_sequence_next); // out: SubmitTransaction -JSS(account_sequence_available); // out: SubmitTransaction -JSS(account_history_tx_stream); // in: Subscribe, Unsubscribe -JSS(account_history_tx_index); // out: Account txn history subscribe +JSS(XChainAddAccountCreateAttestation); // transaction type. +JSS(XChainAddClaimAttestation); // transaction type. +JSS(XChainAccountCreateCommit); // transaction type. +JSS(XChainClaim); // transaction type. +JSS(XChainCommit); // transaction type. +JSS(XChainCreateBridge); // transaction type. +JSS(XChainCreateClaimID); // transaction type. +JSS(XChainModifyBridge); // transaction type. +JSS(XChainOwnedClaimID); // ledger type. +JSS(XChainOwnedCreateAccountClaimID); // ledger type. +JSS(aborted); // out: InboundLedger +JSS(accepted); // out: LedgerToJson, OwnerInfo, SubmitTransaction +JSS(account); // in/out: many +JSS(accountState); // out: LedgerToJson +JSS(accountTreeHash); // out: ledger/Ledger.cpp +JSS(account_data); // out: AccountInfo +JSS(account_flags); // out: AccountInfo +JSS(account_hash); // out: LedgerToJson +JSS(account_id); // out: WalletPropose +JSS(account_nfts); // out: AccountNFTs +JSS(account_objects); // out: AccountObjects +JSS(account_root); // in: LedgerEntry +JSS(account_sequence_next); // out: SubmitTransaction +JSS(account_sequence_available); // out: SubmitTransaction +JSS(account_history_tx_stream); // in: Subscribe, Unsubscribe +JSS(account_history_tx_index); // out: Account txn history subscribe -JSS(account_history_tx_first); // out: Account txn history subscribe -JSS(account_history_boundary); // out: Account txn history subscribe -JSS(accounts); // in: LedgerEntry, Subscribe, - // handlers/Ledger, Unsubscribe -JSS(accounts_proposed); // in: Subscribe, Unsubscribe +JSS(account_history_tx_first); // out: Account txn history subscribe +JSS(account_history_boundary); // out: Account txn history subscribe +JSS(accounts); // in: LedgerEntry, Subscribe, + // handlers/Ledger, Unsubscribe +JSS(accounts_proposed); // in: Subscribe, Unsubscribe JSS(action); -JSS(acquiring); // out: LedgerRequest -JSS(address); // out: PeerImp -JSS(affected); // out: AcceptedLedgerTx -JSS(age); // out: NetworkOPs, Peers -JSS(alternatives); // out: PathRequest, RipplePathFind -JSS(amendment_blocked); // out: NetworkOPs -JSS(amendments); // in: AccountObjects, out: NetworkOPs -JSS(amm); // out: amm_info -JSS(amm_account); // in: amm_info -JSS(amount); // out: AccountChannels, amm_info -JSS(amount2); // out: amm_info -JSS(api_version); // in: many, out: Version -JSS(api_version_low); // out: Version -JSS(applied); // out: SubmitTransaction -JSS(asks); // out: Subscribe -JSS(asset); // in: amm_info -JSS(asset2); // in: amm_info -JSS(assets); // out: GatewayBalances -JSS(asset_frozen); // out: amm_info -JSS(asset2_frozen); // out: amm_info -JSS(attestations); // -JSS(attestation_reward_account); // -JSS(auction_slot); // out: amm_info -JSS(authorized); // out: AccountLines -JSS(auth_accounts); // out: amm_info -JSS(auth_change); // out: AccountInfo -JSS(auth_change_queued); // out: AccountInfo -JSS(available); // out: ValidatorList -JSS(avg_bps_recv); // out: Peers -JSS(avg_bps_sent); // out: Peers -JSS(balance); // out: AccountLines -JSS(balances); // out: GatewayBalances -JSS(base); // out: LogLevel -JSS(base_asset); // in: get_aggregate_price -JSS(base_fee); // out: NetworkOPs -JSS(base_fee_xrp); // out: NetworkOPs -JSS(bids); // out: Subscribe -JSS(binary); // in: AccountTX, LedgerEntry, - // AccountTxOld, Tx LedgerData -JSS(blob); // out: ValidatorList -JSS(blobs_v2); // out: ValidatorList - // in: UNL -JSS(books); // in: Subscribe, Unsubscribe -JSS(both); // in: Subscribe, Unsubscribe -JSS(both_sides); // in: Subscribe, Unsubscribe -JSS(broadcast); // out: SubmitTransaction -JSS(bridge); // in: LedgerEntry -JSS(bridge_account); // in: LedgerEntry -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 -JSS(check); // in: AccountObjects -JSS(check_nodes); // in: LedgerCleaner -JSS(clear); // in/out: FetchInfo -JSS(close); // out: BookChanges -JSS(close_flags); // out: LedgerToJson -JSS(close_time); // in: Application, out: NetworkOPs, - // RCLCxPeerPos, LedgerToJson -JSS(close_time_iso); // out: Tx, NetworkOPs, TransactionEntry - // AccountTx, LedgerToJson -JSS(close_time_estimated); // in: Application, out: LedgerToJson -JSS(close_time_human); // out: LedgerToJson -JSS(close_time_offset); // out: NetworkOPs -JSS(close_time_resolution); // in: Application; out: LedgerToJson -JSS(closed); // out: NetworkOPs, LedgerToJson, - // handlers/Ledger -JSS(closed_ledger); // out: NetworkOPs -JSS(cluster); // out: PeerImp -JSS(code); // out: errors -JSS(command); // in: RPCHandler -JSS(complete); // out: NetworkOPs, InboundLedger -JSS(complete_ledgers); // out: NetworkOPs, PeerImp -JSS(consensus); // out: NetworkOPs, LedgerConsensus -JSS(converge_time); // out: NetworkOPs -JSS(converge_time_s); // out: NetworkOPs -JSS(cookie); // out: NetworkOPs -JSS(count); // in: AccountTx*, ValidatorList -JSS(counters); // in/out: retrieve counters -JSS(ctid); // in/out: Tx RPC -JSS(currency_a); // out: BookChanges -JSS(currency_b); // out: BookChanges -JSS(currency); // in: paths/PathRequest, STAmount - // out: STPathSet, STAmount, - // AccountLines -JSS(current); // out: OwnerInfo +JSS(acquiring); // out: LedgerRequest +JSS(address); // out: PeerImp +JSS(affected); // out: AcceptedLedgerTx +JSS(age); // out: NetworkOPs, Peers +JSS(alternatives); // out: PathRequest, RipplePathFind +JSS(amendment_blocked); // out: NetworkOPs +JSS(amendments); // in: AccountObjects, out: NetworkOPs +JSS(amm); // out: amm_info +JSS(amm_account); // in: amm_info +JSS(amount); // out: AccountChannels, amm_info +JSS(amount2); // out: amm_info +JSS(api_version); // in: many, out: Version +JSS(api_version_low); // out: Version +JSS(applied); // out: SubmitTransaction +JSS(asks); // out: Subscribe +JSS(asset); // in: amm_info +JSS(asset2); // in: amm_info +JSS(assets); // out: GatewayBalances +JSS(asset_frozen); // out: amm_info +JSS(asset2_frozen); // out: amm_info +JSS(attestations); +JSS(attestation_reward_account); +JSS(auction_slot); // out: amm_info +JSS(authorized); // out: AccountLines +JSS(auth_accounts); // out: amm_info +JSS(auth_change); // out: AccountInfo +JSS(auth_change_queued); // out: AccountInfo +JSS(available); // out: ValidatorList +JSS(avg_bps_recv); // out: Peers +JSS(avg_bps_sent); // out: Peers +JSS(balance); // out: AccountLines +JSS(balances); // out: GatewayBalances +JSS(base); // out: LogLevel +JSS(base_asset); // in: get_aggregate_price +JSS(base_fee); // out: NetworkOPs +JSS(base_fee_xrp); // out: NetworkOPs +JSS(bids); // out: Subscribe +JSS(binary); // in: AccountTX, LedgerEntry, + // AccountTxOld, Tx LedgerData +JSS(blob); // out: ValidatorList +JSS(blobs_v2); // out: ValidatorList + // in: UNL +JSS(books); // in: Subscribe, Unsubscribe +JSS(both); // in: Subscribe, Unsubscribe +JSS(both_sides); // in: Subscribe, Unsubscribe +JSS(broadcast); // out: SubmitTransaction +JSS(bridge); // in: LedgerEntry +JSS(bridge_account); // in: LedgerEntry +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 +JSS(check); // in: AccountObjects +JSS(check_nodes); // in: LedgerCleaner +JSS(clear); // in/out: FetchInfo +JSS(close); // out: BookChanges +JSS(close_flags); // out: LedgerToJson +JSS(close_time); // in: Application, out: NetworkOPs, + // RCLCxPeerPos, LedgerToJson +JSS(close_time_iso); // out: Tx, NetworkOPs, TransactionEntry + // AccountTx, LedgerToJson +JSS(close_time_estimated); // in: Application, out: LedgerToJson +JSS(close_time_human); // out: LedgerToJson +JSS(close_time_offset); // out: NetworkOPs +JSS(close_time_resolution); // in: Application; out: LedgerToJson +JSS(closed); // out: NetworkOPs, LedgerToJson, + // handlers/Ledger +JSS(closed_ledger); // out: NetworkOPs +JSS(cluster); // out: PeerImp +JSS(code); // out: errors +JSS(command); // in: RPCHandler +JSS(complete); // out: NetworkOPs, InboundLedger +JSS(complete_ledgers); // out: NetworkOPs, PeerImp +JSS(consensus); // out: NetworkOPs, LedgerConsensus +JSS(converge_time); // out: NetworkOPs +JSS(converge_time_s); // out: NetworkOPs +JSS(cookie); // out: NetworkOPs +JSS(count); // in: AccountTx*, ValidatorList +JSS(counters); // in/out: retrieve counters +JSS(ctid); // in/out: Tx RPC +JSS(currency_a); // out: BookChanges +JSS(currency_b); // out: BookChanges +JSS(currency); // in: paths/PathRequest, STAmount + // out: STPathSet, STAmount, + // AccountLines +JSS(current); // out: OwnerInfo JSS(current_activities); JSS(current_ledger_size); // out: TxQ JSS(current_queue_size); // out: TxQ @@ -329,356 +330,356 @@ JSS(ephemeral_key); // out: ValidatorInfo // in/out: Manifest JSS(error); // out: error JSS(errored); -JSS(error_code); // out: error -JSS(error_exception); // out: Submit -JSS(error_message); // out: error -JSS(escrow); // in: LedgerEntry -JSS(expand); // in: handler/Ledger -JSS(expected_date); // out: any (warnings) -JSS(expected_date_UTC); // out: any (warnings) -JSS(expected_ledger_size); // out: TxQ -JSS(expiration); // out: AccountOffers, AccountChannels, - // ValidatorList, amm_info -JSS(fail_hard); // in: Sign, Submit -JSS(failed); // out: InboundLedger -JSS(feature); // in: Feature -JSS(features); // out: Feature -JSS(fee); // out: NetworkOPs, Peers -JSS(fee_base); // out: NetworkOPs -JSS(fee_div_max); // in: TransactionSign -JSS(fee_level); // out: AccountInfo -JSS(fee_mult_max); // in: TransactionSign -JSS(fee_ref); // out: NetworkOPs, DEPRECATED -JSS(fetch_pack); // out: NetworkOPs -JSS(FIELDS); // out: RPC server_definitions - // matches definitions.json format -JSS(first); // out: rpc/Version +JSS(error_code); // out: error +JSS(error_exception); // out: Submit +JSS(error_message); // out: error +JSS(escrow); // in: LedgerEntry +JSS(expand); // in: handler/Ledger +JSS(expected_date); // out: any (warnings) +JSS(expected_date_UTC); // out: any (warnings) +JSS(expected_ledger_size); // out: TxQ +JSS(expiration); // out: AccountOffers, AccountChannels, + // ValidatorList, amm_info +JSS(fail_hard); // in: Sign, Submit +JSS(failed); // out: InboundLedger +JSS(feature); // in: Feature +JSS(features); // out: Feature +JSS(fee); // out: NetworkOPs, Peers +JSS(fee_base); // out: NetworkOPs +JSS(fee_div_max); // in: TransactionSign +JSS(fee_level); // out: AccountInfo +JSS(fee_mult_max); // in: TransactionSign +JSS(fee_ref); // out: NetworkOPs, DEPRECATED +JSS(fetch_pack); // out: NetworkOPs +JSS(FIELDS); // out: RPC server_definitions + // matches definitions.json format +JSS(first); // out: rpc/Version JSS(finished); -JSS(fix_txns); // in: LedgerCleaner -JSS(flags); // out: AccountOffers, - // NetworkOPs -JSS(forward); // in: AccountTx -JSS(freeze); // out: AccountLines -JSS(freeze_peer); // out: AccountLines -JSS(frozen_balances); // out: GatewayBalances -JSS(full); // in: LedgerClearer, handlers/Ledger -JSS(full_reply); // out: PathFind -JSS(fullbelow_size); // out: GetCounts -JSS(good); // out: RPCVersion -JSS(hash); // out: NetworkOPs, InboundLedger, - // LedgerToJson, STTx; field -JSS(hashes); // in: AccountObjects -JSS(have_header); // out: InboundLedger -JSS(have_state); // out: InboundLedger -JSS(have_transactions); // out: InboundLedger -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. -JSS(ident); // in: AccountCurrencies, AccountInfo, - // OwnerInfo -JSS(ignore_default); // in: AccountLines -JSS(inLedger); // out: tx/Transaction -JSS(inbound); // out: PeerImp -JSS(index); // in: LedgerEntry - // out: STLedgerEntry, - // LedgerEntry, TxHistory, LedgerData -JSS(info); // out: ServerInfo, ConsensusInfo, FetchInfo +JSS(fix_txns); // in: LedgerCleaner +JSS(flags); // out: AccountOffers, + // NetworkOPs +JSS(forward); // in: AccountTx +JSS(freeze); // out: AccountLines +JSS(freeze_peer); // out: AccountLines +JSS(frozen_balances); // out: GatewayBalances +JSS(full); // in: LedgerClearer, handlers/Ledger +JSS(full_reply); // out: PathFind +JSS(fullbelow_size); // out: GetCounts +JSS(good); // out: RPCVersion +JSS(hash); // out: NetworkOPs, InboundLedger, + // LedgerToJson, STTx; field +JSS(hashes); // in: AccountObjects +JSS(have_header); // out: InboundLedger +JSS(have_state); // out: InboundLedger +JSS(have_transactions); // out: InboundLedger +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. +JSS(ident); // in: AccountCurrencies, AccountInfo, + // OwnerInfo +JSS(ignore_default); // in: AccountLines +JSS(inLedger); // out: tx/Transaction +JSS(inbound); // out: PeerImp +JSS(index); // in: LedgerEntry + // out: STLedgerEntry, + // LedgerEntry, TxHistory, LedgerData +JSS(info); // out: ServerInfo, ConsensusInfo, FetchInfo JSS(initial_sync_duration_us); -JSS(internal_command); // in: Internal -JSS(invalid_API_version); // out: Many, when a request has an invalid - // version -JSS(io_latency_ms); // out: NetworkOPs -JSS(ip); // in: Connect, out: OverlayImpl -JSS(is_burned); // out: nft_info (clio) -JSS(isSerialized); // out: RPC server_definitions - // matches definitions.json format -JSS(isSigningField); // out: RPC server_definitions - // matches definitions.json format -JSS(isVLEncoded); // out: RPC server_definitions - // matches definitions.json format -JSS(issuer); // in: RipplePathFind, Subscribe, - // Unsubscribe, BookOffers - // out: STPathSet, STAmount +JSS(internal_command); // in: Internal +JSS(invalid_API_version); // out: Many, when a request has an invalid + // version +JSS(io_latency_ms); // out: NetworkOPs +JSS(ip); // in: Connect, out: OverlayImpl +JSS(is_burned); // out: nft_info (clio) +JSS(isSerialized); // out: RPC server_definitions + // matches definitions.json format +JSS(isSigningField); // out: RPC server_definitions + // matches definitions.json format +JSS(isVLEncoded); // out: RPC server_definitions + // matches definitions.json format +JSS(issuer); // in: RipplePathFind, Subscribe, + // Unsubscribe, BookOffers + // out: STPathSet, STAmount JSS(job); JSS(job_queue); JSS(jobs); -JSS(jsonrpc); // json version -JSS(jq_trans_overflow); // JobQueue transaction limit overflow. -JSS(kept); // out: SubmitTransaction -JSS(key); // out -JSS(key_type); // in/out: WalletPropose, TransactionSign -JSS(latency); // out: PeerImp -JSS(last); // out: RPCVersion -JSS(last_close); // out: NetworkOPs -JSS(last_refresh_time); // out: ValidatorSite -JSS(last_refresh_status); // out: ValidatorSite -JSS(last_refresh_message); // out: ValidatorSite -JSS(ledger); // in: NetworkOPs, LedgerCleaner, - // RPCHelpers - // out: NetworkOPs, PeerImp -JSS(ledger_current_index); // out: NetworkOPs, RPCHelpers, - // LedgerCurrent, LedgerAccept, - // AccountLines -JSS(ledger_data); // out: LedgerHeader -JSS(ledger_hash); // in: RPCHelpers, LedgerRequest, - // RipplePathFind, TransactionEntry, - // handlers/Ledger - // out: NetworkOPs, RPCHelpers, - // LedgerClosed, LedgerData, - // AccountLines -JSS(ledger_hit_rate); // out: GetCounts -JSS(ledger_index); // in/out: many -JSS(ledger_index_max); // in, out: AccountTx* -JSS(ledger_index_min); // in, out: AccountTx* -JSS(ledger_max); // in, out: AccountTx* -JSS(ledger_min); // in, out: AccountTx* -JSS(ledger_time); // out: NetworkOPs -JSS(LEDGER_ENTRY_TYPES); // out: RPC server_definitions - // matches definitions.json format -JSS(levels); // LogLevels -JSS(limit); // in/out: AccountTx*, AccountOffers, - // AccountLines, AccountObjects - // in: LedgerData, BookOffers -JSS(limit_peer); // out: AccountLines -JSS(lines); // out: AccountLines -JSS(list); // out: ValidatorList -JSS(load); // out: NetworkOPs, PeerImp -JSS(load_base); // out: NetworkOPs -JSS(load_factor); // out: NetworkOPs -JSS(load_factor_cluster); // out: NetworkOPs -JSS(load_factor_fee_escalation); // out: NetworkOPs -JSS(load_factor_fee_queue); // out: NetworkOPs -JSS(load_factor_fee_reference); // out: NetworkOPs -JSS(load_factor_local); // out: NetworkOPs -JSS(load_factor_net); // out: NetworkOPs -JSS(load_factor_server); // out: NetworkOPs -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 -JSS(lp_token); // out: amm_info -JSS(majority); // out: RPC feature -JSS(manifest); // out: ValidatorInfo, Manifest -JSS(marker); // in/out: AccountTx, AccountOffers, - // AccountLines, AccountObjects, - // LedgerData - // in: BookOffers -JSS(master_key); // out: WalletPropose, NetworkOPs, - // ValidatorInfo - // in/out: Manifest -JSS(master_seed); // out: WalletPropose -JSS(master_seed_hex); // out: WalletPropose -JSS(master_signature); // out: pubManifest -JSS(max_ledger); // in/out: LedgerCleaner -JSS(max_queue_size); // out: TxQ -JSS(max_spend_drops); // out: AccountInfo -JSS(max_spend_drops_total); // out: AccountInfo -JSS(mean); // out: get_aggregate_price -JSS(median); // out: get_aggregate_price -JSS(median_fee); // out: TxQ -JSS(median_level); // out: TxQ -JSS(message); // error. -JSS(meta); // out: NetworkOPs, AccountTx*, Tx -JSS(meta_blob); // out: NetworkOPs, AccountTx*, Tx +JSS(jsonrpc); // json version +JSS(jq_trans_overflow); // JobQueue transaction limit overflow. +JSS(kept); // out: SubmitTransaction +JSS(key); // out +JSS(key_type); // in/out: WalletPropose, TransactionSign +JSS(latency); // out: PeerImp +JSS(last); // out: RPCVersion +JSS(last_close); // out: NetworkOPs +JSS(last_refresh_time); // out: ValidatorSite +JSS(last_refresh_status); // out: ValidatorSite +JSS(last_refresh_message); // out: ValidatorSite +JSS(ledger); // in: NetworkOPs, LedgerCleaner, + // RPCHelpers + // out: NetworkOPs, PeerImp +JSS(ledger_current_index); // out: NetworkOPs, RPCHelpers, + // LedgerCurrent, LedgerAccept, + // AccountLines +JSS(ledger_data); // out: LedgerHeader +JSS(ledger_hash); // in: RPCHelpers, LedgerRequest, + // RipplePathFind, TransactionEntry, + // handlers/Ledger + // out: NetworkOPs, RPCHelpers, + // LedgerClosed, LedgerData, + // AccountLines +JSS(ledger_hit_rate); // out: GetCounts +JSS(ledger_index); // in/out: many +JSS(ledger_index_max); // in, out: AccountTx* +JSS(ledger_index_min); // in, out: AccountTx* +JSS(ledger_max); // in, out: AccountTx* +JSS(ledger_min); // in, out: AccountTx* +JSS(ledger_time); // out: NetworkOPs +JSS(LEDGER_ENTRY_TYPES); // out: RPC server_definitions + // matches definitions.json format +JSS(levels); // LogLevels +JSS(limit); // in/out: AccountTx*, AccountOffers, + // AccountLines, AccountObjects + // in: LedgerData, BookOffers +JSS(limit_peer); // out: AccountLines +JSS(lines); // out: AccountLines +JSS(list); // out: ValidatorList +JSS(load); // out: NetworkOPs, PeerImp +JSS(load_base); // out: NetworkOPs +JSS(load_factor); // out: NetworkOPs +JSS(load_factor_cluster); // out: NetworkOPs +JSS(load_factor_fee_escalation); // out: NetworkOPs +JSS(load_factor_fee_queue); // out: NetworkOPs +JSS(load_factor_fee_reference); // out: NetworkOPs +JSS(load_factor_local); // out: NetworkOPs +JSS(load_factor_net); // out: NetworkOPs +JSS(load_factor_server); // out: NetworkOPs +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 +JSS(lp_token); // out: amm_info +JSS(majority); // out: RPC feature +JSS(manifest); // out: ValidatorInfo, Manifest +JSS(marker); // in/out: AccountTx, AccountOffers, + // AccountLines, AccountObjects, + // LedgerData + // in: BookOffers +JSS(master_key); // out: WalletPropose, NetworkOPs, + // ValidatorInfo + // in/out: Manifest +JSS(master_seed); // out: WalletPropose +JSS(master_seed_hex); // out: WalletPropose +JSS(master_signature); // out: pubManifest +JSS(max_ledger); // in/out: LedgerCleaner +JSS(max_queue_size); // out: TxQ +JSS(max_spend_drops); // out: AccountInfo +JSS(max_spend_drops_total); // out: AccountInfo +JSS(mean); // out: get_aggregate_price +JSS(median); // out: get_aggregate_price +JSS(median_fee); // out: TxQ +JSS(median_level); // out: TxQ +JSS(message); // error. +JSS(meta); // out: NetworkOPs, AccountTx*, Tx +JSS(meta_blob); // out: NetworkOPs, AccountTx*, Tx JSS(metaData); -JSS(metadata); // out: TransactionEntry -JSS(method); // RPC +JSS(metadata); // out: TransactionEntry +JSS(method); // RPC JSS(methods); -JSS(metrics); // out: Peers -JSS(min_count); // in: GetCounts -JSS(min_ledger); // in: LedgerCleaner -JSS(minimum_fee); // out: TxQ -JSS(minimum_level); // out: TxQ -JSS(missingCommand); // error -JSS(name); // out: AmendmentTableImpl, PeerImp -JSS(needed_state_hashes); // out: InboundLedger +JSS(metrics); // out: Peers +JSS(min_count); // in: GetCounts +JSS(min_ledger); // in: LedgerCleaner +JSS(minimum_fee); // out: TxQ +JSS(minimum_level); // out: TxQ +JSS(missingCommand); // error +JSS(name); // out: AmendmentTableImpl, PeerImp +JSS(needed_state_hashes); // out: InboundLedger JSS(needed_transaction_hashes); // out: InboundLedger -JSS(network_id); // out: NetworkOPs -JSS(network_ledger); // out: NetworkOPs -JSS(next_refresh_time); // out: ValidatorSite -JSS(nft_id); // in: nft_sell_offers, nft_buy_offers -JSS(nft_offer); // in: LedgerEntry -JSS(nft_offer_index); // out nft_buy_offers, nft_sell_offers -JSS(nft_page); // in: LedgerEntry -JSS(nft_serial); // out: account_nfts -JSS(nft_taxon); // out: nft_info (clio) -JSS(nftoken_id); // out: insertNFTokenID -JSS(nftoken_ids); // out: insertNFTokenID -JSS(no_ripple); // out: AccountLines -JSS(no_ripple_peer); // out: AccountLines -JSS(node); // out: LedgerEntry -JSS(node_binary); // out: LedgerEntry -JSS(node_read_bytes); // out: GetCounts -JSS(node_read_errors); // out: GetCounts -JSS(node_read_retries); // out: GetCounts -JSS(node_reads_hit); // out: GetCounts -JSS(node_reads_total); // out: GetCounts -JSS(node_reads_duration_us); // out: GetCounts -JSS(node_size); // out: server_info -JSS(nodestore); // out: GetCounts -JSS(node_writes); // out: GetCounts -JSS(node_written_bytes); // out: GetCounts -JSS(node_writes_duration_us); // out: GetCounts -JSS(node_write_retries); // out: GetCounts -JSS(node_writes_delayed); // out::GetCounts -JSS(nth); // out: RPC server_definitions -JSS(nunl); // in: AccountObjects -JSS(obligations); // out: GatewayBalances -JSS(offer); // in: LedgerEntry -JSS(offers); // out: NetworkOPs, AccountOffers, Subscribe -JSS(offer_id); // out: insertNFTokenOfferID -JSS(offline); // in: TransactionSign -JSS(offset); // in/out: AccountTxOld -JSS(open); // out: handlers/Ledger -JSS(open_ledger_cost); // out: SubmitTransaction -JSS(open_ledger_fee); // out: TxQ -JSS(open_ledger_level); // out: TxQ -JSS(oracle); // in: LedgerEntry -JSS(oracles); // in: get_aggregate_price -JSS(oracle_document_id); // in: get_aggregate_price -JSS(owner); // in: LedgerEntry, out: NetworkOPs -JSS(owner_funds); // in/out: Ledger, NetworkOPs, AcceptedLedgerTx +JSS(network_id); // out: NetworkOPs +JSS(network_ledger); // out: NetworkOPs +JSS(next_refresh_time); // out: ValidatorSite +JSS(nft_id); // in: nft_sell_offers, nft_buy_offers +JSS(nft_offer); // in: LedgerEntry +JSS(nft_offer_index); // out nft_buy_offers, nft_sell_offers +JSS(nft_page); // in: LedgerEntry +JSS(nft_serial); // out: account_nfts +JSS(nft_taxon); // out: nft_info (clio) +JSS(nftoken_id); // out: insertNFTokenID +JSS(nftoken_ids); // out: insertNFTokenID +JSS(no_ripple); // out: AccountLines +JSS(no_ripple_peer); // out: AccountLines +JSS(node); // out: LedgerEntry +JSS(node_binary); // out: LedgerEntry +JSS(node_read_bytes); // out: GetCounts +JSS(node_read_errors); // out: GetCounts +JSS(node_read_retries); // out: GetCounts +JSS(node_reads_hit); // out: GetCounts +JSS(node_reads_total); // out: GetCounts +JSS(node_reads_duration_us); // out: GetCounts +JSS(node_size); // out: server_info +JSS(nodestore); // out: GetCounts +JSS(node_writes); // out: GetCounts +JSS(node_written_bytes); // out: GetCounts +JSS(node_writes_duration_us); // out: GetCounts +JSS(node_write_retries); // out: GetCounts +JSS(node_writes_delayed); // out::GetCounts +JSS(nth); // out: RPC server_definitions +JSS(nunl); // in: AccountObjects +JSS(obligations); // out: GatewayBalances +JSS(offer); // in: LedgerEntry +JSS(offers); // out: NetworkOPs, AccountOffers, Subscribe +JSS(offer_id); // out: insertNFTokenOfferID +JSS(offline); // in: TransactionSign +JSS(offset); // in/out: AccountTxOld +JSS(open); // out: handlers/Ledger +JSS(open_ledger_cost); // out: SubmitTransaction +JSS(open_ledger_fee); // out: TxQ +JSS(open_ledger_level); // out: TxQ +JSS(oracle); // in: LedgerEntry +JSS(oracles); // in: get_aggregate_price +JSS(oracle_document_id); // in: get_aggregate_price +JSS(owner); // in: LedgerEntry, out: NetworkOPs +JSS(owner_funds); // in/out: Ledger, NetworkOPs, AcceptedLedgerTx JSS(page_index); -JSS(params); // RPC -JSS(parent_close_time); // out: LedgerToJson -JSS(parent_hash); // out: LedgerToJson -JSS(partition); // in: LogLevel -JSS(passphrase); // in: WalletPropose -JSS(password); // in: Subscribe -JSS(paths); // in: RipplePathFind -JSS(paths_canonical); // out: RipplePathFind -JSS(paths_computed); // out: PathRequest, RipplePathFind -JSS(payment_channel); // in: LedgerEntry -JSS(peer); // in: AccountLines -JSS(peer_authorized); // out: AccountLines -JSS(peer_id); // out: RCLCxPeerPos -JSS(peers); // out: InboundLedger, handlers/Peers, Overlay -JSS(peer_disconnects); // Severed peer connection counter. -JSS(peer_disconnects_resources); // Severed peer connections because of - // excess resource consumption. -JSS(port); // in: Connect, out: NetworkOPs -JSS(ports); // out: NetworkOPs -JSS(previous); // out: Reservations -JSS(previous_ledger); // out: LedgerPropose -JSS(price); // out: amm_info, AuctionSlot -JSS(proof); // in: BookOffers -JSS(propose_seq); // out: LedgerPropose -JSS(proposers); // out: NetworkOPs, LedgerConsensus -JSS(protocol); // out: NetworkOPs, PeerImp -JSS(proxied); // out: RPC ping -JSS(pubkey_node); // out: NetworkOPs -JSS(pubkey_publisher); // out: ValidatorList -JSS(pubkey_validator); // out: NetworkOPs, ValidatorList -JSS(public_key); // out: OverlayImpl, PeerImp, WalletPropose, - // ValidatorInfo - // in/out: Manifest -JSS(public_key_hex); // out: WalletPropose -JSS(published_ledger); // out: NetworkOPs -JSS(publisher_lists); // out: ValidatorList -JSS(quality); // out: NetworkOPs -JSS(quality_in); // out: AccountLines -JSS(quality_out); // out: AccountLines -JSS(queue); // in: AccountInfo -JSS(queue_data); // out: AccountInfo -JSS(queued); // out: SubmitTransaction +JSS(params); // RPC +JSS(parent_close_time); // out: LedgerToJson +JSS(parent_hash); // out: LedgerToJson +JSS(partition); // in: LogLevel +JSS(passphrase); // in: WalletPropose +JSS(password); // in: Subscribe +JSS(paths); // in: RipplePathFind +JSS(paths_canonical); // out: RipplePathFind +JSS(paths_computed); // out: PathRequest, RipplePathFind +JSS(payment_channel); // in: LedgerEntry +JSS(peer); // in: AccountLines +JSS(peer_authorized); // out: AccountLines +JSS(peer_id); // out: RCLCxPeerPos +JSS(peers); // out: InboundLedger, handlers/Peers, Overlay +JSS(peer_disconnects); // Severed peer connection counter. +JSS(peer_disconnects_resources); // Severed peer connections because of + // excess resource consumption. +JSS(port); // in: Connect, out: NetworkOPs +JSS(ports); // out: NetworkOPs +JSS(previous); // out: Reservations +JSS(previous_ledger); // out: LedgerPropose +JSS(price); // out: amm_info, AuctionSlot +JSS(proof); // in: BookOffers +JSS(propose_seq); // out: LedgerPropose +JSS(proposers); // out: NetworkOPs, LedgerConsensus +JSS(protocol); // out: NetworkOPs, PeerImp +JSS(proxied); // out: RPC ping +JSS(pubkey_node); // out: NetworkOPs +JSS(pubkey_publisher); // out: ValidatorList +JSS(pubkey_validator); // out: NetworkOPs, ValidatorList +JSS(public_key); // out: OverlayImpl, PeerImp, WalletPropose, + // ValidatorInfo + // in/out: Manifest +JSS(public_key_hex); // out: WalletPropose +JSS(published_ledger); // out: NetworkOPs +JSS(publisher_lists); // out: ValidatorList +JSS(quality); // out: NetworkOPs +JSS(quality_in); // out: AccountLines +JSS(quality_out); // out: AccountLines +JSS(queue); // in: AccountInfo +JSS(queue_data); // out: AccountInfo +JSS(queued); // out: SubmitTransaction JSS(queued_duration_us); -JSS(quote_asset); // in: get_aggregate_price -JSS(random); // out: Random -JSS(raw_meta); // out: AcceptedLedgerTx -JSS(receive_currencies); // out: AccountCurrencies -JSS(reference_level); // out: TxQ -JSS(refresh_interval); // in: UNL -JSS(refresh_interval_min); // out: ValidatorSites -JSS(regular_seed); // in/out: LedgerEntry -JSS(remaining); // out: ValidatorList -JSS(remote); // out: Logic.h -JSS(request); // RPC -JSS(requested); // out: Manifest -JSS(reservations); // out: Reservations -JSS(reserve_base); // out: NetworkOPs -JSS(reserve_base_xrp); // out: NetworkOPs -JSS(reserve_inc); // out: NetworkOPs -JSS(reserve_inc_xrp); // out: NetworkOPs -JSS(response); // websocket -JSS(result); // RPC -JSS(ripple_lines); // out: NetworkOPs -JSS(ripple_state); // in: LedgerEntr -JSS(ripplerpc); // ripple RPC version -JSS(role); // out: Ping.cpp +JSS(quote_asset); // in: get_aggregate_price +JSS(random); // out: Random +JSS(raw_meta); // out: AcceptedLedgerTx +JSS(receive_currencies); // out: AccountCurrencies +JSS(reference_level); // out: TxQ +JSS(refresh_interval); // in: UNL +JSS(refresh_interval_min); // out: ValidatorSites +JSS(regular_seed); // in/out: LedgerEntry +JSS(remaining); // out: ValidatorList +JSS(remote); // out: Logic.h +JSS(request); // RPC +JSS(requested); // out: Manifest +JSS(reservations); // out: Reservations +JSS(reserve_base); // out: NetworkOPs +JSS(reserve_base_xrp); // out: NetworkOPs +JSS(reserve_inc); // out: NetworkOPs +JSS(reserve_inc_xrp); // out: NetworkOPs +JSS(response); // websocket +JSS(result); // RPC +JSS(ripple_lines); // out: NetworkOPs +JSS(ripple_state); // in: LedgerEntr +JSS(ripplerpc); // ripple RPC version +JSS(role); // out: Ping.cpp JSS(rpc); -JSS(rt_accounts); // in: Subscribe, Unsubscribe +JSS(rt_accounts); // in: Subscribe, Unsubscribe JSS(running_duration_us); -JSS(search_depth); // in: RipplePathFind -JSS(searched_all); // out: Tx -JSS(secret); // in: TransactionSign, - // ValidationCreate, ValidationSeed, - // channel_authorize -JSS(seed); // -JSS(seed_hex); // in: WalletPropose, TransactionSign -JSS(send_currencies); // out: AccountCurrencies -JSS(send_max); // in: PathRequest, RipplePathFind -JSS(seq); // in: LedgerEntry; - // out: NetworkOPs, RPCSub, AccountOffers, - // ValidatorList, ValidatorInfo, Manifest -JSS(sequence); // in: UNL -JSS(sequence_count); // out: AccountInfo -JSS(server_domain); // out: NetworkOPs -JSS(server_state); // out: NetworkOPs -JSS(server_state_duration_us); // out: NetworkOPs -JSS(server_status); // out: NetworkOPs -JSS(server_version); // out: NetworkOPs -JSS(settle_delay); // out: AccountChannels -JSS(severity); // in: LogLevel -JSS(signature); // out: NetworkOPs, ChannelAuthorize -JSS(signature_verified); // out: ChannelVerify -JSS(signing_key); // out: NetworkOPs -JSS(signing_keys); // out: ValidatorList -JSS(signing_time); // out: NetworkOPs -JSS(signer_list); // in: AccountObjects -JSS(signer_lists); // in/out: AccountInfo -JSS(size); // out: get_aggregate_price -JSS(snapshot); // in: Subscribe -JSS(source_account); // in: PathRequest, RipplePathFind -JSS(source_amount); // in: PathRequest, RipplePathFind -JSS(source_currencies); // in: PathRequest, RipplePathFind -JSS(source_tag); // out: AccountChannels -JSS(stand_alone); // out: NetworkOPs -JSS(standard_deviation); // out: get_aggregate_price -JSS(start); // in: TxHistory +JSS(search_depth); // in: RipplePathFind +JSS(searched_all); // out: Tx +JSS(secret); // in: TransactionSign, + // ValidationCreate, ValidationSeed, + // channel_authorize +JSS(seed); // +JSS(seed_hex); // in: WalletPropose, TransactionSign +JSS(send_currencies); // out: AccountCurrencies +JSS(send_max); // in: PathRequest, RipplePathFind +JSS(seq); // in: LedgerEntry; + // out: NetworkOPs, RPCSub, AccountOffers, + // ValidatorList, ValidatorInfo, Manifest +JSS(sequence); // in: UNL +JSS(sequence_count); // out: AccountInfo +JSS(server_domain); // out: NetworkOPs +JSS(server_state); // out: NetworkOPs +JSS(server_state_duration_us);// out: NetworkOPs +JSS(server_status); // out: NetworkOPs +JSS(server_version); // out: NetworkOPs +JSS(settle_delay); // out: AccountChannels +JSS(severity); // in: LogLevel +JSS(signature); // out: NetworkOPs, ChannelAuthorize +JSS(signature_verified); // out: ChannelVerify +JSS(signing_key); // out: NetworkOPs +JSS(signing_keys); // out: ValidatorList +JSS(signing_time); // out: NetworkOPs +JSS(signer_list); // in: AccountObjects +JSS(signer_lists); // in/out: AccountInfo +JSS(size); // out: get_aggregate_price +JSS(snapshot); // in: Subscribe +JSS(source_account); // in: PathRequest, RipplePathFind +JSS(source_amount); // in: PathRequest, RipplePathFind +JSS(source_currencies); // in: PathRequest, RipplePathFind +JSS(source_tag); // out: AccountChannels +JSS(stand_alone); // out: NetworkOPs +JSS(standard_deviation); // out: get_aggregate_price +JSS(start); // in: TxHistory JSS(started); -JSS(state); // out: Logic.h, ServerState, LedgerData -JSS(state_accounting); // out: NetworkOPs -JSS(state_now); // in: Subscribe -JSS(status); // error -JSS(stop); // in: LedgerCleaner -JSS(stop_history_tx_only); // in: Unsubscribe, stop history tx stream -JSS(streams); // in: Subscribe, Unsubscribe -JSS(strict); // in: AccountCurrencies, AccountInfo -JSS(sub_index); // in: LedgerEntry -JSS(subcommand); // in: PathFind -JSS(success); // rpc -JSS(supported); // out: AmendmentTableImpl -JSS(sync_mode); // in: Submit -JSS(system_time_offset); // out: NetworkOPs -JSS(tag); // out: Peers -JSS(taker); // in: Subscribe, BookOffers -JSS(taker_gets); // in: Subscribe, Unsubscribe, BookOffers -JSS(taker_gets_funded); // out: NetworkOPs -JSS(taker_pays); // in: Subscribe, Unsubscribe, BookOffers -JSS(taker_pays_funded); // out: NetworkOPs -JSS(threshold); // in: Blacklist -JSS(ticket); // in: AccountObjects -JSS(ticket_count); // out: AccountInfo -JSS(ticket_seq); // in: LedgerEntry +JSS(state); // out: Logic.h, ServerState, LedgerData +JSS(state_accounting); // out: NetworkOPs +JSS(state_now); // in: Subscribe +JSS(status); // error +JSS(stop); // in: LedgerCleaner +JSS(stop_history_tx_only); // in: Unsubscribe, stop history tx stream +JSS(streams); // in: Subscribe, Unsubscribe +JSS(strict); // in: AccountCurrencies, AccountInfo +JSS(sub_index); // in: LedgerEntry +JSS(subcommand); // in: PathFind +JSS(success); // rpc +JSS(supported); // out: AmendmentTableImpl +JSS(sync_mode); // in: Submit +JSS(system_time_offset); // out: NetworkOPs +JSS(tag); // out: Peers +JSS(taker); // in: Subscribe, BookOffers +JSS(taker_gets); // in: Subscribe, Unsubscribe, BookOffers +JSS(taker_gets_funded); // out: NetworkOPs +JSS(taker_pays); // in: Subscribe, Unsubscribe, BookOffers +JSS(taker_pays_funded); // out: NetworkOPs +JSS(threshold); // in: Blacklist +JSS(ticket); // in: AccountObjects +JSS(ticket_count); // out: AccountInfo +JSS(ticket_seq); // in: LedgerEntry JSS(time); JSS(timeouts); // out: InboundLedger JSS(time_threshold); // in/out: Oracle aggregate @@ -774,10 +775,11 @@ JSS(vote_weight); // out: amm_info JSS(warning); // rpc: JSS(warnings); // out: server_info, server_state JSS(workers); -JSS(write_load); // out: GetCounts -JSS(xchain_owned_claim_id); // in: LedgerEntry, AccountObjects +JSS(write_load); // out: GetCounts +JSS(xchain_owned_claim_id); // in: LedgerEntry, AccountObjects JSS(xchain_owned_create_account_claim_id); // in: LedgerEntry -JSS(NegativeUNL); // out: ValidatorList; ledger type +JSS(NegativeUNL); // out: ValidatorList; ledger type +// clang-format on #undef JSS } // namespace jss From 3cc70033ff6127d3010c7a987612e1fd46cdc2a8 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 3 Oct 2024 09:01:20 -0400 Subject: [PATCH 08/58] Apply suggestions from code review Co-authored-by: Ed Hennis --- src/libxrpl/protocol/STAmount.cpp | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index a05c68a9532..cac9b7230dd 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -278,7 +278,7 @@ STAmount::xrp() const IOUAmount STAmount::iou() const { - if (native() || holds()) + if (native() || !holds()) Throw("Cannot return native STAmount as IOUAmount"); auto mantissa = static_cast(mValue); @@ -714,13 +714,10 @@ STAmount::canonicalize() { // N.B. do not move the overflow check to after the // multiplication - if (native()) - { - if (mValue > cMaxNativeN) - Throw( - "Native currency amount out of range"); - } - else if (mValue > maxMPTokenAmount) + if (native() && mValue > cMaxNativeN) + Throw( + "Native currency amount out of range"); + else if (!native() && mValue > maxMPTokenAmount) Throw("MPT amount out of range"); } mValue *= 10; @@ -728,13 +725,10 @@ STAmount::canonicalize() } } - if (native()) - { - if (mValue > cMaxNativeN) - Throw( - "Native currency amount out of range"); - } - else if (mValue > maxMPTokenAmount) + if (native() && mValue > cMaxNativeN) + Throw( + "Native currency amount out of range"); + else if (!native() && mValue > maxMPTokenAmount) Throw("MPT amount out of range"); return; @@ -1403,7 +1397,6 @@ mulRoundImpl( bool const xrp = isXRP(asset); - // TODO MPT if (v1.native() && v2.native() && xrp) { std::uint64_t minV = From 069273cb4f695f3304588e78d96ca47a1e2e22bb Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 3 Oct 2024 09:05:33 -0400 Subject: [PATCH 09/58] Fix clang format --- src/libxrpl/protocol/STAmount.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index cac9b7230dd..5cc542700f3 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -726,8 +726,7 @@ STAmount::canonicalize() } if (native() && mValue > cMaxNativeN) - Throw( - "Native currency amount out of range"); + Throw("Native currency amount out of range"); else if (!native() && mValue > maxMPTokenAmount) Throw("MPT amount out of range"); From 86bcebdae4b7841f9645609d5a2547f9141dc6cb Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 3 Oct 2024 09:43:13 -0400 Subject: [PATCH 10/58] Apply suggestions from code review use std::min, std::max Co-authored-by: Ed Hennis --- src/libxrpl/protocol/STAmount.cpp | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 5cc542700f3..fc47f8441c6 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1201,12 +1201,8 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset) } if (v1.holds() && v2.holds() && asset.holds()) { - std::uint64_t const minV = getMPTValue(v1) < getMPTValue(v2) - ? getMPTValue(v1) - : getMPTValue(v2); - std::uint64_t const maxV = getMPTValue(v1) < getMPTValue(v2) - ? getMPTValue(v2) - : getMPTValue(v1); + std::uint64_t const minV = std::min(getMPTValue(v1), getMPTValue(v2)); + std::uint64_t const maxV = std::max(getMPTValue(v1), getMPTValue(v2)); if (minV > 3000000000ull) // sqrt(cMaxNative) Throw("Asset value overflow"); @@ -1414,12 +1410,8 @@ mulRoundImpl( if (v1.holds() && v2.holds() && asset.holds()) { - std::uint64_t minV = (getMPTValue(v1) < getMPTValue(v2)) - ? getMPTValue(v1) - : getMPTValue(v2); - std::uint64_t maxV = (getMPTValue(v1) < getMPTValue(v2)) - ? getMPTValue(v2) - : getMPTValue(v1); + std::uint64_t const minV = std::min(getMPTValue(v1), getMPTValue(v2)); + std::uint64_t const maxV = std::max(getMPTValue(v1), getMPTValue(v2)); if (minV > 3000000000ull) // sqrt(cMaxNative) Throw("Asset value overflow"); From b9cb9ca47c20d1177ddb899fce6ba7e6d33d0197 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 3 Oct 2024 10:11:28 -0400 Subject: [PATCH 11/58] Address reviewer's feedback * Refactor getSNValue/getMPTValue * Use std::min, std::max * Fix min/max values for MPT --- src/libxrpl/protocol/STAmount.cpp | 54 ++++++++++++++----------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index fc47f8441c6..d9ffc743d4a 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -64,10 +64,11 @@ static const std::uint64_t tenTo17 = tenTo14 * 1000; //------------------------------------------------------------------------------ static std::int64_t -getSNValue(STAmount const& amount) +getInt64Value(STAmount const& amount, bool valid, const char* error) { - if (!amount.native()) - Throw("amount is not native!"); + if (!valid) + Throw(error); + assert(amount.exponent() == 0); auto ret = static_cast(amount.mantissa()); @@ -80,19 +81,16 @@ getSNValue(STAmount const& amount) } static std::int64_t -getMPTValue(STAmount const& amount) +getSNValue(STAmount const& amount) { - if (!amount.holds()) - Throw("amount is not MPT!"); - - auto ret = static_cast(amount.mantissa()); - - assert(static_cast(ret) == amount.mantissa()); - - if (amount.negative()) - ret = -ret; + return getInt64Value(amount, amount.native(), "amount is not native!"); +} - return ret; +static std::int64_t +getMPTValue(STAmount const& amount) +{ + return getInt64Value( + amount, amount.holds(), "amount is not MPT!"); } static bool @@ -1186,10 +1184,8 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset) if (v1.native() && v2.native() && isXRP(asset)) { - std::uint64_t const minV = - getSNValue(v1) < getSNValue(v2) ? getSNValue(v1) : getSNValue(v2); - std::uint64_t const maxV = - getSNValue(v1) < getSNValue(v2) ? getSNValue(v2) : getSNValue(v1); + std::uint64_t const minV = std::min(getSNValue(v1), getSNValue(v2)); + std::uint64_t const maxV = std::max(getSNValue(v1), getSNValue(v2)); if (minV > 3000000000ull) // sqrt(cMaxNative) Throw("Native value overflow"); @@ -1204,11 +1200,11 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset) std::uint64_t const minV = std::min(getMPTValue(v1), getMPTValue(v2)); std::uint64_t const maxV = std::max(getMPTValue(v1), getMPTValue(v2)); - if (minV > 3000000000ull) // sqrt(cMaxNative) - Throw("Asset value overflow"); + if (minV > 3037000499ull) // maxMPTokenAmount ~ 3037000499.98 + Throw("MPT value overflow"); - if (((maxV >> 32) * minV) > 2095475792ull) // cMaxNative / 2^32 - Throw("Asset value overflow"); + if (((maxV >> 32) * minV) > 2147483648ull) // maxMPTokenAmount / 2^32 + Throw("MPT value overflow"); return STAmount(asset, minV * maxV); } @@ -1394,10 +1390,8 @@ mulRoundImpl( if (v1.native() && v2.native() && xrp) { - std::uint64_t minV = - (getSNValue(v1) < getSNValue(v2)) ? getSNValue(v1) : getSNValue(v2); - std::uint64_t maxV = - (getSNValue(v1) < getSNValue(v2)) ? getSNValue(v2) : getSNValue(v1); + std::uint64_t minV = std::min(getSNValue(v1), getSNValue(v2)); + std::uint64_t maxV = std::max(getSNValue(v1), getSNValue(v2)); if (minV > 3000000000ull) // sqrt(cMaxNative) Throw("Native value overflow"); @@ -1413,11 +1407,11 @@ mulRoundImpl( std::uint64_t const minV = std::min(getMPTValue(v1), getMPTValue(v2)); std::uint64_t const maxV = std::max(getMPTValue(v1), getMPTValue(v2)); - if (minV > 3000000000ull) // sqrt(cMaxNative) - Throw("Asset value overflow"); + if (minV > 3037000499ull) // maxMPTokenAmount ~ 3037000499.98 + Throw("MPT value overflow"); - if (((maxV >> 32) * minV) > 2095475792ull) // cMaxNative / 2^32 - Throw("Asset value overflow"); + if (((maxV >> 32) * minV) > 2147483648ull) // maxMPTokenAmount / 2^32 + Throw("MPT value overflow"); return STAmount(asset, minV * maxV); } From 8af452ee272dd10e20ece7f7e9a03579e41926ce Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:07:43 -0400 Subject: [PATCH 12/58] remove unneeded fields (#36) --- include/xrpl/protocol/SField.h | 2 -- include/xrpl/protocol/TER.h | 5 +++-- include/xrpl/protocol/jss.h | 1 - src/libxrpl/protocol/LedgerFormats.cpp | 2 -- src/libxrpl/protocol/SField.cpp | 3 +-- src/libxrpl/protocol/STInteger.cpp | 3 +-- src/libxrpl/protocol/STParsedJSON.cpp | 3 +-- src/libxrpl/protocol/TER.cpp | 2 +- src/test/app/MPToken_test.cpp | 2 +- src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp | 2 +- src/xrpld/ledger/detail/View.cpp | 5 +---- 11 files changed, 10 insertions(+), 20 deletions(-) diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index b7391d2971b..849332ecc3c 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -470,7 +470,6 @@ 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 @@ -661,7 +660,6 @@ extern SField const sfXChainClaimProofSig; extern SField const sfXChainCreateAccountProofSig; extern SField const sfXChainClaimAttestationCollectionElement; extern SField const sfXChainCreateAccountAttestationCollectionElement; -extern SField const MPToken; // array of objects (common) // ARRAY/1 is reserved for end of array diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index b4dcd6ff875..70234fa994a 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -125,7 +125,6 @@ enum TEMcodes : TERUnderlyingType { temSEQ_AND_TICKET, temBAD_NFTOKEN_TRANSFER_FEE, - temBAD_MPTOKEN_TRANSFER_FEE, temBAD_AMM_TOKENS, @@ -139,7 +138,9 @@ enum TEMcodes : TERUnderlyingType { temEMPTY_DID, temARRAY_EMPTY, - temARRAY_TOO_LARGE + temARRAY_TOO_LARGE, + + temBAD_TRANSFER_FEE }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 1716be7e7ec..63ffab0604f 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -462,7 +462,6 @@ 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/protocol/LedgerFormats.cpp b/src/libxrpl/protocol/LedgerFormats.cpp index 71f04cd8d10..ebabad3322f 100644 --- a/src/libxrpl/protocol/LedgerFormats.cpp +++ b/src/libxrpl/protocol/LedgerFormats.cpp @@ -375,7 +375,6 @@ LedgerFormats::LedgerFormats() {sfAssetScale, soeDEFAULT}, {sfMaximumAmount, soeOPTIONAL}, {sfOutstandingAmount, soeREQUIRED}, - {sfLockedAmount, soeDEFAULT}, {sfMPTokenMetadata, soeOPTIONAL}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED} @@ -388,7 +387,6 @@ LedgerFormats::LedgerFormats() {sfAccount, soeREQUIRED}, {sfMPTokenIssuanceID, soeREQUIRED}, {sfMPTAmount, soeDEFAULT}, - {sfLockedAmount, soeDEFAULT}, {sfOwnerNode, soeREQUIRED}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED} diff --git a/src/libxrpl/protocol/SField.cpp b/src/libxrpl/protocol/SField.cpp index dc4bfb69a21..85aca815acc 100644 --- a/src/libxrpl/protocol/SField.cpp +++ b/src/libxrpl/protocol/SField.cpp @@ -196,8 +196,7 @@ CONSTRUCT_TYPED_SFIELD(sfXChainAccountClaimCount, "XChainAccountClaimCount", U 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); +CONSTRUCT_TYPED_SFIELD(sfMPTAmount, "MPTAmount", UINT64, 26); // 128-bit CONSTRUCT_TYPED_SFIELD(sfEmailHash, "EmailHash", UINT128, 1); diff --git a/src/libxrpl/protocol/STInteger.cpp b/src/libxrpl/protocol/STInteger.cpp index 148a05d2ab6..2db4ff32c34 100644 --- a/src/libxrpl/protocol/STInteger.cpp +++ b/src/libxrpl/protocol/STInteger.cpp @@ -206,8 +206,7 @@ Json::Value STUInt64::getJson(JsonOptions) const }; if (auto const& fName = getFName(); fName == sfMaximumAmount || - fName == sfOutstandingAmount || fName == sfLockedAmount || - fName == sfMPTAmount) + fName == sfOutstandingAmount || fName == sfMPTAmount) { return convertToString(value_, 10); // Convert to base 10 } diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index 7e6b8ff5975..1ca4e83d9e6 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -399,8 +399,7 @@ parseLeaf( std::uint64_t val; bool const useBase10 = field == sfMaximumAmount || - field == sfOutstandingAmount || - field == sfLockedAmount || field == sfMPTAmount; + field == sfOutstandingAmount || field == sfMPTAmount; // if the field is amount, serialize as base 10 auto [p, ec] = std::from_chars( diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 45da1f30e56..9fead8ae339 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -202,7 +202,6 @@ 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."), @@ -211,6 +210,7 @@ transResults() MAKE_ERROR(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT, "Malformed: Bad reward amount."), MAKE_ERROR(temARRAY_EMPTY, "Malformed: Array is empty."), MAKE_ERROR(temARRAY_TOO_LARGE, "Malformed: Array is too large."), + MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."), MAKE_ERROR(terRETRY, "Retry transaction."), MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."), diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index df37651d8a4..7516c124edb 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -66,7 +66,7 @@ class MPToken_test : public beast::unit_test::suite .transferFee = maxTransferFee + 1, .metadata = "test", .flags = tfMPTCanTransfer, - .err = temBAD_MPTOKEN_TRANSFER_FEE}); + .err = temBAD_TRANSFER_FEE}); // tries to set a txfee while not enabling transfer mptAlice.create( diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp index de01341e296..881e24bfa20 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -40,7 +40,7 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) if (auto const fee = ctx.tx[~sfTransferFee]) { if (fee > maxTransferFee) - return temBAD_MPTOKEN_TRANSFER_FEE; + return temBAD_TRANSFER_FEE; // If a non-zero TransferFee is set then the tfTransferable flag // must also be set. diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 6e1f4536fcc..51810286813 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -329,10 +329,7 @@ accountHolds( amount.clear(mptIssue); else { - auto const amt = sleMpt->getFieldU64(sfMPTAmount); - auto const locked = sleMpt->getFieldU64(sfLockedAmount); - if (amt > locked) - amount = STAmount{mptIssue, amt - locked}; + amount = STAmount{mptIssue, sleMpt->getFieldU64(sfMPTAmount)}; // only if auth check is needed, as it needs to do an additional read // operation From fab0c783f367c0094631b7afb2b1a5865d40bbe6 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:10:24 -0400 Subject: [PATCH 13/58] rename codes (#37) --- include/xrpl/protocol/TER.h | 6 +----- src/libxrpl/protocol/TER.cpp | 6 +----- src/test/app/MPToken_test.cpp | 14 +++++++------- src/test/jtx/impl/mpt.cpp | 2 +- src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 2 +- src/xrpld/app/tx/detail/Payment.cpp | 4 ++-- src/xrpld/ledger/detail/View.cpp | 6 +++--- 7 files changed, 16 insertions(+), 24 deletions(-) diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 70234fa994a..214b37698f3 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -342,11 +342,7 @@ enum TECcodes : TERUnderlyingType { tecTOKEN_PAIR_NOT_FOUND = 189, tecARRAY_EMPTY = 190, tecARRAY_TOO_LARGE = 191, - tecMPTOKEN_EXISTS = 192, - tecMPT_MAX_AMOUNT_EXCEEDED = 193, - tecMPT_LOCKED = 194, - tecMPT_NOT_SUPPORTED = 195, - tecMPT_ISSUANCE_NOT_FOUND = 196 + tecLOCKED = 192, }; //------------------------------------------------------------------------------ diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 9fead8ae339..788b3a86152 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -85,7 +85,6 @@ 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."), @@ -116,10 +115,7 @@ 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(tecMPT_ISSUANCE_NOT_FOUND, "The MPTokenIssuance object is not found"), + MAKE_ERROR(tecLOCKED, "Fund is locked."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 7516c124edb..3420f289dfd 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -280,7 +280,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.authorize({.account = &bob, .holderCount = 1}); // bob cannot create the mptoken the second time - mptAlice.authorize({.account = &bob, .err = tecMPTOKEN_EXISTS}); + mptAlice.authorize({.account = &bob, .err = tecDUPLICATE}); // Check that bob cannot delete MPToken when his balance is // non-zero @@ -766,8 +766,8 @@ class MPToken_test : public beast::unit_test::suite // 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); + mptAlice.pay(bob, carol, 1, tecLOCKED); + mptAlice.pay(carol, bob, 2, tecLOCKED); // Issuer can send mptAlice.pay(alice, bob, 3); // Holder can send back to issuer @@ -779,8 +779,8 @@ class MPToken_test : public beast::unit_test::suite 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); + mptAlice.pay(bob, carol, 5, tecLOCKED); + mptAlice.pay(carol, bob, 6, tecLOCKED); // Issuer can send mptAlice.pay(alice, bob, 7); // Holder can send back to issuer @@ -1030,7 +1030,7 @@ class MPToken_test : public beast::unit_test::suite // alice tries to send bob fund after issuance is destroy, should // fail. - mptAlice.pay(alice, bob, 100, tecMPT_ISSUANCE_NOT_FOUND); + mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND); } // Issuer fails trying to send to some who doesn't own MPT for a @@ -1047,7 +1047,7 @@ class MPToken_test : public beast::unit_test::suite // alice tries to send bob who doesn't own the MPT after issuance is // destroyed, it should fail - mptAlice.pay(alice, bob, 100, tecMPT_ISSUANCE_NOT_FOUND); + mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND); } // Issuers issues maximum amount of MPT to a holder, the holder should diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 6af502ee2cc..0f23990f485 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -186,7 +186,7 @@ MPTTester::authorize(MPTAuthorize const& arg) arg.account != nullptr && *arg.account != issuer_ && arg.flags.value_or(0) == 0 && issuanceKey_) { - if (result == tecMPTOKEN_EXISTS) + if (result == tecDUPLICATE) { // Verify that MPToken already exists env_.require(requireAny([&]() -> bool { diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index 1e9871e0ae3..7aea2020a47 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -91,7 +91,7 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) // if holder wants to use and create a mpt if (sleMpt) - return tecMPTOKEN_EXISTS; + return tecDUPLICATE; return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 08742ff0bbf..b20d597f574 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -681,7 +681,7 @@ applyHelper(ApplyContext& ctx, XRPAmount const&, XRPAmount const&) // - issuer can send to holder if (isFrozen(ctx.view(), account, mpt) || isFrozen(ctx.view(), uDstAccountID, mpt)) - return tecMPT_LOCKED; + return tecLOCKED; auto const sendMax(ctx.tx[~sfSendMax]); // If the transfer fee is included then SendMax has to be included @@ -698,7 +698,7 @@ applyHelper(ApplyContext& ctx, XRPAmount const&, XRPAmount const&) accountSendMPT(pv, account, uDstAccountID, saDstAmount, ctx.journal); if (res == tesSUCCESS) pv.apply(ctx.rawView()); - else if (res == tecINSUFFICIENT_FUNDS || res == tecMPT_MAX_AMOUNT_EXCEEDED) + else if (res == tecINSUFFICIENT_FUNDS || res == tecPATH_DRY) res = tecPATH_PARTIAL; return res; diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 51810286813..19fc424a999 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -1363,7 +1363,7 @@ rippleSendMPT( keylet::mptIssuance(saAmount.get().getMptID()); auto const sle = view.peek(mptID); if (!sle) - return tecMPT_ISSUANCE_NOT_FOUND; + return tecOBJECT_NOT_FOUND; auto const sendAmount = saAmount.mpt().value(); auto const maximumAmount = @@ -1371,7 +1371,7 @@ rippleSendMPT( if (sendAmount > maximumAmount || sle->getFieldU64(sfOutstandingAmount) > maximumAmount - sendAmount) - return tecMPT_MAX_AMOUNT_EXCEEDED; + return tecPATH_DRY; } // Direct send: redeeming IOUs and/or sending own IOUs. @@ -1885,7 +1885,7 @@ rippleMPTCredit( auto const mptID = keylet::mptIssuance(saAmount.get().getMptID()); auto const issuer = saAmount.getIssuer(); if (!view.exists(mptID)) - return tecMPT_ISSUANCE_NOT_FOUND; + return tecOBJECT_NOT_FOUND; if (uSenderID == issuer) { if (auto sle = view.peek(mptID)) From ea887bfc93b22f6a1225f2843a11131e04f0dfd2 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 3 Oct 2024 14:05:06 -0400 Subject: [PATCH 14/58] Address reviewer feedback --- include/xrpl/basics/MPTAmount.h | 3 -- include/xrpl/protocol/Asset.h | 39 ++++++++++++----- include/xrpl/protocol/Indexes.h | 8 ++-- include/xrpl/protocol/Issue.h | 5 ++- include/xrpl/protocol/MPTIssue.h | 14 ++++--- include/xrpl/protocol/STAmount.h | 4 +- src/libxrpl/protocol/Asset.cpp | 42 ++++--------------- src/libxrpl/protocol/Indexes.cpp | 6 +-- src/libxrpl/protocol/Issue.cpp | 6 +++ src/libxrpl/protocol/STAmount.cpp | 12 +++--- src/test/app/MPToken_test.cpp | 28 ++++++------- src/test/jtx/impl/mpt.cpp | 2 +- .../app/tx/detail/MPTokenIssuanceCreate.cpp | 2 +- src/xrpld/rpc/detail/MPTokenIssuanceID.cpp | 2 +- src/xrpld/rpc/detail/RPCHelpers.cpp | 4 +- src/xrpld/rpc/handlers/LedgerData.cpp | 2 +- 16 files changed, 89 insertions(+), 90 deletions(-) diff --git a/include/xrpl/basics/MPTAmount.h b/include/xrpl/basics/MPTAmount.h index 06487194a47..bd523c5abd7 100644 --- a/include/xrpl/basics/MPTAmount.h +++ b/include/xrpl/basics/MPTAmount.h @@ -88,9 +88,6 @@ class MPTAmount : private boost::totally_ordered, constexpr value_type value() const; - friend std::istream& - operator>>(std::istream& s, MPTAmount& val); - static MPTAmount minPositiveAmount(); }; diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index c42fdac3203..c23e0f683e2 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -30,11 +30,10 @@ template concept ValidIssueType = std::is_same_v || std::is_same_v; -/* Asset is a variant of Issue (XRP and IOU) and MPTIssue (MPT). - * It enables handling of different issues when either one is expected. - * For instance, it extends STAmount class to support either issue - * in a general way. It handles specifics such arithmetics and serialization - * depending on specific issue type held by Asset. +/* Asset is an abstraction of three different issue types: XRP, IOU, MPT. + * For historical reasons, two issue types XRP and IOU are wrapped in Issue + * type. Asset replaces Issue where any issue type is expected. For instance, + * STAmount replaces Issue with Asset to represent any issue amount. */ class Asset { @@ -45,15 +44,27 @@ class Asset public: Asset() = default; - Asset(Issue const& issue); + Asset(Issue const& issue) : issue_(issue) + { + } - Asset(MPTIssue const& mptIssue); + Asset(MPTIssue const& mptIssue) : issue_(mptIssue) + { + } - Asset(MPTID const& issuanceID); + Asset(MPTID const& issuanceID) : issue_(MPTIssue{issuanceID}) + { + } - explicit operator Issue() const; + explicit operator Issue() const + { + return get(); + } - explicit operator MPTIssue() const; + explicit operator MPTIssue() const + { + return get(); + } AccountID const& getIssuer() const; @@ -79,6 +90,12 @@ class Asset void setJson(Json::Value& jv) const; + bool + native() const + { + return holds() && get().native(); + } + friend constexpr bool operator==(Asset const& lhs, Asset const& rhs); @@ -141,7 +158,7 @@ operator!=(Asset const& lhs, Asset const& rhs) inline bool isXRP(Asset const& asset) { - return asset.holds() && isXRP(asset.get()); + return asset.native(); } std::string diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 63fa34ada37..8249eabb43a 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -288,7 +288,7 @@ Keylet oracle(AccountID const& account, std::uint32_t const& documentID) noexcept; Keylet -mptIssuance(AccountID const& issuer, std::uint32_t seq) noexcept; +mptIssuance(std::uint32_t seq, AccountID const& issuer) noexcept; Keylet mptIssuance(MPTID const& issuanceID) noexcept; @@ -303,9 +303,9 @@ Keylet mptoken(MPTID const& issuanceID, AccountID const& holder) noexcept; inline Keylet -mptoken(uint256 const& issuanceKey) +mptoken(uint256 const& mptokenKey) { - return {ltMPTOKEN, issuanceKey}; + return {ltMPTOKEN, mptokenKey}; } Keylet @@ -352,7 +352,7 @@ std::array, 6> const directAccountKeylets{ {&keylet::did, jss::DID, true}}}; MPTID -makeMptID(AccountID const& account, std::uint32_t sequence); +makeMptID(std::uint32_t sequence, AccountID const& account); } // namespace ripple diff --git a/include/xrpl/protocol/Issue.h b/include/xrpl/protocol/Issue.h index aa5f5174485..335dd91354a 100644 --- a/include/xrpl/protocol/Issue.h +++ b/include/xrpl/protocol/Issue.h @@ -55,6 +55,9 @@ class Issue void setJson(Json::Value& jv) const; + + bool + native() const; }; bool @@ -126,7 +129,7 @@ noIssue() inline bool isXRP(Issue const& issue) { - return issue == xrpIssue(); + return issue.native(); } } // namespace ripple diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index d5bcc7f0751..c681bdc0757 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -25,11 +25,9 @@ namespace ripple { -/* MPTIssue represents a Multi Purpose Token (MPT) and enables handling of - * either Issue or MPTIssue tokens by Asset and STAmount. MPT is identified - * by MPTID, which is a 192-bit concatenation of a 32-bit account sequence - * number at the time of MPT creation and a 160-bit account id. - * The sequence number is stored in big endian order. +/* Adapt MPTID to provide the same interface as Issue. Enables using static + * polymorphism by Asset and other classes. MPTID is a 192-bit concatenation + * of a 32-bit account sequence and a 160-bit account id. */ class MPTIssue { @@ -58,6 +56,12 @@ class MPTIssue friend constexpr bool operator!=(MPTIssue const& lhs, MPTIssue const& rhs); + + bool + native() const + { + return false; + } }; constexpr bool diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index a8efb05cd7b..bc5a3ae674f 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -447,7 +447,7 @@ STAmount::exponent() const noexcept inline bool STAmount::native() const noexcept { - return isXRP(mAsset); + return mAsset.native(); } template @@ -679,7 +679,7 @@ getRate(STAmount const& offerOut, STAmount const& offerIn); inline bool isXRP(STAmount const& amount) { - return isXRP(amount.asset()); + return amount.native(); } // Since `canonicalize` does not have access to a ledger, this is needed to put diff --git a/src/libxrpl/protocol/Asset.cpp b/src/libxrpl/protocol/Asset.cpp index 4d0ce4c4750..b265d4a504a 100644 --- a/src/libxrpl/protocol/Asset.cpp +++ b/src/libxrpl/protocol/Asset.cpp @@ -23,59 +23,31 @@ namespace ripple { -Asset::Asset(Issue const& issue) : issue_(issue) -{ -} - -Asset::Asset(MPTIssue const& mptIssue) : issue_(mptIssue) -{ -} - -Asset::Asset(MPTID const& issuanceID) : issue_(MPTIssue{issuanceID}) -{ -} - -Asset::operator Issue() const -{ - return get(); -} - -Asset::operator MPTIssue() const -{ - return get(); -} - AccountID const& Asset::getIssuer() const { - if (holds()) - return get().getIssuer(); - return get().getIssuer(); + return std::visit( + [&](auto&& issue) -> AccountID const& { return issue.getIssuer(); }, + issue_); } std::string Asset::getText() const { - if (holds()) - return get().getText(); - return get().getText(); + return std::visit([&](auto&& issue) { return issue.getText(); }, issue_); } void Asset::setJson(Json::Value& jv) const { - if (holds()) - get().setJson(jv); - else - get().setJson(jv); + std::visit([&](auto&& issue) { issue.setJson(jv); }, issue_); } std::string to_string(Asset const& asset) { - if (asset.holds()) - return to_string(asset.get()); - return to_string(asset.get()); + return std::visit( + [&](auto const& issue) { return to_string(issue); }, asset.value()); } bool diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index a1f79752b38..9a537eaaf2b 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -138,7 +138,7 @@ getTicketIndex(AccountID const& account, SeqProxy ticketSeq) } MPTID -makeMptID(AccountID const& account, std::uint32_t sequence) +makeMptID(std::uint32_t sequence, AccountID const& account) { MPTID u; sequence = boost::endian::native_to_big(sequence); @@ -464,9 +464,9 @@ oracle(AccountID const& account, std::uint32_t const& documentID) noexcept } Keylet -mptIssuance(AccountID const& issuer, std::uint32_t seq) noexcept +mptIssuance(std::uint32_t seq, AccountID const& issuer) noexcept { - return mptIssuance(makeMptID(issuer, seq)); + return mptIssuance(makeMptID(seq, issuer)); } Keylet diff --git a/src/libxrpl/protocol/Issue.cpp b/src/libxrpl/protocol/Issue.cpp index 59ca3b9f395..ff6b2b8767a 100644 --- a/src/libxrpl/protocol/Issue.cpp +++ b/src/libxrpl/protocol/Issue.cpp @@ -57,6 +57,12 @@ Issue::setJson(Json::Value& jv) const jv[jss::issuer] = toBase58(account); } +bool +Issue::native() const +{ + return *this == xrpIssue(); +} + bool isConsistent(Issue const& ac) { diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index d9ffc743d4a..46be391e592 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -839,7 +839,7 @@ amountFromString(Asset const& asset, std::string const& amount) bool negative = (match[1].matched && (match[1] == "-")); // Can't specify XRP or MPT using fractional representation - if ((isXRP(asset) || asset.holds()) && match[3].matched) + if ((asset.native() || asset.holds()) && match[3].matched) Throw( "XRP and MPT must be specified as integral amount."); @@ -962,7 +962,7 @@ amountFromJson(SField const& name, Json::Value const& v) if (!issuer.isString() || !to_issuer(issue.account, issuer.asString())) Throw("invalid issuer"); - if (isXRP(issue)) + if (issue.native()) Throw("invalid issuer"); asset = issue; } @@ -1182,7 +1182,7 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset) if (v1 == beast::zero || v2 == beast::zero) return STAmount(asset); - if (v1.native() && v2.native() && isXRP(asset)) + if (v1.native() && v2.native() && asset.native()) { std::uint64_t const minV = std::min(getSNValue(v1), getSNValue(v2)); std::uint64_t const maxV = std::max(getSNValue(v1), getSNValue(v2)); @@ -1386,7 +1386,7 @@ mulRoundImpl( if (v1 == beast::zero || v2 == beast::zero) return {asset}; - bool const xrp = isXRP(asset); + bool const xrp = asset.native(); if (v1.native() && v2.native() && xrp) { @@ -1557,7 +1557,7 @@ divRoundImpl( if (resultNegative != roundUp) canonicalizeRound( - isXRP(asset) || asset.holds(), amount, offset, roundUp); + asset.native() || asset.holds(), amount, offset, roundUp); STAmount result = [&]() { // If appropriate, tell Number the rounding mode we are using. @@ -1571,7 +1571,7 @@ divRoundImpl( if (roundUp && !resultNegative && !result) { - if (isXRP(asset) || asset.holds()) + if (asset.native() || asset.holds()) { // return the smallest value above zero amount = 1; diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 3420f289dfd..e8a6435a988 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -152,7 +152,7 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features - featureMPTokensV1}; MPTTester mptAlice(env, alice); - auto const id = makeMptID(alice, env.seq(alice)); + auto const id = makeMptID(env.seq(alice), alice); mptAlice.destroy({.id = id, .ownerCount = 0, .err = temDISABLED}); env.enableFeature(featureMPTokensV1); @@ -167,7 +167,7 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {&bob}}); mptAlice.destroy( - {.id = makeMptID(alice.id(), env.seq(alice)), + {.id = makeMptID(env.seq(alice), alice), .ownerCount = 0, .err = tecOBJECT_NOT_FOUND}); @@ -224,7 +224,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.authorize( {.account = &bob, - .id = makeMptID(alice, env.seq(alice)), + .id = makeMptID(env.seq(alice), alice), .err = temDISABLED}); } @@ -249,7 +249,7 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {&bob}}); - auto const id = makeMptID(alice, env.seq(alice)); + auto const id = makeMptID(env.seq(alice), alice); mptAlice.authorize( {.holder = &bob, .id = id, .err = tecOBJECT_NOT_FOUND}); @@ -474,7 +474,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.set( {.account = &bob, - .id = makeMptID(alice, env.seq(alice)), + .id = makeMptID(env.seq(alice), alice), .err = temDISABLED}); env.enableFeature(featureMPTokensV1); @@ -551,7 +551,7 @@ class MPToken_test : public beast::unit_test::suite // alice trying to set when the mptissuance doesn't exist yet mptAlice.set( - {.id = makeMptID(alice.id(), env.seq(alice)), + {.id = makeMptID(env.seq(alice), alice), .flags = tfMPTLock, .err = tecOBJECT_NOT_FOUND}); @@ -845,7 +845,7 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; env.fund(XRP(1'000), alice, bob); - STAmount mpt{MPTIssue{makeMptID(alice.id(), 1)}, UINT64_C(100)}; + STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)}; Json::Value jv; jv[jss::secret] = alice.name(); jv[jss::tx_json] = pay(alice, bob, mpt); @@ -935,7 +935,7 @@ class MPToken_test : public beast::unit_test::suite env.fund(XRP(1'000), alice); env.fund(XRP(1'000), bob); - STAmount mpt{MPTIssue{makeMptID(alice.id(), 1)}, UINT64_C(100)}; + STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)}; env(pay(alice, bob, mpt), ter(temDISABLED)); } @@ -949,7 +949,7 @@ class MPToken_test : public beast::unit_test::suite env.fund(XRP(1'000), alice); env.fund(XRP(1'000), carol); - STAmount mpt{MPTIssue{makeMptID(alice.id(), 1)}, UINT64_C(100)}; + STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)}; Json::Value jv; jv[jss::secret] = alice.name(); @@ -1103,7 +1103,7 @@ class MPToken_test : public beast::unit_test::suite Account const alice("alice"); auto const USD = alice["USD"]; Account const carol("carol"); - MPTIssue issue(makeMptID(alice.id(), 1)); + MPTIssue issue(makeMptID(1, alice)); STAmount mpt{issue, UINT64_C(100)}; auto const jvb = bridge(alice, USD, alice, USD); for (auto const& feature : {features, features - featureMPTokensV1}) @@ -1422,7 +1422,7 @@ class MPToken_test : public beast::unit_test::suite auto const USD = alice["USD"]; auto const mpt = ripple::test::jtx::MPT( - alice.name(), makeMptID(alice.id(), env.seq(alice))); + alice.name(), makeMptID(env.seq(alice), alice)); env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED)); env.close(); @@ -1445,7 +1445,7 @@ class MPToken_test : public beast::unit_test::suite auto const USD = alice["USD"]; auto const mpt = ripple::test::jtx::MPT( - alice.name(), makeMptID(alice.id(), env.seq(alice))); + alice.name(), makeMptID(env.seq(alice), alice)); // clawing back IOU from a MPT holder fails env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED)); @@ -1504,7 +1504,7 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {&bob}}); auto const fakeMpt = ripple::test::jtx::MPT( - alice.name(), makeMptID(alice.id(), env.seq(alice))); + alice.name(), makeMptID(env.seq(alice), alice)); // issuer tries to clawback MPT where issuance doesn't exist env(claw(alice, fakeMpt(5), bob), ter(tecOBJECT_NOT_FOUND)); @@ -1543,7 +1543,7 @@ class MPToken_test : public beast::unit_test::suite env.close(); auto const mpt = ripple::test::jtx::MPT( - alice.name(), makeMptID(alice.id(), env.seq(alice))); + alice.name(), makeMptID(env.seq(alice), alice)); Json::Value jv = claw(alice, mpt(1), bob); jv[jss::Amount][jss::value] = to_string(maxMPTokenAmount + 1); diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 0f23990f485..c4d16f1317b 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -85,7 +85,7 @@ MPTTester::create(const MPTCreate& arg) { if (issuanceKey_) Throw("MPT can't be reused"); - id_ = makeMptID(issuer_.id(), env_.seq(issuer_)); + id_ = makeMptID(env_.seq(issuer_), issuer_); issuanceKey_ = keylet::mptIssuance(*id_).key; Json::Value jv; jv[sfAccount.jsonName] = issuer_.human(); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp index 881e24bfa20..a164584aa81 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -82,7 +82,7 @@ MPTokenIssuanceCreate::create( return tecINSUFFICIENT_RESERVE; auto const mptIssuanceKeylet = - keylet::mptIssuance(args.account, args.sequence); + keylet::mptIssuance(args.sequence, args.account); // create the MPTokenIssuance { diff --git a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp index 4c6a0308693..551f88deb55 100644 --- a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp +++ b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp @@ -59,7 +59,7 @@ getIDFromCreatedIssuance(TxMeta const& transactionMeta) auto const& mptNode = node.peekAtField(sfNewFields).downcast(); return makeMptID( - mptNode.getAccountID(sfIssuer), mptNode.getFieldU32(sfSequence)); + mptNode.getFieldU32(sfSequence), mptNode.getAccountID(sfIssuer)); } return std::nullopt; diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index 99b9e21fed4..f3df4be263e 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -288,8 +288,8 @@ getAccountObjects( auto sleJson = sleNode->getJson(JsonOptions::none); if (sleNode->getType() == ltMPTOKEN_ISSUANCE) sleJson[jss::mpt_issuance_id] = to_string(makeMptID( - sleNode->getAccountID(sfIssuer), - (*sleNode)[sfSequence])); + (*sleNode)[sfSequence], + sleNode->getAccountID(sfIssuer))); jvObjects.append(sleJson); } diff --git a/src/xrpld/rpc/handlers/LedgerData.cpp b/src/xrpld/rpc/handlers/LedgerData.cpp index 16f35100c8f..573f3f62f88 100644 --- a/src/xrpld/rpc/handlers/LedgerData.cpp +++ b/src/xrpld/rpc/handlers/LedgerData.cpp @@ -127,7 +127,7 @@ doLedgerData(RPC::JsonContext& context) if (sle->getType() == ltMPTOKEN_ISSUANCE) entry[jss::mpt_issuance_id] = to_string(makeMptID( - sle->getAccountID(sfIssuer), (*sle)[sfSequence])); + (*sle)[sfSequence], sle->getAccountID(sfIssuer))); } } } From d9fc97f88d23e1a18d834e5b88064d8e3cbb4863 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 3 Oct 2024 20:16:56 -0400 Subject: [PATCH 15/58] Check Payment's sendMax and amount have the same issue --- src/test/app/MPToken_test.cpp | 6 ++++++ src/xrpld/app/tx/detail/Payment.cpp | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index e8a6435a988..c96520e202d 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -988,6 +988,12 @@ class MPToken_test : public beast::unit_test::suite env(pay(alice, carol, XRP(100)), sendmax(MPT(100)), ter(temMALFORMED)); + // sendmax and amount are different MPT issue + test::jtx::MPT const MPT1( + "MPT", makeMptID(env.seq(alice) + 10, alice)); + env(pay(alice, carol, MPT1(100)), + sendmax(MPT(100)), + ter(temMALFORMED)); } // build_path is invalid if MPT diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index b20d597f574..16cdc515a08 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -224,8 +224,9 @@ preflightHelper(PreflightContext const& ctx) if (ctx.tx.isFieldPresent(sfDeliverMin) || ctx.tx.isFieldPresent(sfPaths)) return temMALFORMED; - if (auto const sendMax = ctx.tx[~sfSendMax]; - sendMax && !sendMax->holds()) + if (auto const sendMax = ctx.tx[~sfSendMax]; sendMax && + (!sendMax->holds() || + sendMax->asset() != ctx.tx[sfAmount].asset())) return temMALFORMED; auto& tx = ctx.tx; From 98d419f4cc0c5a2310ff21b1e2324239f162de14 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Fri, 4 Oct 2024 07:56:16 -0400 Subject: [PATCH 16/58] Apply suggestions from code review Co-authored-by: Ed Hennis --- src/libxrpl/protocol/STAmount.cpp | 4 ++-- src/libxrpl/protocol/STTx.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 46be391e592..a3878d53825 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1200,7 +1200,7 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset) std::uint64_t const minV = std::min(getMPTValue(v1), getMPTValue(v2)); std::uint64_t const maxV = std::max(getMPTValue(v1), getMPTValue(v2)); - if (minV > 3037000499ull) // maxMPTokenAmount ~ 3037000499.98 + if (minV > 3037000499ull) // sqrt(maxMPTokenAmount) ~ 3037000499.98 Throw("MPT value overflow"); if (((maxV >> 32) * minV) > 2147483648ull) // maxMPTokenAmount / 2^32 @@ -1407,7 +1407,7 @@ mulRoundImpl( std::uint64_t const minV = std::min(getMPTValue(v1), getMPTValue(v2)); std::uint64_t const maxV = std::max(getMPTValue(v1), getMPTValue(v2)); - if (minV > 3037000499ull) // maxMPTokenAmount ~ 3037000499.98 + if (minV > 3037000499ull) // sqrt(maxMPTokenAmount) ~ 3037000499.98 Throw("MPT value overflow"); if (((maxV >> 32) * minV) > 2147483648ull) // maxMPTokenAmount / 2^32 diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index cbb49e26b49..7bd25246c53 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -560,7 +560,7 @@ invalidMPTAmountInTx(STObject const& tx) field.getSType() == STI_AMOUNT && static_cast(field).holds()) { - if (e.supportMPT() == soeMPTNotSupported) + if (e.supportMPT() != soeMPTSupported) return true; } } From 3b7861dc585495a16133374d1904225770af21e2 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Mon, 7 Oct 2024 07:57:23 -0400 Subject: [PATCH 17/58] Address reviewer's feedback --- include/xrpl/basics/Number.h | 5 + include/xrpl/protocol/Asset.h | 13 +-- include/xrpl/protocol/detail/STVar.h | 23 +++++ src/libxrpl/protocol/STVar.cpp | 149 ++++++++++----------------- 4 files changed, 87 insertions(+), 103 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 89c15f7d1b8..bec24d21428 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -90,6 +90,11 @@ class Number static constexpr Number lowest() noexcept; + /** Conversions to Number are implicit and conversions away from Number + * are explicit. This design encourages and facilitates the use of Number + * as the preferred type for floating point arithmetic as it makes + * "mixed mode" more convenient, e.g. MPTAmount + Number. + */ 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 diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index c23e0f683e2..e936f7569df 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -44,6 +44,9 @@ class Asset public: Asset() = default; + /** Conversions to Asset are implicit and conversions to specific issue + * type are explicit. This design facilitates the use of Asset. + */ Asset(Issue const& issue) : issue_(issue) { } @@ -56,16 +59,6 @@ class Asset { } - explicit operator Issue() const - { - return get(); - } - - explicit operator MPTIssue() const - { - return get(); - } - AccountID const& getIssuer() const; diff --git a/include/xrpl/protocol/detail/STVar.h b/include/xrpl/protocol/detail/STVar.h index bee48aa53f6..4accfb3e20b 100644 --- a/include/xrpl/protocol/detail/STVar.h +++ b/include/xrpl/protocol/detail/STVar.h @@ -23,8 +23,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -44,6 +46,19 @@ struct nonPresentObject_t extern defaultObject_t defaultObject; extern nonPresentObject_t nonPresentObject; +// Concept to constrain STVar constructors, which +// instantiate ST* types from SerializedTypeID +// clang-format off +template +concept ValidConstructSTArgs = + (std::is_same_v< + std::tuple...>, + std::tuple> || + std::is_same_v< + std::tuple...>, + std::tuple>); +// clang-format on + // "variant" that can hold any type of serialized object // and includes a small-object allocation optimization. class STVar @@ -131,6 +146,14 @@ class STVar p_ = new (&d_) T(std::forward(args)...); } + /** Construct requested Serializable Type according to id. + * The variadic args are: (SField), or (SerialIter, SField). + * depth is ignored in former case. + */ + template + requires ValidConstructSTArgs void + constructST(SerializedTypeID id, int depth, Args&&... arg); + bool on_heap() const { diff --git a/src/libxrpl/protocol/STVar.cpp b/src/libxrpl/protocol/STVar.cpp index 0cb52b5d24e..aa693308600 100644 --- a/src/libxrpl/protocol/STVar.cpp +++ b/src/libxrpl/protocol/STVar.cpp @@ -115,147 +115,110 @@ STVar::STVar(SerialIter& sit, SField const& name, int depth) { if (depth > 10) Throw("Maximum nesting depth of STVar exceeded"); - switch (name.fieldType) - { - case STI_NOTPRESENT: - construct(name); - return; - case STI_UINT8: - construct(sit, name); - return; - case STI_UINT16: - construct(sit, name); - return; - case STI_UINT32: - construct(sit, name); - return; - case STI_UINT64: - construct(sit, name); - return; - case STI_AMOUNT: - construct(sit, name); - return; - case STI_UINT128: - construct(sit, name); - return; - case STI_UINT160: - construct(sit, name); - return; - case STI_UINT192: - construct(sit, name); - return; - case STI_UINT256: - construct(sit, name); - return; - case STI_VECTOR256: - construct(sit, name); - return; - case STI_VL: - construct(sit, name); - return; - case STI_ACCOUNT: - construct(sit, name); - return; - case STI_PATHSET: - construct(sit, name); - return; - case STI_OBJECT: - construct(sit, name, depth); - return; - case STI_ARRAY: - construct(sit, name, depth); - return; - case STI_ISSUE: - construct(sit, name); - return; - case STI_XCHAIN_BRIDGE: - construct(sit, name); - return; - case STI_CURRENCY: - construct(sit, name); - return; - default: - Throw("Unknown object type"); - } + constructST(name.fieldType, depth, sit, name); } STVar::STVar(SerializedTypeID id, SField const& name) { assert((id == STI_NOTPRESENT) || (id == name.fieldType)); + constructST(id, 0, name); +} + +void +STVar::destroy() +{ + if (on_heap()) + delete p_; + else + p_->~STBase(); + + p_ = nullptr; +} + +template +requires ValidConstructSTArgs void +STVar::constructST(SerializedTypeID id, int depth, Args&&... args) +{ + auto constructWithDepth = [&]() { + if constexpr (std::is_same_v< + std::tuple...>, + std::tuple>) + construct(std::forward(args)...); + else if constexpr (std::is_same_v< + std::tuple...>, + std::tuple>) + construct(std::forward(args)..., depth); + else + static_assert(false, "Invalid STVar constructor arguments"); + }; + switch (id) { - case STI_NOTPRESENT: - construct(name); + case STI_NOTPRESENT: { + // Last argument is always SField + SField const& field = + std::get(std::forward_as_tuple(args...)); + construct(field); return; + } case STI_UINT8: - construct(name); + construct(std::forward(args)...); return; case STI_UINT16: - construct(name); + construct(std::forward(args)...); return; case STI_UINT32: - construct(name); + construct(std::forward(args)...); return; case STI_UINT64: - construct(name); + construct(std::forward(args)...); return; case STI_AMOUNT: - construct(name); + construct(std::forward(args)...); return; case STI_UINT128: - construct(name); + construct(std::forward(args)...); return; case STI_UINT160: - construct(name); + construct(std::forward(args)...); return; case STI_UINT192: - construct(name); + construct(std::forward(args)...); return; case STI_UINT256: - construct(name); + construct(std::forward(args)...); return; case STI_VECTOR256: - construct(name); + construct(std::forward(args)...); return; case STI_VL: - construct(name); + construct(std::forward(args)...); return; case STI_ACCOUNT: - construct(name); + construct(std::forward(args)...); return; case STI_PATHSET: - construct(name); + construct(std::forward(args)...); return; case STI_OBJECT: - construct(name); + constructWithDepth.template operator()(); return; case STI_ARRAY: - construct(name); + constructWithDepth.template operator()(); return; case STI_ISSUE: - construct(name); + construct(std::forward(args)...); return; case STI_XCHAIN_BRIDGE: - construct(name); + construct(std::forward(args)...); return; case STI_CURRENCY: - construct(name); + construct(std::forward(args)...); return; default: Throw("Unknown object type"); } } -void -STVar::destroy() -{ - if (on_heap()) - delete p_; - else - p_->~STBase(); - - p_ = nullptr; -} - } // namespace detail } // namespace ripple From 93c545d9ec1e814a4a5543c443369bac6209365d Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Mon, 7 Oct 2024 12:08:26 -0400 Subject: [PATCH 18/58] Fix static_assert in STVar and other changes * Update flags check in Payment * Check sendMax bad amount in Payment * Refactor MPT payment unit-test - order by preflight/preclaim/apply validation --- src/libxrpl/protocol/STVar.cpp | 10 +- src/test/app/MPToken_test.cpp | 447 ++++++++++++++++------------ src/test/jtx/amount.h | 13 - src/xrpld/app/tx/detail/Payment.cpp | 32 +- 4 files changed, 277 insertions(+), 225 deletions(-) diff --git a/src/libxrpl/protocol/STVar.cpp b/src/libxrpl/protocol/STVar.cpp index aa693308600..e44c37259b7 100644 --- a/src/libxrpl/protocol/STVar.cpp +++ b/src/libxrpl/protocol/STVar.cpp @@ -143,13 +143,21 @@ STVar::constructST(SerializedTypeID id, int depth, Args&&... args) if constexpr (std::is_same_v< std::tuple...>, std::tuple>) + { construct(std::forward(args)...); + } else if constexpr (std::is_same_v< std::tuple...>, std::tuple>) + { construct(std::forward(args)..., depth); + } else - static_assert(false, "Invalid STVar constructor arguments"); + { + constexpr bool alwaysFalse = + !std::is_same_v, std::tuple>; + static_assert(alwaysFalse, "Invalid STVar constructor arguments"); + } }; switch (id) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index c96520e202d..e6370888e87 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -637,51 +637,173 @@ class MPToken_test : public beast::unit_test::suite Account const alice("alice"); // issuer Account const bob("bob"); // holder Account const carol("carol"); // holder + + // preflight validation + + // 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); + STAmount mpt{MPTIssue{makeMptID(1, alice)}, 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); + STAmount mpt{MPTIssue{makeMptID(1, alice)}, 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 flag + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + auto const MPT = mptAlice["MPT"]; + + mptAlice.authorize({.account = &bob}); + + for (auto flags : + {tfNoRippleDirect, tfPartialPayment, tfLimitQuality}) + env(pay(alice, bob, MPT(10)), + txflags(flags), + ter(temINVALID_FLAG)); + } + + // Invalid combination of send, sendMax, deliverMin, paths + { + 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 + auto const MPT = mptAlice["MPT"]; + env(pay(alice, carol, MPT(100)), + sendmax(XRP(100)), + ter(temMALFORMED)); + env(pay(alice, carol, MPT(100)), + delivermin(XRP(100)), + ter(temMALFORMED)); + // sendMax MPT is invalid with IOU or XRP + auto const USD = alice["USD"]; + env(pay(alice, carol, USD(100)), + sendmax(MPT(100)), + ter(temMALFORMED)); + env(pay(alice, carol, XRP(100)), + sendmax(MPT(100)), + ter(temMALFORMED)); + // sendmax and amount are different MPT issue + test::jtx::MPT const MPT1( + "MPT", makeMptID(env.seq(alice) + 10, alice)); + env(pay(alice, carol, MPT1(100)), + sendmax(MPT(100)), + ter(temMALFORMED)); + // paths is invalid + env(pay(alice, carol, MPT(100)), path(~USD), 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, .flags = tfMPTCanTransfer}); + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + auto const MPT = mptAlice["MPT"]; + + mptAlice.authorize({.account = &carol}); + + Json::Value payment; + payment[jss::secret] = alice.name(); + payment[jss::tx_json] = pay(alice, carol, 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."); + } - // env(mpt::authorize(alice, id.key, std::nullopt)); - // env.close(); + // Can't pay negative amount + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + auto const MPT = mptAlice["MPT"]; mptAlice.authorize({.account = &bob}); - mptAlice.authorize({.account = &carol}); - // issuer to holder - mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, bob, -1, temBAD_AMOUNT); - // holder to issuer - mptAlice.pay(bob, alice, 100); + env(pay(alice, bob, MPT(10)), sendmax(MPT(-1)), ter(temBAD_AMOUNT)); + } - // holder to holder - mptAlice.pay(alice, bob, 100); - mptAlice.pay(bob, carol, 50); + // Pay to self + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &bob}); + + mptAlice.pay(bob, bob, 10, temREDUNDANT); } - // Holder is not authorized + // preclaim validation + + // Destination doesn't exist { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + MPTTester mptAlice(env, alice, {.holders = {&bob}}); - mptAlice.create( - {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + mptAlice.create({.ownerCount = 1, .holderCount = 0}); - // issuer to holder - mptAlice.pay(alice, bob, 100, tecNO_AUTH); + mptAlice.authorize({.account = &bob}); - // holder to issuer - mptAlice.pay(bob, alice, 100, tecNO_AUTH); + Account const bad{"bad"}; + env.memoize(bad); - // holder to holder - mptAlice.pay(bob, carol, 50, tecNO_AUTH); + mptAlice.pay(bob, bad, 10, tecNO_DST); } - // If allowlisting is enabled, Payment fails if the receiver is not + // apply validation + + // If RequireAuth is enabled, Payment fails if the receiver is not // authorized { Env env{*this, features}; @@ -698,7 +820,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.pay(alice, bob, 100, tecNO_AUTH); } - // If allowlisting is enabled, Payment fails if the sender is not + // If RequireAuth is enabled, Payment fails if the sender is not // authorized { Env env{*this, features}; @@ -728,6 +850,54 @@ class MPToken_test : public beast::unit_test::suite mptAlice.pay(bob, alice, 100, tecNO_AUTH); } + // 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); + } + + // 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); + } + // Payer doesn't have enough funds { Env env{*this, features}; @@ -787,74 +957,6 @@ class MPToken_test : public beast::unit_test::suite 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, tecPATH_PARTIAL); - } - - // 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, tecPATH_PARTIAL); - } - - // 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); - STAmount mpt{MPTIssue{makeMptID(1, alice)}, 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}; @@ -892,133 +994,63 @@ class MPToken_test : public beast::unit_test::suite sendmax(MPT(109)), ter(tecPATH_PARTIAL)); - // Payment succeeds if SendMax is included. + // Payment succeeds if sufficient SendMax is included. env(pay(bob, carol, MPT(100)), sendmax(MPT(110))); env(pay(bob, carol, MPT(100)), sendmax(MPT(115))); } - // Test that non-issuer cannot send to each other if MPTCanTransfer - // isn't set + // Issuer fails trying to send more than the maximum amount allowed { - Env env(*this, features); - Account const alice{"alice"}; - Account const bob{"bob"}; - Account const cindy{"cindy"}; + Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob, &cindy}}); + MPTTester mptAlice(env, alice, {.holders = {&bob}}); - // alice creates issuance without MPTCanTransfer - mptAlice.create({.ownerCount = 1, .holderCount = 0}); + mptAlice.create( + {.maxAmt = "100", + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer}); - // bob creates a MPToken mptAlice.authorize({.account = &bob}); - // cindy creates a MPToken - mptAlice.authorize({.account = &cindy}); - - // alice pays bob 100 tokens + // issuer sends holder the max amount allowed 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); - STAmount mpt{MPTIssue{makeMptID(1, alice)}, 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); - STAmount mpt{MPTIssue{makeMptID(1, alice)}, 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"); + // issuer tries to exceed max amount + mptAlice.pay(alice, bob, 1, tecPATH_PARTIAL); } - // Invalid combination of send, sendMax, deliverMin + // Issuer fails trying to send more than the default maximum + // amount allowed { Env env{*this, features}; - Account const alice("alice"); - Account const carol("carol"); - MPTTester mptAlice(env, alice, {.holders = {&carol}}); + MPTTester mptAlice(env, alice, {.holders = {&bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); - mptAlice.authorize({.account = &carol}); + mptAlice.authorize({.account = &bob}); - // sendMax and DeliverMin are valid XRP amount, - // but is invalid combination with MPT amount - auto const MPT = mptAlice["MPT"]; - env(pay(alice, carol, MPT(100)), - sendmax(XRP(100)), - ter(temMALFORMED)); - env(pay(alice, carol, MPT(100)), - delivermin(XRP(100)), - ter(temMALFORMED)); - // sendMax MPT is invalid with IOU or XRP - auto const USD = alice["USD"]; - env(pay(alice, carol, USD(100)), - sendmax(MPT(100)), - ter(temMALFORMED)); - env(pay(alice, carol, XRP(100)), - sendmax(MPT(100)), - ter(temMALFORMED)); - // sendmax and amount are different MPT issue - test::jtx::MPT const MPT1( - "MPT", makeMptID(env.seq(alice) + 10, alice)); - env(pay(alice, carol, MPT1(100)), - sendmax(MPT(100)), - ter(temMALFORMED)); + // issuer sends holder the default max amount allowed + mptAlice.pay(alice, bob, maxMPTokenAmount); + + // issuer tries to exceed max amount + mptAlice.pay(alice, bob, 1, tecPATH_PARTIAL); } - // build_path is invalid if MPT + // pay more than max amount fails in the json parser before + // transactor is called { 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}); - auto const MPT = mptAlice["MPT"]; - - mptAlice.authorize({.account = &carol}); - - Json::Value payment; - payment[jss::secret] = alice.name(); - payment[jss::tx_json] = pay(alice, carol, MPT(100)); - - payment[jss::build_path] = true; - auto jrr = env.rpc("json", "submit", to_string(payment)); + env.fund(XRP(1'000), alice, bob); + STAmount mpt{MPTIssue{makeMptID(1, alice)}, 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"); - BEAST_EXPECT( - jrr[jss::result][jss::error_message] == - "Field 'build_path' not allowed in this context."); } // Issuer fails trying to send fund after issuance was destroyed @@ -1034,13 +1066,13 @@ class MPToken_test : public beast::unit_test::suite // alice destroys issuance mptAlice.destroy({.ownerCount = 0}); - // alice tries to send bob fund after issuance is destroy, should + // alice tries to send bob fund after issuance is destroyed, should // fail. mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND); } - // Issuer fails trying to send to some who doesn't own MPT for a - // issuance that was destroyed + // Issuer fails trying to send to an account, which doesn't own MPT for + // an issuance that was destroyed { Env env{*this, features}; @@ -1077,6 +1109,29 @@ class MPToken_test : public beast::unit_test::suite // transfer max amount to another holder mptAlice.pay(bob, carol, 100); } + + // Simple payment + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + + 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); + } } void diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index c73ac2bcf09..698526c29f9 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -393,25 +393,12 @@ class MPT requires(sizeof(T) >= sizeof(int) && std::is_arithmetic_v) PrettyAmount operator()(T v) const { - // VFALCO NOTE Should throw if the - // representation of v is not exact. return {amountFromString(mpt(), std::to_string(v)), name}; } PrettyAmount operator()(epsilon_t) const; PrettyAmount operator()(detail::epsilon_multiple) const; - // VFALCO TODO - // STAmount operator()(char const* s) const; - - /** Returns None-of-Issue */ -#if 0 - None operator()(none_t) const - { - return {Issue{}}; - } -#endif - friend BookSpec operator~(MPT const& mpt) { diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 16cdc515a08..5fb2fbe1828 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -221,26 +221,28 @@ preflightHelper(PreflightContext const& ctx) if (!ctx.rules.enabled(featureMPTokensV1)) return temDISABLED; - if (ctx.tx.isFieldPresent(sfDeliverMin) || ctx.tx.isFieldPresent(sfPaths)) - return temMALFORMED; - - if (auto const sendMax = ctx.tx[~sfSendMax]; sendMax && - (!sendMax->holds() || - sendMax->asset() != ctx.tx[sfAmount].asset())) - return temMALFORMED; - auto& tx = ctx.tx; auto& j = ctx.j; std::uint32_t const uTxFlags = tx.getFlags(); - if (uTxFlags & tfPaymentMask) + if (uTxFlags & tfUniversalMask) { JLOG(j.trace()) << "Malformed transaction: " << "Invalid flags set."; return temINVALID_FLAG; } + if (ctx.tx.isFieldPresent(sfDeliverMin) || ctx.tx.isFieldPresent(sfPaths)) + return temMALFORMED; + + auto const sendMax = ctx.tx[~sfSendMax]; + + if (sendMax && + (!sendMax->holds() || + sendMax->asset() != ctx.tx[sfAmount].asset())) + return temMALFORMED; + STAmount const saDstAmount(tx.getFieldAmount(sfAmount)); auto const account = tx.getAccountID(sfAccount); @@ -261,6 +263,12 @@ preflightHelper(PreflightContext const& ctx) << "bad dst amount: " << saDstAmount.getFullText(); return temBAD_AMOUNT; } + if (sendMax && *sendMax <= beast::zero) + { + JLOG(j.trace()) << "Malformed transaction: " + << "bad max amount: " << sendMax->getFullText(); + return temBAD_AMOUNT; + } if (account == uDstAccountID) { // You're signing yourself a payment. @@ -269,12 +277,6 @@ preflightHelper(PreflightContext const& ctx) << " to self without path for " << to_string(uDstMptID); return temREDUNDANT; } - if (uTxFlags & (tfPartialPayment | tfLimitQuality | tfNoRippleDirect)) - { - JLOG(j.trace()) << "Malformed transaction: invalid MPT flags: " - << uTxFlags; - return temMALFORMED; - } return preflight2(ctx); } From aacb1c81555194687768f803813ad5796480a9cf Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:11:48 -0400 Subject: [PATCH 19/58] comment (#38) --- include/xrpl/protocol/TxFlags.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index e364b9ba6e2..8b5e430ecb7 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -133,8 +133,7 @@ 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. +// NOTE - there is intentionally no flag here for 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; From 560cf91c6084055827609be71d05a7ea5bc91aff Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Mon, 7 Oct 2024 14:14:44 -0400 Subject: [PATCH 20/58] Fix SendMax with no transfer fee --- src/test/app/MPToken_test.cpp | 24 ++++++++++++++++++++++++ src/xrpld/app/tx/detail/Payment.cpp | 15 ++++++++------- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index e6370888e87..d311b31173d 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -999,6 +999,30 @@ class MPToken_test : public beast::unit_test::suite env(pay(bob, carol, MPT(100)), sendmax(MPT(115))); } + // Insufficient SendMax with no transfer fee + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + + // Holders create MPToken + mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = &carol}); + mptAlice.pay(alice, bob, 1'000); + + auto const MPT = mptAlice["MPT"]; + // SendMax is less than the amount + env(pay(bob, carol, MPT(100)), + sendmax(MPT(99)), + ter(tecPATH_PARTIAL)); + + // Payment succeeds if sufficient SendMax is included. + env(pay(bob, carol, MPT(100)), sendmax(MPT(100))); + } + // Issuer fails trying to send more than the maximum amount allowed { Env env{*this, features}; diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 5fb2fbe1828..82a09c046b6 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -686,13 +686,14 @@ applyHelper(ApplyContext& ctx, XRPAmount const&, XRPAmount const&) isFrozen(ctx.view(), uDstAccountID, mpt)) return tecLOCKED; - auto const sendMax(ctx.tx[~sfSendMax]); - // If the transfer fee is included then SendMax has to be included - // and be large enough to cover the fee. The payment can still fail if - // the sender has insufficient funds. - if (auto const rate = transferRate(ctx.view(), mpt.getMptID()); - rate != parityRate && - (!sendMax || multiply(saDstAmount, rate) > *sendMax)) + auto const sendMax = ctx.tx[~sfSendMax].value_or(saDstAmount); + // If SendMax is included then it should be greater or equal to + // the send amount plus the transfer fees if applicable. + // The payment can still fail if the sender has insufficient funds. + auto const amount = + multiply(saDstAmount, transferRate(ctx.view(), mpt.getMptID())); + if (multiply(saDstAmount, transferRate(ctx.view(), mpt.getMptID())) > + sendMax) return tecPATH_PARTIAL; } From 6a7a8c27b9d98b0f40685345786d4803f8139098 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Wed, 9 Oct 2024 11:51:49 -0400 Subject: [PATCH 21/58] Combine Issue/MPTIssue handling in Payment transactor plus other changes * Add support for DeliverMin and partial payment * Fix error for non-existent issuance payment * Update unit-tests * Refactor unit-test for tx with non-MPT amounts --- include/xrpl/protocol/Asset.h | 9 + include/xrpl/protocol/MPTIssue.h | 2 + include/xrpl/protocol/Rate.h | 4 +- src/libxrpl/protocol/MPTIssue.cpp | 2 + src/libxrpl/protocol/Rate2.cpp | 12 +- src/libxrpl/protocol/TxFormats.cpp | 2 +- src/test/app/MPToken_test.cpp | 197 +++++++++--- src/xrpld/app/tx/detail/Clawback.cpp | 2 +- src/xrpld/app/tx/detail/Payment.cpp | 460 ++++++++++----------------- src/xrpld/ledger/View.h | 2 +- src/xrpld/ledger/detail/View.cpp | 15 +- 11 files changed, 336 insertions(+), 371 deletions(-) diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index e936f7569df..b787c153180 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -94,6 +94,9 @@ class Asset friend constexpr bool operator!=(Asset const& lhs, Asset const& rhs); + + friend constexpr bool + operator==(Currency const& lhs, Asset const& rhs); }; template @@ -148,6 +151,12 @@ operator!=(Asset const& lhs, Asset const& rhs) return !(lhs == rhs); } +constexpr bool +operator==(Currency const& lhs, Asset const& rhs) +{ + return rhs.holds() && rhs.get().currency == lhs; +} + inline bool isXRP(Asset const& asset) { diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index c681bdc0757..941a3be5cbd 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -76,6 +76,8 @@ operator!=(MPTIssue const& lhs, MPTIssue const& rhs) return !(lhs == rhs); } +/** MPT is a non-native token. + */ inline bool isXRP(MPTID const&) { diff --git a/include/xrpl/protocol/Rate.h b/include/xrpl/protocol/Rate.h index b065acb2316..f58aae6e305 100644 --- a/include/xrpl/protocol/Rate.h +++ b/include/xrpl/protocol/Rate.h @@ -74,7 +74,7 @@ STAmount multiplyRound( STAmount const& amount, Rate const& rate, - Issue const& issue, + Asset const& asset, bool roundUp); STAmount @@ -87,7 +87,7 @@ STAmount divideRound( STAmount const& amount, Rate const& rate, - Issue const& issue, + Asset const& issue, bool roundUp); namespace nft { diff --git a/src/libxrpl/protocol/MPTIssue.cpp b/src/libxrpl/protocol/MPTIssue.cpp index 951b9d139c3..c68c26a40cd 100644 --- a/src/libxrpl/protocol/MPTIssue.cpp +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -30,6 +30,8 @@ MPTIssue::MPTIssue(MPTID const& issuanceID) : mptID_(issuanceID) AccountID const& MPTIssue::getIssuer() const { + // MPTID is concatenation of sequence + account + static_assert(sizeof(MPTID) == (sizeof(std::uint32_t) + sizeof(AccountID))); // copy from id skipping the sequence AccountID const* account = reinterpret_cast( mptID_.data() + sizeof(std::uint32_t)); diff --git a/src/libxrpl/protocol/Rate2.cpp b/src/libxrpl/protocol/Rate2.cpp index 01a3e7deca5..33bd9c5d0be 100644 --- a/src/libxrpl/protocol/Rate2.cpp +++ b/src/libxrpl/protocol/Rate2.cpp @@ -69,7 +69,7 @@ STAmount multiplyRound( STAmount const& amount, Rate const& rate, - Issue const& issue, + Asset const& asset, bool roundUp) { assert(rate.value != 0); @@ -79,7 +79,7 @@ multiplyRound( return amount; } - return mulRound(amount, detail::as_amount(rate), issue, roundUp); + return mulRound(amount, detail::as_amount(rate), asset, roundUp); } STAmount @@ -90,7 +90,7 @@ divide(STAmount const& amount, Rate const& rate) if (rate == parityRate) return amount; - return divide(amount, detail::as_amount(rate), amount.issue()); + return divide(amount, detail::as_amount(rate), amount.asset()); } STAmount @@ -101,14 +101,14 @@ divideRound(STAmount const& amount, Rate const& rate, bool roundUp) if (rate == parityRate) return amount; - return divRound(amount, detail::as_amount(rate), amount.issue(), roundUp); + return divRound(amount, detail::as_amount(rate), amount.asset(), roundUp); } STAmount divideRound( STAmount const& amount, Rate const& rate, - Issue const& issue, + Asset const& asset, bool roundUp) { assert(rate.value != 0); @@ -116,7 +116,7 @@ divideRound( if (rate == parityRate) return amount; - return divRound(amount, detail::as_amount(rate), issue, roundUp); + return divRound(amount, detail::as_amount(rate), asset, roundUp); } } // namespace ripple diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index 01d097aa17f..2b506ffc0e6 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -167,7 +167,7 @@ TxFormats::TxFormats() {sfPaths, soeDEFAULT}, {sfInvoiceID, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, - {sfDeliverMin, soeOPTIONAL}, + {sfDeliverMin, soeOPTIONAL, soeMPTSupported}, }, commonFields); diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index d311b31173d..800aaa9496d 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -683,8 +683,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.authorize({.account = &bob}); - for (auto flags : - {tfNoRippleDirect, tfPartialPayment, tfLimitQuality}) + for (auto flags : {tfNoRippleDirect, tfLimitQuality}) env(pay(alice, bob, MPT(10)), txflags(flags), ter(temINVALID_FLAG)); @@ -710,7 +709,7 @@ class MPToken_test : public beast::unit_test::suite ter(temMALFORMED)); env(pay(alice, carol, MPT(100)), delivermin(XRP(100)), - ter(temMALFORMED)); + ter(temBAD_AMOUNT)); // sendMax MPT is invalid with IOU or XRP auto const USD = alice["USD"]; env(pay(alice, carol, USD(100)), @@ -719,6 +718,12 @@ class MPToken_test : public beast::unit_test::suite env(pay(alice, carol, XRP(100)), sendmax(MPT(100)), ter(temMALFORMED)); + env(pay(alice, carol, USD(100)), + delivermin(MPT(100)), + ter(temBAD_AMOUNT)); + env(pay(alice, carol, XRP(100)), + delivermin(MPT(100)), + ter(temBAD_AMOUNT)); // sendmax and amount are different MPT issue test::jtx::MPT const MPT1( "MPT", makeMptID(env.seq(alice) + 10, alice)); @@ -979,6 +984,7 @@ class MPToken_test : public beast::unit_test::suite // Payment between the holder and the issuer, no transfer fee. mptAlice.pay(bob, alice, 1'000); + BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 1'000)); // Payment between the holders. The sender doesn't have // enough funds to cover the transfer fee. @@ -995,8 +1001,21 @@ class MPToken_test : public beast::unit_test::suite ter(tecPATH_PARTIAL)); // Payment succeeds if sufficient SendMax is included. + // 100 to carol, 10 to issuer env(pay(bob, carol, MPT(100)), sendmax(MPT(110))); + // 100 to carol, 10 to issuer env(pay(bob, carol, MPT(100)), sendmax(MPT(115))); + BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 780)); + BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 200)); + // Payment succeeds if partial payment even if + // SendMax is less than deliver amount + env(pay(bob, carol, MPT(100)), + sendmax(MPT(90)), + txflags(tfPartialPayment)); + // 82 to carol, 8 to issuer (90 / 1.1 ~ 81.81 (rounded to nearest) = + // 82) + BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 690)); + BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 282)); } // Insufficient SendMax with no transfer fee @@ -1018,9 +1037,47 @@ class MPToken_test : public beast::unit_test::suite env(pay(bob, carol, MPT(100)), sendmax(MPT(99)), ter(tecPATH_PARTIAL)); + env(pay(bob, alice, MPT(100)), + sendmax(MPT(99)), + ter(tecPATH_PARTIAL)); // Payment succeeds if sufficient SendMax is included. env(pay(bob, carol, MPT(100)), sendmax(MPT(100))); + BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 100)); + // Payment succeeds if partial payment + env(pay(bob, carol, MPT(100)), + sendmax(MPT(99)), + txflags(tfPartialPayment)); + BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 199)); + } + + // DeliverMin + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + + // Holders create MPToken + mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = &carol}); + mptAlice.pay(alice, bob, 1'000); + + auto const MPT = mptAlice["MPT"]; + // Fails even with the partial payment because + // deliver amount < deliverMin + env(pay(bob, alice, MPT(100)), + sendmax(MPT(99)), + delivermin(MPT(100)), + txflags(tfPartialPayment), + ter(tecPATH_PARTIAL)); + // Payment succeeds if deliver amount >= deliverMin + env(pay(bob, alice, MPT(100)), + sendmax(MPT(99)), + delivermin(MPT(99)), + txflags(tfPartialPayment)); } // Issuer fails trying to send more than the maximum amount allowed @@ -1062,7 +1119,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.pay(alice, bob, 1, tecPATH_PARTIAL); } - // pay more than max amount fails in the json parser before + // Pay more than max amount fails in the json parser before // transactor is called { Env env{*this, features}; @@ -1095,6 +1152,16 @@ class MPToken_test : public beast::unit_test::suite mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND); } + // Non-existent issuance + { + Env env{*this, features}; + + env.fund(XRP(1'000), alice, bob); + + STAmount const mpt{MPTID{0}, 100}; + env(pay(alice, bob, mpt), ter(tecOBJECT_NOT_FOUND)); + } + // Issuer fails trying to send to an account, which doesn't own MPT for // an issuance that was destroyed { @@ -1164,6 +1231,10 @@ class MPToken_test : public beast::unit_test::suite testcase("MPT Amount Invalid in Transaction"); using namespace test::jtx; + // Validate that every transaction with an amount field, + // which doesn't support MPT, fails. + + // keyed by transaction + amount field std::set txWithAmounts; for (auto const& format : TxFormats::getInstance()) { @@ -1174,12 +1245,12 @@ class MPToken_test : public beast::unit_test::suite // 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 && + if (e.supportMPT() == soeMPTNotSupported && e.sField().getName() != jss::Fee && - format.getName() != jss::Clawback && format.getName() != jss::SetFee) { - txWithAmounts.insert(format.getName()); + txWithAmounts.insert( + format.getName() + e.sField().fieldName); break; } } @@ -1196,8 +1267,10 @@ class MPToken_test : public beast::unit_test::suite 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()); + auto test = [&](Json::Value const& jv, + std::string const& amtField) { + txWithAmounts.erase( + jv[jss::TransactionType].asString() + amtField); // tx is signed auto jtx = env.jt(jv); @@ -1229,7 +1302,7 @@ class MPToken_test : public beast::unit_test::suite ? mpt.getJson(JsonOptions::none) : "100000000"; jv[jss::TradingFee] = 0; - test(jv); + test(jv, field.fieldName); }; ammCreate(sfAmount); ammCreate(sfAmount2); @@ -1242,7 +1315,7 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Asset2] = to_json(USD.issue()); jv[field.fieldName] = mpt.getJson(JsonOptions::none); jv[jss::Flags] = tfSingleAsset; - test(jv); + test(jv, field.fieldName); }; ammDeposit(sfAmount); for (SField const& field : @@ -1259,7 +1332,7 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Asset2] = to_json(USD.issue()); jv[jss::Flags] = tfSingleAsset; jv[field.fieldName] = mpt.getJson(JsonOptions::none); - test(jv); + test(jv, field.fieldName); }; ammWithdraw(sfAmount); for (SField const& field : @@ -1275,7 +1348,7 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Asset] = to_json(xrpIssue()); jv[jss::Asset2] = to_json(USD.issue()); jv[field.fieldName] = mpt.getJson(JsonOptions::none); - test(jv); + test(jv, field.fieldName); }; ammBid(sfBidMin); ammBid(sfBidMax); @@ -1286,7 +1359,7 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Account] = alice.human(); jv[sfCheckID.fieldName] = to_string(uint256{1}); jv[field.fieldName] = mpt.getJson(JsonOptions::none); - test(jv); + test(jv, field.fieldName); }; checkCash(sfAmount); checkCash(sfDeliverMin); @@ -1297,7 +1370,7 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Account] = alice.human(); jv[jss::Destination] = carol.human(); jv[jss::SendMax] = mpt.getJson(JsonOptions::none); - test(jv); + test(jv, jss::SendMax.c_str()); } // EscrowCreate { @@ -1306,12 +1379,14 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Account] = alice.human(); jv[jss::Destination] = carol.human(); jv[jss::Amount] = mpt.getJson(JsonOptions::none); - test(jv); + test(jv, jss::Amount.c_str()); } // OfferCreate { - Json::Value const jv = offer(alice, USD(100), mpt); - test(jv); + Json::Value jv = offer(alice, USD(100), mpt); + test(jv, jss::TakerPays.c_str()); + jv = offer(alice, mpt, USD(100)); + test(jv, jss::TakerGets.c_str()); } // PaymentChannelCreate { @@ -1322,7 +1397,7 @@ class MPToken_test : public beast::unit_test::suite jv[jss::SettleDelay] = 1; jv[sfPublicKey.fieldName] = strHex(alice.pk().slice()); jv[jss::Amount] = mpt.getJson(JsonOptions::none); - test(jv); + test(jv, jss::Amount.c_str()); } // PaymentChannelFund { @@ -1331,7 +1406,7 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Account] = alice.human(); jv[sfChannel.fieldName] = to_string(uint256{1}); jv[jss::Amount] = mpt.getJson(JsonOptions::none); - test(jv); + test(jv, jss::Amount.c_str()); } // PaymentChannelClaim { @@ -1340,17 +1415,7 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Account] = alice.human(); jv[sfChannel.fieldName] = to_string(uint256{1}); jv[jss::Amount] = mpt.getJson(JsonOptions::none); - test(jv); - } - // Payment - { - 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); - jv[jss::DeliverMin] = mpt.getJson(JsonOptions::none); - test(jv); + test(jv, jss::Amount.c_str()); } // NFTokenCreateOffer { @@ -1359,7 +1424,7 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Account] = alice.human(); jv[sfNFTokenID.fieldName] = to_string(uint256{1}); jv[jss::Amount] = mpt.getJson(JsonOptions::none); - test(jv); + test(jv, jss::Amount.c_str()); } // NFTokenAcceptOffer { @@ -1368,7 +1433,7 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Account] = alice.human(); jv[sfNFTokenBrokerFee.fieldName] = mpt.getJson(JsonOptions::none); - test(jv); + test(jv, sfNFTokenBrokerFee.fieldName); } // NFTokenMint { @@ -1377,7 +1442,7 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Account] = alice.human(); jv[sfNFTokenTaxon.fieldName] = 1; jv[jss::Amount] = mpt.getJson(JsonOptions::none); - test(jv); + test(jv, jss::Amount.c_str()); } // TrustSet auto trustSet = [&](SField const& field) { @@ -1386,25 +1451,25 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Account] = alice.human(); jv[jss::Flags] = 0; jv[field.fieldName] = mpt.getJson(JsonOptions::none); - test(jv); + test(jv, field.fieldName); }; trustSet(sfLimitAmount); trustSet(sfFee); // XChainCommit { Json::Value const jv = xchain_commit(alice, jvb, 1, mpt); - test(jv); + test(jv, jss::Amount.c_str()); } // XChainClaim { Json::Value const jv = xchain_claim(alice, jvb, 1, mpt, alice); - test(jv); + test(jv, jss::Amount.c_str()); } // XChainCreateClaimID { Json::Value const jv = xchain_create_claim_id(alice, jvb, mpt, alice); - test(jv); + test(jv, sfSignatureReward.fieldName); } // XChainAddClaimAttestation { @@ -1418,11 +1483,11 @@ class MPToken_test : public beast::unit_test::suite 1, alice, signer(alice)); - test(jv); + test(jv, jss::Amount.c_str()); } // XChainAddAccountCreateAttestation { - Json::Value const jv = create_account_attestation( + Json::Value jv = create_account_attestation( alice, jvb, alice, @@ -1433,32 +1498,58 @@ class MPToken_test : public beast::unit_test::suite 1, alice, signer(alice)); - test(jv); + for (auto const& field : + {sfAmount.fieldName, sfSignatureReward.fieldName}) + { + jv[field] = mpt.getJson(JsonOptions::none); + test(jv, field); + } } // XChainAccountCreateCommit { - Json::Value const jv = sidechain_xchain_account_create( + Json::Value jv = sidechain_xchain_account_create( alice, jvb, alice, mpt, XRP(10)); - test(jv); + for (auto const& field : + {sfAmount.fieldName, sfSignatureReward.fieldName}) + { + jv[field] = mpt.getJson(JsonOptions::none); + test(jv, field); + } } // XChain[Create|Modify]Bridge auto bridgeTx = [&](Json::StaticString const& tt, - bool minAmount = false) { + STAmount const& rewardAmount, + STAmount const& minAccountAmount, + std::string const& field) { 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); + rewardAmount.getJson(JsonOptions::none); + jv[sfMinAccountCreateAmount.fieldName] = + minAccountAmount.getJson(JsonOptions::none); + test(jv, field); }; - bridgeTx(jss::XChainCreateBridge); - bridgeTx(jss::XChainCreateBridge, true); - bridgeTx(jss::XChainModifyBridge); - bridgeTx(jss::XChainModifyBridge, true); + auto reward = STAmount{sfSignatureReward, mpt}; + auto minAmount = STAmount{sfMinAccountCreateAmount, USD(10)}; + for (SField const& field : + {std::ref(sfSignatureReward), + std::ref(sfMinAccountCreateAmount)}) + { + bridgeTx( + jss::XChainCreateBridge, + reward, + minAmount, + field.fieldName); + bridgeTx( + jss::XChainModifyBridge, + reward, + minAmount, + field.fieldName); + reward = STAmount{sfSignatureReward, USD(10)}; + minAmount = STAmount{sfMinAccountCreateAmount, mpt}; + } } BEAST_EXPECT(txWithAmounts.empty()); } diff --git a/src/xrpld/app/tx/detail/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index c11dfbd8c11..4b6d513b152 100644 --- a/src/xrpld/app/tx/detail/Clawback.cpp +++ b/src/xrpld/app/tx/detail/Clawback.cpp @@ -252,7 +252,7 @@ applyHelper(ApplyContext& ctx) ahIGNORE_AUTH, ctx.journal); - return rippleMPTCredit( + return rippleCreditMPT( ctx.view(), holder, issuer, diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 82a09c046b6..572711fbd39 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -43,13 +43,8 @@ Payment::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)}; } -template -static NotTEC -preflightHelper(PreflightContext const& ctx); - -template <> NotTEC -preflightHelper(PreflightContext const& ctx) +Payment::preflight(PreflightContext const& ctx) { if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -57,45 +52,63 @@ preflightHelper(PreflightContext const& ctx) auto& tx = ctx.tx; auto& j = ctx.j; + STAmount const saDstAmount(tx.getFieldAmount(sfAmount)); + bool const bMPTDirect = saDstAmount.holds(); + + if (bMPTDirect && !ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + std::uint32_t const uTxFlags = tx.getFlags(); - if (uTxFlags & tfPaymentMask) + std::uint32_t paymentMask = + bMPTDirect ? ~(tfUniversal | tfPartialPayment) : tfPaymentMask; + + if (uTxFlags & paymentMask) { JLOG(j.trace()) << "Malformed transaction: " << "Invalid flags set."; return temINVALID_FLAG; } + if (bMPTDirect && ctx.tx.isFieldPresent(sfPaths)) + return temMALFORMED; + bool const partialPaymentAllowed = uTxFlags & tfPartialPayment; bool const limitQuality = uTxFlags & tfLimitQuality; bool const defaultPathsAllowed = !(uTxFlags & tfNoRippleDirect); bool const bPaths = tx.isFieldPresent(sfPaths); bool const bMax = tx.isFieldPresent(sfSendMax); - STAmount const saDstAmount(tx.getFieldAmount(sfAmount)); + auto const deliverMin = tx[~sfDeliverMin]; STAmount maxSourceAmount; auto const account = tx.getAccountID(sfAccount); if (bMax) maxSourceAmount = tx.getFieldAmount(sfSendMax); - else if (saDstAmount.native()) + else if (saDstAmount.native() || bMPTDirect) maxSourceAmount = saDstAmount; else maxSourceAmount = STAmount( - Issue{saDstAmount.getCurrency(), account}, + Issue{saDstAmount.get().currency, account}, saDstAmount.mantissa(), saDstAmount.exponent(), saDstAmount < beast::zero); - if (!maxSourceAmount.holds()) + if ((bMPTDirect && saDstAmount.asset() != maxSourceAmount.asset()) || + (!bMPTDirect && maxSourceAmount.holds())) + { + JLOG(j.trace()) << "Malformed transaction: " + << "inconsistent issues: " << saDstAmount.getFullText() + << " " << maxSourceAmount.getFullText() << " " + << deliverMin.value_or(STAmount{}).getFullText(); return temMALFORMED; + } - auto const& uSrcCurrency = maxSourceAmount.getCurrency(); - auto const& uDstCurrency = saDstAmount.getCurrency(); + auto const& uSrcAsset = maxSourceAmount.asset(); + auto const& uDstAsset = saDstAmount.asset(); - // isZero() is XRP. FIX! - bool const bXRPDirect = uSrcCurrency.isZero() && uDstCurrency.isZero(); + bool const bXRPDirect = uSrcAsset.native() && uDstAsset.native(); if (!isLegalNet(saDstAmount) || !isLegalNet(maxSourceAmount)) return temBAD_AMOUNT; @@ -120,20 +133,19 @@ preflightHelper(PreflightContext const& ctx) << "bad dst amount: " << saDstAmount.getFullText(); return temBAD_AMOUNT; } - if (badCurrency() == uSrcCurrency || badCurrency() == uDstCurrency) + if (badCurrency() == uSrcAsset || badCurrency() == uDstAsset) { JLOG(j.trace()) << "Malformed transaction: " << "Bad currency."; return temBAD_CURRENCY; } - if (account == uDstAccountID && uSrcCurrency == uDstCurrency && !bPaths) + if (account == uDstAccountID && uSrcAsset == uDstAsset && !bPaths) { // You're signing yourself a payment. // If bPaths is true, you might be trying some arbitrage. JLOG(j.trace()) << "Malformed transaction: " << "Redundant payment from " << to_string(account) - << " to self without path for " - << to_string(uDstCurrency); + << " to self without path for " << to_string(uDstAsset); return temREDUNDANT; } if (bXRPDirect && bMax) @@ -143,11 +155,11 @@ preflightHelper(PreflightContext const& ctx) << "SendMax specified for XRP to XRP."; return temBAD_SEND_XRP_MAX; } - if (bXRPDirect && bPaths) + if ((bXRPDirect || bMPTDirect) && bPaths) { // XRP is sent without paths. JLOG(j.trace()) << "Malformed transaction: " - << "Paths specified for XRP to XRP."; + << "Paths specified for XRP to XRP or MPT to MPT."; return temBAD_SEND_XRP_PATHS; } if (bXRPDirect && partialPaymentAllowed) @@ -157,22 +169,23 @@ preflightHelper(PreflightContext const& ctx) << "Partial payment specified for XRP to XRP."; return temBAD_SEND_XRP_PARTIAL; } - if (bXRPDirect && limitQuality) + if ((bXRPDirect || bMPTDirect) && limitQuality) { // Consistent but redundant transaction. - JLOG(j.trace()) << "Malformed transaction: " - << "Limit quality specified for XRP to XRP."; + JLOG(j.trace()) + << "Malformed transaction: " + << "Limit quality specified for XRP to XRP or MPT to MPT."; return temBAD_SEND_XRP_LIMIT; } - if (bXRPDirect && !defaultPathsAllowed) + if ((bXRPDirect || bMPTDirect) && !defaultPathsAllowed) { // Consistent but redundant transaction. - JLOG(j.trace()) << "Malformed transaction: " - << "No ripple direct specified for XRP to XRP."; + JLOG(j.trace()) + << "Malformed transaction: " + << "No ripple direct specified for XRP to XRP or MPT to MPT."; return temBAD_SEND_XRP_NO_DIRECT; } - auto const deliverMin = tx[~sfDeliverMin]; if (deliverMin) { if (!partialPaymentAllowed) @@ -191,7 +204,7 @@ preflightHelper(PreflightContext const& ctx) << " amount. " << dMin.getFullText(); return temBAD_AMOUNT; } - if (dMin.issue() != saDstAmount.issue()) + if (dMin.asset() != saDstAmount.asset()) { JLOG(j.trace()) << "Malformed transaction: Dst issue differs " @@ -211,89 +224,8 @@ preflightHelper(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; - - auto& tx = ctx.tx; - auto& j = ctx.j; - - std::uint32_t const uTxFlags = tx.getFlags(); - - if (uTxFlags & tfUniversalMask) - { - JLOG(j.trace()) << "Malformed transaction: " - << "Invalid flags set."; - return temINVALID_FLAG; - } - - if (ctx.tx.isFieldPresent(sfDeliverMin) || ctx.tx.isFieldPresent(sfPaths)) - return temMALFORMED; - - auto const sendMax = ctx.tx[~sfSendMax]; - - if (sendMax && - (!sendMax->holds() || - sendMax->asset() != ctx.tx[sfAmount].asset())) - return temMALFORMED; - - STAmount const saDstAmount(tx.getFieldAmount(sfAmount)); - - auto const account = tx.getAccountID(sfAccount); - - auto const& uDstMptID = saDstAmount.get().getMptID(); - - 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 (sendMax && *sendMax <= beast::zero) - { - JLOG(j.trace()) << "Malformed transaction: " - << "bad max amount: " << sendMax->getFullText(); - return temBAD_AMOUNT; - } - 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(uDstMptID); - return temREDUNDANT; - } - - return preflight2(ctx); -} - -template -static TER -preclaimHelper( - PreclaimContext const& ctx, - std::size_t maxPathSize, - std::size_t maxPathLength); - -template <> TER -preclaimHelper( - PreclaimContext const& ctx, - std::size_t maxPathSize, - std::size_t maxPathLength) +Payment::preclaim(PreclaimContext const& ctx) { // Ripple if source or destination is non-native or if there are paths. std::uint32_t const uTxFlags = ctx.tx.getFlags(); @@ -364,9 +296,9 @@ preclaimHelper( { 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; @@ -376,119 +308,70 @@ preclaimHelper( return tesSUCCESS; } -template <> -TER -preclaimHelper(PreclaimContext const& ctx, std::size_t, std::size_t) -{ - 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) +Payment::doApply() { - AccountID const account = ctx.tx[sfAccount]; - auto const deliverMin = ctx.tx[~sfDeliverMin]; + 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(ctx_.tx.getFieldAmount(sfAmount)); + bool const bMPTDirect = saDstAmount.holds(); STAmount maxSourceAmount; if (sendMax) maxSourceAmount = *sendMax; - else if (saDstAmount.native()) + else if (saDstAmount.native() || bMPTDirect) maxSourceAmount = saDstAmount; else maxSourceAmount = STAmount( - Issue{saDstAmount.getCurrency(), account}, + Issue{saDstAmount.getCurrency(), account_}, saDstAmount.mantissa(), saDstAmount.exponent(), saDstAmount < beast::zero); - JLOG(ctx.journal.trace()) - << "maxSourceAmount=" << maxSourceAmount.getFullText() - << " saDstAmount=" << saDstAmount.getFullText(); + JLOG(j_.trace()) << "maxSourceAmount=" << maxSourceAmount.getFullText() + << " saDstAmount=" << saDstAmount.getFullText(); // Open a ledger for editing. auto const k = keylet::account(uDstAccountID); - SLE::pointer sleDst = ctx.view().peek(k); + SLE::pointer sleDst = view().peek(k); if (!sleDst) { std::uint32_t const seqno{ - ctx.view().rules().enabled(featureDeletableAccounts) - ? ctx.view().seq() - : 1}; + view().rules().enabled(featureDeletableAccounts) ? view().seq() + : 1}; // Create the account. sleDst = std::make_shared(k); sleDst->setAccountID(sfAccount, uDstAccountID); sleDst->setFieldU32(sfSequence, seqno); - ctx.view().insert(sleDst); + 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. - ctx.view().update(sleDst); + view().update(sleDst); } // Determine whether the destination requires deposit authorization. bool const reqDepositAuth = sleDst->getFlags() & lsfDepositAuth && - ctx.view().rules().enabled(featureDepositAuth); + view().rules().enabled(featureDepositAuth); - bool const depositPreauth = - ctx.view().rules().enabled(featureDepositPreauth); + bool const depositPreauth = view().rules().enabled(featureDepositPreauth); - bool const bRipple = paths || sendMax || !saDstAmount.native(); + bool const bRipple = + (paths || sendMax || !saDstAmount.native()) && !bMPTDirect; // If the destination has lsfDepositAuth set, then only direct XRP // payments (no intermediate steps) are allowed to the destination. @@ -506,10 +389,10 @@ applyHelper( // 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 (!ctx.view().exists( - keylet::depositPreauth(uDstAccountID, account))) + if (!view().exists( + keylet::depositPreauth(uDstAccountID, account_))) return tecNO_PERMISSION; } } @@ -518,26 +401,26 @@ applyHelper( rcInput.partialPaymentAllowed = partialPaymentAllowed; rcInput.defaultPathsAllowed = defaultPathsAllowed; rcInput.limitQuality = limitQuality; - rcInput.isLedgerOpen = ctx.view().open(); + rcInput.isLedgerOpen = view().open(); path::RippleCalc::Output rc; { - PaymentSandbox pv(&ctx.view()); - JLOG(ctx.journal.debug()) << "Entering RippleCalc in payment: " - << ctx.tx.getTransactionID(); + PaymentSandbox pv(&view()); + JLOG(j_.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 @@ -547,7 +430,7 @@ applyHelper( if (deliverMin && rc.actualAmountOut < *deliverMin) rc.setResult(tecPATH_PARTIAL); else - ctx.deliver(rc.actualAmountOut); + ctx_.deliver(rc.actualAmountOut); } auto terResult = rc.result(); @@ -560,12 +443,81 @@ applyHelper( terResult = tecPATH_DRY; return terResult; } + else if (bMPTDirect) + { + JLOG(j_.trace()) << " saDstAmount=" << saDstAmount.getFullText(); + + if (auto const ter = + requireAuth(view(), saDstAmount.get(), account_); + ter != tesSUCCESS) + return ter; + + if (auto const ter = + requireAuth(view(), saDstAmount.get(), uDstAccountID); + ter != tesSUCCESS) + return ter; + + if (auto const ter = canTransfer( + view(), saDstAmount.get(), account_, uDstAccountID); + ter != tesSUCCESS) + return ter; + + auto const& mptIssue = saDstAmount.get(); + auto const& issuer = mptIssue.getIssuer(); + + // Transfer rate + Rate rate{QUALITY_ONE}; + // Payment between the holders + if (account_ != issuer && uDstAccountID != issuer) + { + // If globally/individually locked then + // - can't send between holders + // - holder can send back to issuer + // - issuer can send to holder + if (isFrozen(view(), account_, mptIssue) || + isFrozen(view(), uDstAccountID, mptIssue)) + return tecLOCKED; + + // Get the rate for a payment between the holders. + rate = transferRate(view(), mptIssue.getMptID()); + } + + // Amount to deliver. + STAmount amountDeliver = saDstAmount; + // Factor in the transfer rate. + // No rounding. It'll change once MPT integrated into DEX. + STAmount requiredMaxSourceAmount = multiply(saDstAmount, rate); + + // Send more than the account wants to pay or less than + // the account wants to deliver (if no SendMax). + // Adjust the amount to deliver. + if (partialPaymentAllowed && requiredMaxSourceAmount > maxSourceAmount) + { + requiredMaxSourceAmount = maxSourceAmount; + // No rounding. It'll change once MPT integrated into DEX. + amountDeliver = divide(maxSourceAmount, rate); + } + + if (requiredMaxSourceAmount > maxSourceAmount || + (deliverMin && amountDeliver < *deliverMin)) + return tecPATH_PARTIAL; + + PaymentSandbox pv(&view()); + auto res = accountSendMPT( + pv, account_, uDstAccountID, amountDeliver, ctx_.journal); + if (res == tesSUCCESS) + pv.apply(ctx_.rawView()); + else if (res == tecINSUFFICIENT_FUNDS || res == tecPATH_DRY) + res = tecPATH_PARTIAL; + + return res; + } assert(saDstAmount.native()); // Direct XRP payment. - auto const sleSrc = ctx.view().peek(keylet::account(account)); + auto const sleSrc = view().peek(keylet::account(account_)); if (!sleSrc) return tefINTERNAL; @@ -574,21 +526,21 @@ applyHelper( auto const uOwnerCount = sleSrc->getFieldU32(sfOwnerCount); // This is the total reserve in drops. - auto const reserve = ctx.view().fees().accountReserve(uOwnerCount); + auto const reserve = 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 (priorBalance < saDstAmount.xrp() + mmm) + if (mPriorBalance < saDstAmount.xrp() + mmm) { // Vote no. However the transaction might succeed, if applied in // a different order. - JLOG(ctx.journal.trace()) << "Delay transaction: Insufficient funds: " - << " " << to_string(priorBalance) << " / " - << to_string(saDstAmount.xrp() + mmm) << " (" - << to_string(reserve) << ")"; + JLOG(j_.trace()) << "Delay transaction: Insufficient funds: " + << " " << to_string(mPriorBalance) << " / " + << to_string(saDstAmount.xrp() + mmm) << " (" + << to_string(reserve) << ")"; return tecUNFUNDED_PAYMENT; } @@ -620,13 +572,12 @@ applyHelper( // 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 (!ctx.view().exists( - keylet::depositPreauth(uDstAccountID, account))) + if (!view().exists(keylet::depositPreauth(uDstAccountID, account_))) { // Get the base reserve. - XRPAmount const dstReserve{ctx.view().fees().accountReserve(0)}; + XRPAmount const dstReserve{view().fees().accountReserve(0)}; if (saDstAmount > dstReserve || sleDst->getFieldAmount(sfBalance) > dstReserve) @@ -636,7 +587,7 @@ applyHelper( } // Do the arithmetic for the transfer and make the ledger change. - sleSrc->setFieldAmount(sfBalance, sourceBalance - saDstAmount); + sleSrc->setFieldAmount(sfBalance, mSourceBalance - saDstAmount); sleDst->setFieldAmount( sfBalance, sleDst->getFieldAmount(sfBalance) + saDstAmount); @@ -647,95 +598,4 @@ applyHelper( 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(ctx.tx.getFieldAmount(sfAmount)); - - JLOG(ctx.journal.trace()) << " saDstAmount=" << saDstAmount.getFullText(); - - if (auto const ter = - requireAuth(ctx.view(), saDstAmount.get(), account); - ter != tesSUCCESS) - return ter; - - if (auto const ter = - requireAuth(ctx.view(), saDstAmount.get(), uDstAccountID); - ter != tesSUCCESS) - return ter; - - if (auto const ter = canTransfer( - ctx.view(), saDstAmount.get(), account, uDstAccountID); - ter != tesSUCCESS) - return ter; - - auto const& mpt = saDstAmount.get(); - auto const& issuer = mpt.getIssuer(); - - if (account != issuer && uDstAccountID != issuer) - { - // If globally/individually locked then - // - can't send between holders - // - holder can send back to issuer - // - issuer can send to holder - if (isFrozen(ctx.view(), account, mpt) || - isFrozen(ctx.view(), uDstAccountID, mpt)) - return tecLOCKED; - - auto const sendMax = ctx.tx[~sfSendMax].value_or(saDstAmount); - // If SendMax is included then it should be greater or equal to - // the send amount plus the transfer fees if applicable. - // The payment can still fail if the sender has insufficient funds. - auto const amount = - multiply(saDstAmount, transferRate(ctx.view(), mpt.getMptID())); - if (multiply(saDstAmount, transferRate(ctx.view(), mpt.getMptID())) > - sendMax) - return tecPATH_PARTIAL; - } - - PaymentSandbox pv(&ctx.view()); - auto res = - accountSendMPT(pv, account, uDstAccountID, saDstAmount, ctx.journal); - if (res == tesSUCCESS) - pv.apply(ctx.rawView()); - else if (res == tecINSUFFICIENT_FUNDS || res == tecPATH_DRY) - res = tecPATH_PARTIAL; - - return res; -} - -NotTEC -Payment::preflight(PreflightContext const& ctx) -{ - return std::visit( - [&](TDelIss const&) { - return preflightHelper(ctx); - }, - ctx.tx[sfAmount].asset().value()); -} - -TER -Payment::preclaim(PreclaimContext const& ctx) -{ - return std::visit( - [&](TDelIss const&) { - return preclaimHelper(ctx, MaxPathSize, MaxPathLength); - }, - ctx.tx[sfAmount].asset().value()); -} - -TER -Payment::doApply() -{ - return std::visit( - [&](TDelIss const&) { - return applyHelper(ctx_, mPriorBalance, mSourceBalance); - }, - ctx_.tx[sfAmount].asset().value()); -} - } // namespace ripple diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 472276d8857..5ec7d598f75 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -451,7 +451,7 @@ rippleCredit( beast::Journal j); [[nodiscard]] TER -rippleMPTCredit( +rippleCreditMPT( ApplyView& view, AccountID const& uSenderID, AccountID const& uReceiverID, diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 19fc424a999..8ebe6d56157 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -1353,7 +1353,7 @@ rippleSendMPT( // Safe to get MPT since rippleSendMPT is only called by accountSendMPT auto const issuer = saAmount.getIssuer(); - if (uSenderID == issuer || uReceiverID == issuer || issuer == noAccount()) + if (uSenderID == issuer || uReceiverID == issuer) { // if sender is issuer, check that the new OutstandingAmount will not // exceed MaximumAmount @@ -1376,7 +1376,7 @@ rippleSendMPT( // Direct send: redeeming IOUs and/or sending own IOUs. auto const ter = - rippleMPTCredit(view, uSenderID, uReceiverID, saAmount, j); + rippleCreditMPT(view, uSenderID, uReceiverID, saAmount, j); if (ter != tesSUCCESS) return ter; saActual = saAmount; @@ -1385,7 +1385,8 @@ rippleSendMPT( // Sending 3rd party MPTs: transit. if (auto const sle = - view.read(keylet::mptIssuance(saAmount.get().getMptID()))) + view.read(keylet::mptIssuance(saAmount.get().getMptID())); + sle != nullptr) { saActual = (waiveFee == WaiveTransferFee::Yes) ? saAmount @@ -1399,14 +1400,14 @@ rippleSendMPT( << " cost=" << saActual.getFullText(); if (auto const terResult = - rippleMPTCredit(view, issuer, uReceiverID, saAmount, j); + rippleCreditMPT(view, issuer, uReceiverID, saAmount, j); terResult != tesSUCCESS) return terResult; - return rippleMPTCredit(view, uSenderID, issuer, saActual, j); + return rippleCreditMPT(view, uSenderID, issuer, saActual, j); } - return tecINTERNAL; + return tecOBJECT_NOT_FOUND; } TER @@ -1875,7 +1876,7 @@ deleteAMMTrustLine( } TER -rippleMPTCredit( +rippleCreditMPT( ApplyView& view, AccountID const& uSenderID, AccountID const& uReceiverID, From 879664145c008b4506c43fd22f096f55b364b4b7 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Wed, 9 Oct 2024 12:17:05 -0400 Subject: [PATCH 22/58] Fix non-unity build --- src/xrpld/app/tx/detail/Payment.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 572711fbd39..73a136b8408 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include From 757b55a75aa74a43f282a86be662c4a030666edb Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Wed, 9 Oct 2024 12:32:14 -0400 Subject: [PATCH 23/58] Update src/libxrpl/protocol/TxFormats.cpp Co-authored-by: Ed Hennis --- src/libxrpl/protocol/TxFormats.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index 2b506ffc0e6..bbd67342e1a 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -532,16 +532,16 @@ TxFormats::TxFormats() }, commonFields); - add(jss::MPTokenAuthorize, - ttMPTOKEN_AUTHORIZE, + add(jss::MPTokenIssuanceSet, + ttMPTOKEN_ISSUANCE_SET, { {sfMPTokenIssuanceID, soeREQUIRED}, {sfMPTokenHolder, soeOPTIONAL}, }, commonFields); - add(jss::MPTokenIssuanceSet, - ttMPTOKEN_ISSUANCE_SET, + add(jss::MPTokenAuthorize, + ttMPTOKEN_AUTHORIZE, { {sfMPTokenIssuanceID, soeREQUIRED}, {sfMPTokenHolder, soeOPTIONAL}, From 11aa41c517507ea15ffe83508d509c51a08716a1 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 10 Oct 2024 08:52:03 -0400 Subject: [PATCH 24/58] New SField flag to override hex input/output formatting to use decimal (#39) * New SField flag to override hex input/output formatting to use decimal * Fix formatting --- include/xrpl/protocol/SField.h | 1 + src/libxrpl/protocol/SField.cpp | 6 +++--- src/libxrpl/protocol/STInteger.cpp | 3 +-- src/libxrpl/protocol/STParsedJSON.cpp | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 849332ecc3c..db6ffd8d095 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -148,6 +148,7 @@ class SField sMD_DeleteFinal = 0x04, // final value when it is deleted sMD_Create = 0x08, // value when it's created sMD_Always = 0x10, // value when node containing it is affected at all + sMD_BaseTen = 0x20, sMD_Default = sMD_ChangeOrig | sMD_ChangeNew | sMD_DeleteFinal | sMD_Create }; diff --git a/src/libxrpl/protocol/SField.cpp b/src/libxrpl/protocol/SField.cpp index 85aca815acc..5a96afb937f 100644 --- a/src/libxrpl/protocol/SField.cpp +++ b/src/libxrpl/protocol/SField.cpp @@ -194,9 +194,9 @@ 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(sfMPTAmount, "MPTAmount", UINT64, 26); +CONSTRUCT_TYPED_SFIELD(sfMaximumAmount, "MaximumAmount", UINT64, 24, SField::sMD_BaseTen); +CONSTRUCT_TYPED_SFIELD(sfOutstandingAmount, "OutstandingAmount", UINT64, 25, SField::sMD_BaseTen); +CONSTRUCT_TYPED_SFIELD(sfMPTAmount, "MPTAmount", UINT64, 26, SField::sMD_BaseTen); // 128-bit CONSTRUCT_TYPED_SFIELD(sfEmailHash, "EmailHash", UINT128, 1); diff --git a/src/libxrpl/protocol/STInteger.cpp b/src/libxrpl/protocol/STInteger.cpp index 2db4ff32c34..82ea6c7a9c8 100644 --- a/src/libxrpl/protocol/STInteger.cpp +++ b/src/libxrpl/protocol/STInteger.cpp @@ -205,8 +205,7 @@ Json::Value STUInt64::getJson(JsonOptions) const return str; }; - if (auto const& fName = getFName(); fName == sfMaximumAmount || - fName == sfOutstandingAmount || fName == sfMPTAmount) + if (auto const& fName = getFName(); fName.shouldMeta(SField::sMD_BaseTen)) { return convertToString(value_, 10); // Convert to base 10 } diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index 1ca4e83d9e6..c0d0fc8f059 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -398,8 +398,8 @@ parseLeaf( std::uint64_t val; - bool const useBase10 = field == sfMaximumAmount || - field == sfOutstandingAmount || field == sfMPTAmount; + bool const useBase10 = + field.shouldMeta(SField::sMD_BaseTen); // if the field is amount, serialize as base 10 auto [p, ec] = std::from_chars( From eac438c4264c5dfea8e92af1a480b5392a4686d7 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 10 Oct 2024 09:09:32 -0400 Subject: [PATCH 25/58] Change maxAmount in the test to uint64 for better visualization * Add unit-test with max amount and transfer fee --- src/test/app/MPToken_test.cpp | 53 ++++++++++++++++++++++++++++------- src/test/jtx/impl/mpt.cpp | 2 +- src/test/jtx/mpt.h | 2 +- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 800aaa9496d..1574741d0e0 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -53,7 +53,7 @@ class MPToken_test : public beast::unit_test::suite // tries to set a txfee while not enabling in the flag mptAlice.create( - {.maxAmt = "100", + {.maxAmt = 100, .assetScale = 0, .transferFee = 1, .metadata = "test", @@ -61,7 +61,7 @@ class MPToken_test : public beast::unit_test::suite // tries to set a txfee greater than max mptAlice.create( - {.maxAmt = "100", + {.maxAmt = 100, .assetScale = 0, .transferFee = maxTransferFee + 1, .metadata = "test", @@ -70,7 +70,7 @@ class MPToken_test : public beast::unit_test::suite // tries to set a txfee while not enabling transfer mptAlice.create( - {.maxAmt = "100", + {.maxAmt = 100, .assetScale = 0, .transferFee = maxTransferFee, .metadata = "test", @@ -78,7 +78,7 @@ class MPToken_test : public beast::unit_test::suite // empty metadata returns error mptAlice.create( - {.maxAmt = "100", + {.maxAmt = 100, .assetScale = 0, .transferFee = 0, .metadata = "", @@ -86,7 +86,7 @@ class MPToken_test : public beast::unit_test::suite // MaximumAmout of 0 returns error mptAlice.create( - {.maxAmt = "0", + {.maxAmt = 0, .assetScale = 1, .transferFee = 1, .metadata = "test", @@ -94,13 +94,13 @@ class MPToken_test : public beast::unit_test::suite // MaximumAmount larger than 63 bit returns error mptAlice.create( - {.maxAmt = "18446744073709551600", // FFFFFFFFFFFFFFF0 + {.maxAmt = 0xFFFF'FFFF'FFFF'FFF0, // 18'446'744'073'709'551'600 .assetScale = 0, .transferFee = 0, .metadata = "test", .err = temMALFORMED}); mptAlice.create( - {.maxAmt = "9223372036854775808", // 8000000000000000 + {.maxAmt = 0x8000'0000'0000'0000, // 9'223'372'036'854'775'808 .assetScale = 0, .transferFee = 0, .metadata = "test", @@ -122,7 +122,7 @@ class MPToken_test : public beast::unit_test::suite Env env{*this, features}; MPTTester mptAlice(env, alice); mptAlice.create( - {.maxAmt = "9223372036854775807", // 7FFFFFFFFFFFFFFF + {.maxAmt = 0x7FFF'FFFF'FFFF'FFFF, // 9'223'372'036'854'775'807 .assetScale = 1, .transferFee = 10, .metadata = "123", @@ -1087,7 +1087,7 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {&bob}}); mptAlice.create( - {.maxAmt = "100", + {.maxAmt = 100, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); @@ -1134,6 +1134,39 @@ class MPToken_test : public beast::unit_test::suite BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); } + // Pay maximum amount with the transfer fee, SendMax, and + // partial payment + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create( + {.maxAmt = 10'000, + .transferFee = 100, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer}); + auto const MPT = mptAlice["MPT"]; + + mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = &carol}); + + // issuer sends holder the max amount allowed + mptAlice.pay(alice, bob, 10'000); + + // payment between the holders + env(pay(bob, carol, MPT(10'000)), + sendmax(MPT(10'000)), + txflags(tfPartialPayment)); + + // payment between the holders fails without + // partial payment + env(pay(bob, carol, MPT(10'000)), + sendmax(MPT(10'000)), + ter(tecPATH_PARTIAL)); + } + // Issuer fails trying to send fund after issuance was destroyed { Env env{*this, features}; @@ -1190,7 +1223,7 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); mptAlice.create( - {.maxAmt = "100", .ownerCount = 1, .flags = tfMPTCanTransfer}); + {.maxAmt = 100, .ownerCount = 1, .flags = tfMPTCanTransfer}); mptAlice.authorize({.account = &bob}); mptAlice.authorize({.account = &carol}); diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index c4d16f1317b..18c151ea174 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -97,7 +97,7 @@ MPTTester::create(const MPTCreate& arg) if (arg.metadata) jv[sfMPTokenMetadata.jsonName] = strHex(*arg.metadata); if (arg.maxAmt) - jv[sfMaximumAmount.jsonName] = *arg.maxAmt; + jv[sfMaximumAmount.jsonName] = std::to_string(*arg.maxAmt); if (submit(arg, jv) != tesSUCCESS) { // Verify issuance doesn't exist diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 0e61b4ee2b6..49e2004fd77 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -97,7 +97,7 @@ struct MPTConstr struct MPTCreate { - std::optional maxAmt = std::nullopt; + std::optional maxAmt = std::nullopt; std::optional assetScale = std::nullopt; std::optional transferFee = std::nullopt; std::optional metadata = std::nullopt; From 9d513892851540c05a7a84fab0922ff238d6a6e2 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 10 Oct 2024 10:57:24 -0400 Subject: [PATCH 26/58] Refactor Clawback plus other minor refactoring --- include/xrpl/protocol/Rate.h | 2 +- src/xrpld/app/tx/detail/Clawback.cpp | 135 ++++++++++++++------------- src/xrpld/ledger/detail/View.cpp | 3 +- 3 files changed, 71 insertions(+), 69 deletions(-) diff --git a/include/xrpl/protocol/Rate.h b/include/xrpl/protocol/Rate.h index f58aae6e305..6970d9c16a8 100644 --- a/include/xrpl/protocol/Rate.h +++ b/include/xrpl/protocol/Rate.h @@ -87,7 +87,7 @@ STAmount divideRound( STAmount const& amount, Rate const& rate, - Asset const& issue, + Asset const& asset, bool roundUp); namespace nft { diff --git a/src/xrpld/app/tx/detail/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index 4b6d513b152..2ea9878e590 100644 --- a/src/xrpld/app/tx/detail/Clawback.cpp +++ b/src/xrpld/app/tx/detail/Clawback.cpp @@ -36,15 +36,6 @@ template <> NotTEC preflightHelper(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureClawback)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfClawbackMask) - return temINVALID_FLAG; - if (ctx.tx.isFieldPresent(sfMPTokenHolder)) return temMALFORMED; @@ -57,31 +48,22 @@ preflightHelper(PreflightContext const& ctx) if (issuer == holder || isXRP(clawAmount) || clawAmount <= beast::zero) return temBAD_AMOUNT; - return preflight2(ctx); + return tesSUCCESS; } template <> NotTEC preflightHelper(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureClawback)) + if (!ctx.rules.enabled(featureMPTokensV1)) return temDISABLED; auto const mptHolder = ctx.tx[~sfMPTokenHolder]; auto const clawAmount = 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; @@ -90,30 +72,49 @@ preflightHelper(PreflightContext const& ctx) clawAmount <= beast::zero) return temBAD_AMOUNT; + return tesSUCCESS; +} + +NotTEC +Clawback::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureClawback)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfClawbackMask) + return temINVALID_FLAG; + + if (auto const ret = std::visit( + [&](T const&) { return preflightHelper(ctx); }, + ctx.tx[sfAmount].asset().value()); + !isTesSuccess(ret)) + return ret; + return preflight2(ctx); } template static TER -preclaimHelper(PreclaimContext const& ctx); +preclaimHelper( + PreclaimContext const& ctx, + SLE const& sleIssuer, + AccountID const& issuer, + AccountID const& holder, + STAmount const& clawAmount); template <> TER -preclaimHelper(PreclaimContext const& ctx) +preclaimHelper( + PreclaimContext const& ctx, + SLE const& sleIssuer, + AccountID const& issuer, + AccountID const& holder, + STAmount const& clawAmount) { - AccountID const issuer = ctx.tx[sfAccount]; - STAmount const clawAmount = ctx.tx[sfAmount]; - AccountID const& holder = clawAmount.getIssuer(); - - 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; - - std::uint32_t const issuerFlagsIn = sleIssuer->getFieldU32(sfFlags); + std::uint32_t const issuerFlagsIn = sleIssuer.getFieldU32(sfFlags); // If AllowTrustLineClawback is not set or NoFreeze is set, return no // permission @@ -159,20 +160,13 @@ preclaimHelper(PreclaimContext const& ctx) template <> TER -preclaimHelper(PreclaimContext const& ctx) +preclaimHelper( + PreclaimContext const& ctx, + SLE const& sleIssuer, + AccountID const& issuer, + AccountID const& holder, + STAmount const& clawAmount) { - AccountID const issuer = ctx.tx[sfAccount]; - auto const clawAmount = 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.get().getMptID()); auto const sleIssuance = ctx.view.read(issuanceKey); @@ -200,6 +194,31 @@ preclaimHelper(PreclaimContext const& ctx) return tesSUCCESS; } +TER +Clawback::preclaim(PreclaimContext const& ctx) +{ + AccountID const issuer = ctx.tx[sfAccount]; + auto const clawAmount = ctx.tx[sfAmount]; + AccountID const holder = clawAmount.holds() + ? clawAmount.getIssuer() + : 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; + + return std::visit( + [&](T const&) { + return preclaimHelper( + ctx, *sleIssuer, issuer, holder, clawAmount); + }, + ctx.tx[sfAmount].asset().value()); +} + template static TER applyHelper(ApplyContext& ctx); @@ -208,7 +227,7 @@ template <> TER applyHelper(ApplyContext& ctx) { - AccountID const& issuer = ctx.tx[sfAccount]; + AccountID const issuer = ctx.tx[sfAccount]; STAmount clawAmount = ctx.tx[sfAmount]; AccountID const holder = clawAmount.getIssuer(); // cannot be reference @@ -239,7 +258,7 @@ template <> TER applyHelper(ApplyContext& ctx) { - AccountID const& issuer = ctx.tx[sfAccount]; + AccountID const issuer = ctx.tx[sfAccount]; auto clawAmount = ctx.tx[sfAmount]; AccountID const holder = ctx.tx[sfMPTokenHolder]; @@ -260,22 +279,6 @@ applyHelper(ApplyContext& ctx) ctx.journal); } -NotTEC -Clawback::preflight(PreflightContext const& ctx) -{ - return std::visit( - [&](T const&) { return preflightHelper(ctx); }, - ctx.tx[sfAmount].asset().value()); -} - -TER -Clawback::preclaim(PreclaimContext const& ctx) -{ - return std::visit( - [&](T const&) { return preclaimHelper(ctx); }, - ctx.tx[sfAmount].asset().value()); -} - TER Clawback::doApply() { diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 8ebe6d56157..01d2b7134b6 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -1385,8 +1385,7 @@ rippleSendMPT( // Sending 3rd party MPTs: transit. if (auto const sle = - view.read(keylet::mptIssuance(saAmount.get().getMptID())); - sle != nullptr) + view.read(keylet::mptIssuance(saAmount.get().getMptID()))) { saActual = (waiveFee == WaiveTransferFee::Yes) ? saAmount From 8774b903f456f3fa076f777a146f9653bc2ca587 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Fri, 11 Oct 2024 13:27:58 -0400 Subject: [PATCH 27/58] Fix requireAuth (#40) * improve requireAuth * var naming * rename vars * update canTransfer --- src/xrpld/ledger/detail/View.cpp | 41 ++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 01d2b7134b6..afae132c1fc 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -1720,15 +1720,29 @@ requireAuth( AccountID const& account) { auto const mptID = keylet::mptIssuance(mptIssue.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}; - } + auto const sleIssuance = view.read(mptID); + + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + auto const mptIssuer = sleIssuance->getAccountID(sfIssuer); + + // issuer is always "authorized" + if (mptIssuer == account) + return tesSUCCESS; + + auto const mptokenID = keylet::mptoken(mptID.key, account); + auto const sleToken = view.read(mptokenID); + + // if account has no MPToken, fail + if (!sleToken) + return tecNO_AUTH; + + // mptoken must be authorized if issuance enabled requireAuth + if (sleIssuance->getFieldU32(sfFlags) & lsfMPTRequireAuth && + !(sleToken->getFlags() & lsfMPTAuthorized)) + return tecNO_AUTH; + return tesSUCCESS; } @@ -1740,10 +1754,13 @@ canTransfer( AccountID const& to) { auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); - if (auto const sle = view.read(mptID); - sle && !(sle->getFieldU32(sfFlags) & lsfMPTCanTransfer)) + auto const sleIssuance = view.read(mptID); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + if (!(sleIssuance->getFieldU32(sfFlags) & lsfMPTCanTransfer)) { - if (from != (*sle)[sfIssuer] && to != (*sle)[sfIssuer]) + if (from != (*sleIssuance)[sfIssuer] && to != (*sleIssuance)[sfIssuer]) return TER{tecNO_AUTH}; } return tesSUCCESS; From 4bcf463cd60f9856743151fa98518301b7f53576 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Fri, 11 Oct 2024 15:41:03 -0400 Subject: [PATCH 28/58] Address reviewer's feedback --- include/xrpl/protocol/TxFlags.h | 1 + src/xrpld/app/tx/detail/Payment.cpp | 226 ++++++++++++++-------------- src/xrpld/ledger/detail/View.cpp | 59 +++----- 3 files changed, 137 insertions(+), 149 deletions(-) diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 8b5e430ecb7..5b26488ab12 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -106,6 +106,7 @@ constexpr std::uint32_t tfPartialPayment = 0x00020000; constexpr std::uint32_t tfLimitQuality = 0x00040000; constexpr std::uint32_t tfPaymentMask = ~(tfUniversal | tfPartialPayment | tfLimitQuality | tfNoRippleDirect); +constexpr std::uint32_t tfMPTPaymentMask = ~(tfUniversal | tfPartialPayment); // TrustSet flags: constexpr std::uint32_t tfSetfAuth = 0x00010000; diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 73a136b8408..19952df3040 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -44,6 +44,24 @@ Payment::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)}; } +STAmount +getMaxSourceAmount( + AccountID const& account, + STAmount const& dstAmount, + std::optional const& sendMax) +{ + if (sendMax) + return *sendMax; + else if (dstAmount.native() || dstAmount.holds()) + return dstAmount; + else + return STAmount( + Issue{dstAmount.get().currency, account}, + dstAmount.mantissa(), + dstAmount.exponent(), + dstAmount < beast::zero); +} + NotTEC Payment::preflight(PreflightContext const& ctx) { @@ -53,124 +71,113 @@ Payment::preflight(PreflightContext const& ctx) auto& tx = ctx.tx; auto& j = ctx.j; - STAmount const saDstAmount(tx.getFieldAmount(sfAmount)); - bool const bMPTDirect = saDstAmount.holds(); + STAmount const dstAmount(tx.getFieldAmount(sfAmount)); + bool const mptDirect = dstAmount.holds(); - if (bMPTDirect && !ctx.rules.enabled(featureMPTokensV1)) + if (mptDirect && !ctx.rules.enabled(featureMPTokensV1)) return temDISABLED; - std::uint32_t const uTxFlags = tx.getFlags(); + std::uint32_t const txFlags = tx.getFlags(); - std::uint32_t paymentMask = - bMPTDirect ? ~(tfUniversal | tfPartialPayment) : tfPaymentMask; + std::uint32_t paymentMask = mptDirect ? tfMPTPaymentMask : tfPaymentMask; - if (uTxFlags & paymentMask) + if (txFlags & paymentMask) { JLOG(j.trace()) << "Malformed transaction: " << "Invalid flags set."; return temINVALID_FLAG; } - if (bMPTDirect && ctx.tx.isFieldPresent(sfPaths)) + if (mptDirect && ctx.tx.isFieldPresent(sfPaths)) return temMALFORMED; - bool const partialPaymentAllowed = uTxFlags & tfPartialPayment; - bool const limitQuality = uTxFlags & tfLimitQuality; - bool const defaultPathsAllowed = !(uTxFlags & tfNoRippleDirect); - bool const bPaths = tx.isFieldPresent(sfPaths); - bool const bMax = tx.isFieldPresent(sfSendMax); + bool const partialPaymentAllowed = txFlags & tfPartialPayment; + bool const limitQuality = txFlags & tfLimitQuality; + bool const defaultPathsAllowed = !(txFlags & tfNoRippleDirect); + bool const hasPaths = tx.isFieldPresent(sfPaths); + bool const hasMax = tx.isFieldPresent(sfSendMax); auto const deliverMin = tx[~sfDeliverMin]; - STAmount maxSourceAmount; auto const account = tx.getAccountID(sfAccount); + STAmount const maxSourceAmount = + getMaxSourceAmount(account, dstAmount, tx[~sfSendMax]); - if (bMax) - maxSourceAmount = tx.getFieldAmount(sfSendMax); - else if (saDstAmount.native() || bMPTDirect) - maxSourceAmount = saDstAmount; - else - maxSourceAmount = STAmount( - Issue{saDstAmount.get().currency, account}, - saDstAmount.mantissa(), - saDstAmount.exponent(), - saDstAmount < beast::zero); - - if ((bMPTDirect && saDstAmount.asset() != maxSourceAmount.asset()) || - (!bMPTDirect && maxSourceAmount.holds())) + if ((mptDirect && dstAmount.asset() != maxSourceAmount.asset()) || + (!mptDirect && maxSourceAmount.holds())) { JLOG(j.trace()) << "Malformed transaction: " - << "inconsistent issues: " << saDstAmount.getFullText() + << "inconsistent issues: " << dstAmount.getFullText() << " " << maxSourceAmount.getFullText() << " " << deliverMin.value_or(STAmount{}).getFullText(); return temMALFORMED; } - auto const& uSrcAsset = maxSourceAmount.asset(); - auto const& uDstAsset = saDstAmount.asset(); + auto const& srcAsset = maxSourceAmount.asset(); + auto const& dstAsset = dstAmount.asset(); - bool const bXRPDirect = uSrcAsset.native() && uDstAsset.native(); + bool const xrpDirect = srcAsset.native() && dstAsset.native(); - if (!isLegalNet(saDstAmount) || !isLegalNet(maxSourceAmount)) + if (!isLegalNet(dstAmount) || !isLegalNet(maxSourceAmount)) return temBAD_AMOUNT; - auto const uDstAccountID = tx.getAccountID(sfDestination); + auto const dstAccountID = tx.getAccountID(sfDestination); - if (!uDstAccountID) + if (!dstAccountID) { JLOG(j.trace()) << "Malformed transaction: " << "Payment destination account not specified."; return temDST_NEEDED; } - if (bMax && maxSourceAmount <= beast::zero) + if (hasMax && maxSourceAmount <= beast::zero) { JLOG(j.trace()) << "Malformed transaction: " << "bad max amount: " << maxSourceAmount.getFullText(); return temBAD_AMOUNT; } - if (saDstAmount <= beast::zero) + if (dstAmount <= beast::zero) { JLOG(j.trace()) << "Malformed transaction: " - << "bad dst amount: " << saDstAmount.getFullText(); + << "bad dst amount: " << dstAmount.getFullText(); return temBAD_AMOUNT; } - if (badCurrency() == uSrcAsset || badCurrency() == uDstAsset) + if (badCurrency() == srcAsset || badCurrency() == dstAsset) { JLOG(j.trace()) << "Malformed transaction: " << "Bad currency."; return temBAD_CURRENCY; } - if (account == uDstAccountID && uSrcAsset == uDstAsset && !bPaths) + if (account == dstAccountID && srcAsset == dstAsset && !hasPaths) { // You're signing yourself a payment. - // If bPaths is true, you might be trying some arbitrage. + // If hasPaths is true, you might be trying some arbitrage. JLOG(j.trace()) << "Malformed transaction: " << "Redundant payment from " << to_string(account) - << " to self without path for " << to_string(uDstAsset); + << " to self without path for " << to_string(dstAsset); return temREDUNDANT; } - if (bXRPDirect && bMax) + if (xrpDirect && hasMax) { // Consistent but redundant transaction. JLOG(j.trace()) << "Malformed transaction: " << "SendMax specified for XRP to XRP."; return temBAD_SEND_XRP_MAX; } - if ((bXRPDirect || bMPTDirect) && bPaths) + if ((xrpDirect || mptDirect) && hasPaths) { // XRP is sent without paths. JLOG(j.trace()) << "Malformed transaction: " << "Paths specified for XRP to XRP or MPT to MPT."; return temBAD_SEND_XRP_PATHS; } - if (bXRPDirect && partialPaymentAllowed) + if (xrpDirect && partialPaymentAllowed) { // Consistent but redundant transaction. JLOG(j.trace()) << "Malformed transaction: " << "Partial payment specified for XRP to XRP."; return temBAD_SEND_XRP_PARTIAL; } - if ((bXRPDirect || bMPTDirect) && limitQuality) + if ((xrpDirect || mptDirect) && limitQuality) { // Consistent but redundant transaction. JLOG(j.trace()) @@ -178,7 +185,7 @@ Payment::preflight(PreflightContext const& ctx) << "Limit quality specified for XRP to XRP or MPT to MPT."; return temBAD_SEND_XRP_LIMIT; } - if ((bXRPDirect || bMPTDirect) && !defaultPathsAllowed) + if ((xrpDirect || mptDirect) && !defaultPathsAllowed) { // Consistent but redundant transaction. JLOG(j.trace()) @@ -205,7 +212,7 @@ Payment::preflight(PreflightContext const& ctx) << " amount. " << dMin.getFullText(); return temBAD_AMOUNT; } - if (dMin.asset() != saDstAmount.asset()) + if (dMin.asset() != dstAmount.asset()) { JLOG(j.trace()) << "Malformed transaction: Dst issue differs " @@ -213,7 +220,7 @@ Payment::preflight(PreflightContext const& ctx) << jss::DeliverMin.c_str() << ". " << dMin.getFullText(); return temBAD_AMOUNT; } - if (dMin > saDstAmount) + if (dMin > dstAmount) { JLOG(j.trace()) << "Malformed transaction: Dst amount less than " @@ -228,22 +235,22 @@ Payment::preflight(PreflightContext const& ctx) TER Payment::preclaim(PreclaimContext const& ctx) { - // Ripple if source or destination is non-native or if there are paths. - std::uint32_t const uTxFlags = ctx.tx.getFlags(); - bool const partialPaymentAllowed = uTxFlags & tfPartialPayment; - auto const paths = ctx.tx.isFieldPresent(sfPaths); + // Ripple if source or destination is non-native or if there are hasPaths. + std::uint32_t const txFlags = ctx.tx.getFlags(); + bool const partialPaymentAllowed = txFlags & tfPartialPayment; + auto const hasPaths = ctx.tx.isFieldPresent(sfPaths); auto const sendMax = ctx.tx[~sfSendMax]; - AccountID const uDstAccountID(ctx.tx[sfDestination]); - STAmount const saDstAmount(ctx.tx[sfAmount]); + AccountID const dstAccountID(ctx.tx[sfDestination]); + STAmount const dstAmount(ctx.tx[sfAmount]); - auto const k = keylet::account(uDstAccountID); + auto const k = keylet::account(dstAccountID); auto const sleDst = ctx.view.read(k); if (!sleDst) { // Destination account does not exist. - if (!saDstAmount.native()) + if (!dstAmount.native()) { JLOG(ctx.j.trace()) << "Delay transaction: Destination account does not exist."; @@ -263,7 +270,7 @@ Payment::preclaim(PreclaimContext const& ctx) // transaction would succeed. return telNO_DST_PARTIAL; } - else if (saDstAmount < STAmount(ctx.view.fees().accountReserve(0))) + else if (dstAmount < STAmount(ctx.view.fees().accountReserve(0))) { // accountReserve is the minimum amount that an account can have. // Reserve is not scaled by load. @@ -293,7 +300,7 @@ Payment::preclaim(PreclaimContext const& ctx) } // Payment with at least one intermediate step and uses transitive balances. - if ((paths || sendMax || !saDstAmount.native()) && ctx.view.open()) + if ((hasPaths || sendMax || !dstAmount.native()) && ctx.view.open()) { STPathSet const& paths = ctx.tx.getFieldPathSet(sfPaths); @@ -314,34 +321,25 @@ Payment::doApply() { 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(); - bool const partialPaymentAllowed = uTxFlags & tfPartialPayment; - bool const limitQuality = uTxFlags & tfLimitQuality; - bool const defaultPathsAllowed = !(uTxFlags & tfNoRippleDirect); - auto const paths = ctx_.tx.isFieldPresent(sfPaths); + // Ripple if source or destination is non-native or if there are hasPaths. + std::uint32_t const txFlags = ctx_.tx.getFlags(); + bool const partialPaymentAllowed = txFlags & tfPartialPayment; + bool const limitQuality = txFlags & tfLimitQuality; + bool const defaultPathsAllowed = !(txFlags & tfNoRippleDirect); + auto const hasPaths = ctx_.tx.isFieldPresent(sfPaths); auto const sendMax = ctx_.tx[~sfSendMax]; - AccountID const uDstAccountID(ctx_.tx.getAccountID(sfDestination)); - STAmount const saDstAmount(ctx_.tx.getFieldAmount(sfAmount)); - bool const bMPTDirect = saDstAmount.holds(); - STAmount maxSourceAmount; - if (sendMax) - maxSourceAmount = *sendMax; - else if (saDstAmount.native() || bMPTDirect) - maxSourceAmount = saDstAmount; - else - maxSourceAmount = STAmount( - Issue{saDstAmount.getCurrency(), account_}, - saDstAmount.mantissa(), - saDstAmount.exponent(), - saDstAmount < beast::zero); + AccountID const dstAccountID(ctx_.tx.getAccountID(sfDestination)); + STAmount const dstAmount(ctx_.tx.getFieldAmount(sfAmount)); + bool const mptDirect = dstAmount.holds(); + STAmount const maxSourceAmount = + getMaxSourceAmount(account_, dstAmount, sendMax); JLOG(j_.trace()) << "maxSourceAmount=" << maxSourceAmount.getFullText() - << " saDstAmount=" << saDstAmount.getFullText(); + << " dstAmount=" << dstAmount.getFullText(); // Open a ledger for editing. - auto const k = keylet::account(uDstAccountID); + auto const k = keylet::account(dstAccountID); SLE::pointer sleDst = view().peek(k); if (!sleDst) @@ -352,7 +350,7 @@ Payment::doApply() // Create the account. sleDst = std::make_shared(k); - sleDst->setAccountID(sfAccount, uDstAccountID); + sleDst->setAccountID(sfAccount, dstAccountID); sleDst->setFieldU32(sfSequence, seqno); view().insert(sleDst); @@ -371,15 +369,15 @@ Payment::doApply() bool const depositPreauth = view().rules().enabled(featureDepositPreauth); - bool const bRipple = - (paths || sendMax || !saDstAmount.native()) && !bMPTDirect; + bool const ripple = + (hasPaths || sendMax || !dstAmount.native()) && !mptDirect; // If the destination has lsfDepositAuth set, then only direct XRP // payments (no intermediate steps) are allowed to the destination. - if (!depositPreauth && bRipple && reqDepositAuth) + if (!depositPreauth && ripple && reqDepositAuth) return tecNO_PERMISSION; - if (bRipple) + if (ripple) { // Ripple payment with at least one intermediate step and uses // transitive balances. @@ -390,10 +388,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 (dstAccountID != account_) { if (!view().exists( - keylet::depositPreauth(uDstAccountID, account_))) + keylet::depositPreauth(dstAccountID, account_))) return tecNO_PERMISSION; } } @@ -412,8 +410,8 @@ Payment::doApply() rc = path::RippleCalc::rippleCalculate( pv, maxSourceAmount, - saDstAmount, - uDstAccountID, + dstAmount, + dstAccountID, account_, ctx_.tx.getFieldPathSet(sfPaths), ctx_.app.logs(), @@ -426,7 +424,7 @@ Payment::doApply() // TODO: is this right? If the amount is the correct amount, was // the delivered amount previously set? - if (rc.result() == tesSUCCESS && rc.actualAmountOut != saDstAmount) + if (rc.result() == tesSUCCESS && rc.actualAmountOut != dstAmount) { if (deliverMin && rc.actualAmountOut < *deliverMin) rc.setResult(tecPATH_PARTIAL); @@ -444,39 +442,37 @@ Payment::doApply() terResult = tecPATH_DRY; return terResult; } - else if (bMPTDirect) + else if (mptDirect) { - JLOG(j_.trace()) << " saDstAmount=" << saDstAmount.getFullText(); + JLOG(j_.trace()) << " dstAmount=" << dstAmount.getFullText(); + auto const& mptIssue = dstAmount.get(); - if (auto const ter = - requireAuth(view(), saDstAmount.get(), account_); + if (auto const ter = requireAuth(view(), mptIssue, account_); ter != tesSUCCESS) return ter; - if (auto const ter = - requireAuth(view(), saDstAmount.get(), uDstAccountID); + if (auto const ter = requireAuth(view(), mptIssue, dstAccountID); ter != tesSUCCESS) return ter; - if (auto const ter = canTransfer( - view(), saDstAmount.get(), account_, uDstAccountID); + if (auto const ter = + canTransfer(view(), mptIssue, account_, dstAccountID); ter != tesSUCCESS) return ter; - auto const& mptIssue = saDstAmount.get(); auto const& issuer = mptIssue.getIssuer(); // Transfer rate Rate rate{QUALITY_ONE}; // Payment between the holders - if (account_ != issuer && uDstAccountID != issuer) + if (account_ != issuer && dstAccountID != issuer) { // If globally/individually locked then // - can't send between holders // - holder can send back to issuer // - issuer can send to holder if (isFrozen(view(), account_, mptIssue) || - isFrozen(view(), uDstAccountID, mptIssue)) + isFrozen(view(), dstAccountID, mptIssue)) return tecLOCKED; // Get the rate for a payment between the holders. @@ -484,10 +480,10 @@ Payment::doApply() } // Amount to deliver. - STAmount amountDeliver = saDstAmount; + STAmount amountDeliver = dstAmount; // Factor in the transfer rate. // No rounding. It'll change once MPT integrated into DEX. - STAmount requiredMaxSourceAmount = multiply(saDstAmount, rate); + STAmount requiredMaxSourceAmount = multiply(dstAmount, rate); // Send more than the account wants to pay or less than // the account wants to deliver (if no SendMax). @@ -505,7 +501,7 @@ Payment::doApply() PaymentSandbox pv(&view()); auto res = accountSendMPT( - pv, account_, uDstAccountID, amountDeliver, ctx_.journal); + pv, account_, dstAccountID, amountDeliver, ctx_.journal); if (res == tesSUCCESS) pv.apply(ctx_.rawView()); else if (res == tecINSUFFICIENT_FUNDS || res == tecPATH_DRY) @@ -514,7 +510,7 @@ Payment::doApply() return res; } - assert(saDstAmount.native()); + assert(dstAmount.native()); // Direct XRP payment. @@ -522,25 +518,25 @@ Payment::doApply() if (!sleSrc) return tefINTERNAL; - // uOwnerCount is the number of entries in this ledger for this + // ownerCount is the number of entries in this ledger for this // account that require a reserve. - auto const uOwnerCount = sleSrc->getFieldU32(sfOwnerCount); + auto const ownerCount = sleSrc->getFieldU32(sfOwnerCount); // This is the total reserve in drops. - auto const reserve = view().fees().accountReserve(uOwnerCount); + auto const reserve = view().fees().accountReserve(ownerCount); // 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()); - if (mPriorBalance < saDstAmount.xrp() + mmm) + if (mPriorBalance < dstAmount.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(dstAmount.xrp() + mmm) << " (" << to_string(reserve) << ")"; return tecUNFUNDED_PAYMENT; @@ -573,14 +569,14 @@ 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 (dstAccountID != account_) { - if (!view().exists(keylet::depositPreauth(uDstAccountID, account_))) + if (!view().exists(keylet::depositPreauth(dstAccountID, account_))) { // Get the base reserve. XRPAmount const dstReserve{view().fees().accountReserve(0)}; - if (saDstAmount > dstReserve || + if (dstAmount > dstReserve || sleDst->getFieldAmount(sfBalance) > dstReserve) return tecNO_PERMISSION; } @@ -588,9 +584,9 @@ Payment::doApply() } // Do the arithmetic for the transfer and make the ledger change. - sleSrc->setFieldAmount(sfBalance, mSourceBalance - saDstAmount); + sleSrc->setFieldAmount(sfBalance, mSourceBalance - dstAmount); sleDst->setFieldAmount( - sfBalance, sleDst->getFieldAmount(sfBalance) + saDstAmount); + sfBalance, sleDst->getFieldAmount(sfBalance) + dstAmount); // Re-arm the password change fee if we can and need to. if ((sleDst->getFlags() & lsfPasswordSpent)) diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index afae132c1fc..8bbf5059171 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -248,9 +248,8 @@ isFrozen( AccountID const& account, MPTIssue const& mptIssue) { - if (isGlobalFrozen(view, mptIssue)) - return true; - return isIndividualFrozen(view, account, mptIssue); + return isGlobalFrozen(view, mptIssue) || + isIndividualFrozen(view, account, mptIssue); } STAmount @@ -569,10 +568,9 @@ transferRate(ReadView const& view, AccountID const& issuer) Rate transferRate(ReadView const& view, MPTID const& issuanceID) { - auto const sle = view.read(keylet::mptIssuance(issuanceID)); - // fee is 0-50,000 (0-50%), rate is 1,000,000,000-2,000,000,000 - if (sle && sle->isFieldPresent(sfTransferFee)) + if (auto const sle = view.read(keylet::mptIssuance(issuanceID)); + sle && sle->isFieldPresent(sfTransferFee)) return Rate{1'000'000'000u + 10'000 * sle->getFieldU16(sfTransferFee)}; return parityRate; @@ -1231,7 +1229,7 @@ accountSend( { if (view.rules().enabled(fixAMMv1_1)) { - if (saAmount < beast::zero) + if (saAmount < beast::zero || saAmount.holds()) { return tecINTERNAL; } @@ -1353,18 +1351,17 @@ rippleSendMPT( // Safe to get MPT since rippleSendMPT is only called by accountSendMPT auto const issuer = saAmount.getIssuer(); + auto const sle = + view.read(keylet::mptIssuance(saAmount.get().getMptID())); + if (!sle) + return tecOBJECT_NOT_FOUND; + if (uSenderID == issuer || uReceiverID == issuer) { // if sender is issuer, check that the new OutstandingAmount will not // exceed MaximumAmount if (uSenderID == issuer) { - auto const mptID = - keylet::mptIssuance(saAmount.get().getMptID()); - auto const sle = view.peek(mptID); - if (!sle) - return tecOBJECT_NOT_FOUND; - auto const sendAmount = saAmount.mpt().value(); auto const maximumAmount = sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount); @@ -1374,7 +1371,7 @@ rippleSendMPT( return tecPATH_DRY; } - // Direct send: redeeming IOUs and/or sending own IOUs. + // Direct send: redeeming MPTs and/or sending own MPTs. auto const ter = rippleCreditMPT(view, uSenderID, uReceiverID, saAmount, j); if (ter != tesSUCCESS) @@ -1384,29 +1381,23 @@ rippleSendMPT( } // Sending 3rd party MPTs: transit. - if (auto const sle = - view.read(keylet::mptIssuance(saAmount.get().getMptID()))) - { - saActual = (waiveFee == WaiveTransferFee::Yes) - ? saAmount - : multiply( - saAmount, - transferRate(view, saAmount.get().getMptID())); - - JLOG(j.debug()) << "rippleSend> " << to_string(uSenderID) << " - > " - << to_string(uReceiverID) - << " : deliver=" << saAmount.getFullText() - << " cost=" << saActual.getFullText(); + saActual = (waiveFee == WaiveTransferFee::Yes) + ? saAmount + : multiply( + saAmount, + transferRate(view, saAmount.get().getMptID())); - if (auto const terResult = - rippleCreditMPT(view, issuer, uReceiverID, saAmount, j); - terResult != tesSUCCESS) - return terResult; + JLOG(j.debug()) << "rippleSend> " << to_string(uSenderID) << " - > " + << to_string(uReceiverID) + << " : deliver=" << saAmount.getFullText() + << " cost=" << saActual.getFullText(); - return rippleCreditMPT(view, uSenderID, issuer, saActual, j); - } + if (auto const terResult = + rippleCreditMPT(view, issuer, uReceiverID, saAmount, j); + terResult != tesSUCCESS) + return terResult; - return tecOBJECT_NOT_FOUND; + return rippleCreditMPT(view, uSenderID, issuer, saActual, j); } TER From 9b8564e790ef3f9c346fee5d18b347300c8d2fdf Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Fri, 11 Oct 2024 20:21:32 -0400 Subject: [PATCH 29/58] Fix metadata and add metadata unit-test --- src/libxrpl/protocol/SField.cpp | 6 +++--- src/test/app/MPToken_test.cpp | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/libxrpl/protocol/SField.cpp b/src/libxrpl/protocol/SField.cpp index 5a96afb937f..f67a909beb3 100644 --- a/src/libxrpl/protocol/SField.cpp +++ b/src/libxrpl/protocol/SField.cpp @@ -194,9 +194,9 @@ 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, SField::sMD_BaseTen); -CONSTRUCT_TYPED_SFIELD(sfOutstandingAmount, "OutstandingAmount", UINT64, 25, SField::sMD_BaseTen); -CONSTRUCT_TYPED_SFIELD(sfMPTAmount, "MPTAmount", UINT64, 26, SField::sMD_BaseTen); +CONSTRUCT_TYPED_SFIELD(sfMaximumAmount, "MaximumAmount", UINT64, 24, SField::sMD_BaseTen|SField::sMD_Default); +CONSTRUCT_TYPED_SFIELD(sfOutstandingAmount, "OutstandingAmount", UINT64, 25, SField::sMD_BaseTen|SField::sMD_Default); +CONSTRUCT_TYPED_SFIELD(sfMPTAmount, "MPTAmount", UINT64, 26, SField::sMD_BaseTen|SField::sMD_Default); // 128-bit CONSTRUCT_TYPED_SFIELD(sfEmailHash, "EmailHash", UINT128, 1); diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 1574741d0e0..94bf1405a7c 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1159,6 +1159,24 @@ class MPToken_test : public beast::unit_test::suite env(pay(bob, carol, MPT(10'000)), sendmax(MPT(10'000)), txflags(tfPartialPayment)); + // Verify the metadata + auto const meta = env.meta()->getJson( + JsonOptions::none)[sfAffectedNodes.fieldName]; + // Issuer got 10 in the transfer fees + BEAST_EXPECT( + meta[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName] + [sfOutstandingAmount.fieldName] == "9990"); + // Destination account got 9'990 + BEAST_EXPECT( + meta[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName] + [sfMPTAmount.fieldName] == "9990"); + // Source account spent 10'000 + BEAST_EXPECT( + meta[2u][sfModifiedNode.fieldName][sfPreviousFields.fieldName] + [sfMPTAmount.fieldName] == "10000"); + BEAST_EXPECT( + !meta[2u][sfModifiedNode.fieldName][sfFinalFields.fieldName] + .isMember(sfMPTAmount.fieldName)); // payment between the holders fails without // partial payment From ac4534204f02b0177706ddb900c82b3764cd1c59 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Mon, 14 Oct 2024 11:14:20 -0400 Subject: [PATCH 30/58] Address reviewer's feedback: * Fix paths comments in Payment * Refactor View --- src/xrpld/app/tx/detail/Payment.cpp | 4 +-- src/xrpld/ledger/detail/View.cpp | 44 ++++++++++------------------- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 19952df3040..1cd42517857 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -235,7 +235,7 @@ Payment::preflight(PreflightContext const& ctx) TER Payment::preclaim(PreclaimContext const& ctx) { - // Ripple if source or destination is non-native or if there are hasPaths. + // Ripple if source or destination is non-native or if there are paths. std::uint32_t const txFlags = ctx.tx.getFlags(); bool const partialPaymentAllowed = txFlags & tfPartialPayment; auto const hasPaths = ctx.tx.isFieldPresent(sfPaths); @@ -321,7 +321,7 @@ Payment::doApply() { auto const deliverMin = ctx_.tx[~sfDeliverMin]; - // Ripple if source or destination is non-native or if there are hasPaths. + // Ripple if source or destination is non-native or if there are paths. std::uint32_t const txFlags = ctx_.tx.getFlags(); bool const partialPaymentAllowed = txFlags & tfPartialPayment; bool const limitQuality = txFlags & tfLimitQuality; diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 8bbf5059171..025f6e4cdbb 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -1892,20 +1892,17 @@ rippleCreditMPT( { auto const mptID = keylet::mptIssuance(saAmount.get().getMptID()); auto const issuer = saAmount.getIssuer(); - if (!view.exists(mptID)) + auto sleIssuance = view.peek(mptID); + if (!sleIssuance) return tecOBJECT_NOT_FOUND; if (uSenderID == issuer) { - if (auto sle = view.peek(mptID)) - { - sle->setFieldU64( - sfOutstandingAmount, - sle->getFieldU64(sfOutstandingAmount) + saAmount.mpt().value()); + sleIssuance->setFieldU64( + sfOutstandingAmount, + sleIssuance->getFieldU64(sfOutstandingAmount) + + saAmount.mpt().value()); - view.update(sle); - } - else - return tecINTERNAL; + view.update(sleIssuance); } else { @@ -1914,16 +1911,10 @@ rippleCreditMPT( { auto const amt = sle->getFieldU64(sfMPTAmount); auto const pay = saAmount.mpt().value(); - if (amt >= pay) - { - if (amt == pay) - sle->makeFieldAbsent(sfMPTAmount); - else - sle->setFieldU64(sfMPTAmount, amt - pay); - view.update(sle); - } - else + if (amt < pay) return tecINSUFFICIENT_FUNDS; + (*sle)[sfMPTAmount] = amt - pay; + view.update(sle); } else return tecNO_AUTH; @@ -1931,17 +1922,12 @@ rippleCreditMPT( if (uReceiverID == issuer) { - if (auto sle = view.peek(mptID)) + auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount); + auto const redeem = saAmount.mpt().value(); + if (outstanding >= redeem) { - auto const outstanding = sle->getFieldU64(sfOutstandingAmount); - auto const redeem = saAmount.mpt().value(); - if (outstanding >= redeem) - { - sle->setFieldU64(sfOutstandingAmount, outstanding - redeem); - view.update(sle); - } - else - return tecINSUFFICIENT_FUNDS; + sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem); + view.update(sleIssuance); } else return tecINTERNAL; From af29f41dfaefc166263b62ce4f4c434a9b0f0e26 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Tue, 15 Oct 2024 09:59:49 -0400 Subject: [PATCH 31/58] use mptJson --- src/xrpld/rpc/handlers/LedgerEntry.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index 30b1113cd05..b8937c528eb 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -668,18 +668,18 @@ doLedgerEntry(RPC::JsonContext& context) else if (context.params.isMember(jss::mptoken)) { expectedType = ltMPTOKEN; - if (!context.params[jss::mptoken].isObject()) + auto const& mptJson = context.params[jss::mptoken]; + if (!mptJson.isObject()) { - if (!uNodeIndex.parseHex( - context.params[jss::mptoken].asString())) + if (!uNodeIndex.parseHex(mptJson.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)) + !mptJson.isMember(jss::mpt_issuance_id) || + !mptJson.isMember(jss::account)) { jvResult[jss::error] = "malformedRequest"; } @@ -688,8 +688,7 @@ doLedgerEntry(RPC::JsonContext& context) try { auto const mptIssuanceIdStr = - context.params[jss::mptoken][jss::mpt_issuance_id] - .asString(); + mptJson[jss::mpt_issuance_id].asString(); uint192 mptIssuanceID; if (!mptIssuanceID.parseHex(mptIssuanceIdStr)) @@ -697,7 +696,7 @@ doLedgerEntry(RPC::JsonContext& context) "Cannot parse mpt_issuance_id"); auto const account = parseBase58( - context.params[jss::mptoken][jss::account].asString()); + mptJson[jss::account].asString()); if (!account || account->isZero()) jvResult[jss::error] = "malformedAddress"; From 9e3cfef223900e8fc65af1f19b2e05019ef4845d Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Tue, 15 Oct 2024 13:10:04 -0400 Subject: [PATCH 32/58] Add += and -= operators to ValueProxy * Simplify updating some MPT fields in View --- include/xrpl/protocol/STObject.h | 39 ++++++++++++++++++++++++++++++++ src/xrpld/ledger/detail/View.cpp | 12 ++++------ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index e55351cbc24..2a9293e7397 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -500,6 +500,17 @@ class STObject::Proxy assign(U&& u); }; +// Constraint += and -= ValueProxy operators +// to value types that support arithmetic operations +template +concept CanAddSubstr = + (std::is_arithmetic_v && + (std::is_same_v< + std::make_unsigned_t, + std::make_unsigned_t> || + (std::is_same_v && + std::is_same_v))); + template class STObject::ValueProxy : private Proxy { @@ -515,6 +526,16 @@ class STObject::ValueProxy : private Proxy std::enable_if_t, ValueProxy&> operator=(U&& u); + // Convenience operators for value types supporting + // arithmetic operations + template + requires CanAddSubstr ValueProxy& + operator+=(U const& t); + + template + requires CanAddSubstr ValueProxy& + operator-=(U const& t); + operator value_type() const; private: @@ -732,6 +753,24 @@ STObject::ValueProxy::operator=(U&& u) return *this; } +template +template +requires CanAddSubstr STObject::ValueProxy& +STObject::ValueProxy::operator+=(U const& t) +{ + this->assign(this->value() + t); + return *this; +} + +template +template +requires CanAddSubstr STObject::ValueProxy& +STObject::ValueProxy::operator-=(U const& t) +{ + this->assign(this->value() - t); + return *this; +} + template STObject::ValueProxy::operator value_type() const { diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 025f6e4cdbb..616bcb2436a 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -569,6 +569,8 @@ Rate transferRate(ReadView const& view, MPTID const& issuanceID) { // fee is 0-50,000 (0-50%), rate is 1,000,000,000-2,000,000,000 + // For example, if transfer fee is 50% then 10,000 * 50,000 = 500,000 + // which represents 50% of 1,000,000,000 if (auto const sle = view.read(keylet::mptIssuance(issuanceID)); sle && sle->isFieldPresent(sfTransferFee)) return Rate{1'000'000'000u + 10'000 * sle->getFieldU16(sfTransferFee)}; @@ -1897,11 +1899,7 @@ rippleCreditMPT( return tecOBJECT_NOT_FOUND; if (uSenderID == issuer) { - sleIssuance->setFieldU64( - sfOutstandingAmount, - sleIssuance->getFieldU64(sfOutstandingAmount) + - saAmount.mpt().value()); - + (*sleIssuance)[sfOutstandingAmount] += saAmount.mpt().value(); view.update(sleIssuance); } else @@ -1937,9 +1935,7 @@ rippleCreditMPT( auto const mptokenID = keylet::mptoken(mptID.key, uReceiverID); if (auto sle = view.peek(mptokenID)) { - sle->setFieldU64( - sfMPTAmount, - sle->getFieldU64(sfMPTAmount) + saAmount.mpt().value()); + (*sle)[sfMPTAmount] += saAmount.mpt().value(); view.update(sle); } else From 4bbf61314224dffab11f5db9c05234143bf4b45d Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:27:04 -0400 Subject: [PATCH 33/58] ledger_entry test (#41) * ledger_entry test * remove unneed line --- src/test/rpc/LedgerRPC_test.cpp | 71 +++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 70e4ffbe8dc..73bb96d0817 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -2361,6 +2361,76 @@ class LedgerRPC_test : public beast::unit_test::suite } } + void + testLedgerEntryMPT() + { + testcase("ledger_entry Request MPT"); + using namespace test::jtx; + using namespace std::literals::chrono_literals; + Env env{*this}; + Account const alice{"alice"}; + Account const bob("bob"); + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + mptAlice.create( + {.transferFee = 10, + .metadata = "123", + .ownerCount = 1, + .flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | + tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback}); + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + std::string const ledgerHash{to_string(env.closed()->info().hash)}; + + std::string const badMptID = + "00000193B9DDCAF401B5B3B26875986043F82CD0D13B4315"; + { + // Request the MPTIssuance using its MPTIssuanceID. + Json::Value jvParams; + jvParams[jss::mpt_issuance] = strHex(mptAlice.issuanceID()); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][sfMPTokenMetadata.jsonName] == + strHex(std::string{"123"})); + } + { + // Request an index that is not a MPTIssuance. + Json::Value jvParams; + jvParams[jss::mpt_issuance] = badMptID; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", ""); + } + { + // Request the MPToken using its owner + mptIssuanceID. + Json::Value jvParams; + jvParams[jss::mptoken] = Json::objectValue; + jvParams[jss::mptoken][jss::account] = bob.human(); + jvParams[jss::mptoken][jss::mpt_issuance_id] = + strHex(mptAlice.issuanceID()); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][sfMPTokenIssuanceID.jsonName] == + strHex(mptAlice.issuanceID())); + } + { + // Request the MPToken using a bad mptIssuanceID. + Json::Value jvParams; + jvParams[jss::mptoken] = Json::objectValue; + jvParams[jss::mptoken][jss::account] = bob.human(); + jvParams[jss::mptoken][jss::mpt_issuance_id] = badMptID; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", ""); + } + } + public: void run() override @@ -2388,6 +2458,7 @@ class LedgerRPC_test : public beast::unit_test::suite testLedgerEntryDID(); testInvalidOracleLedgerEntry(); testOracleLedgerEntry(); + testLedgerEntryMPT(); forAllApiVersions(std::bind_front( &LedgerRPC_test::testLedgerEntryInvalidParams, this)); From 3bb44c28f10acbb82cce23dc4762773177d94468 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Wed, 16 Oct 2024 09:42:49 -0400 Subject: [PATCH 34/58] Simplify constraint for +=, -= of ValueProxy * Add STObject unit-test for +=, -= * Update STTx unit-test --- include/xrpl/protocol/STObject.h | 38 ++++++++++++----------------- src/test/protocol/STObject_test.cpp | 24 +++++++++++++++++- src/test/protocol/STTx_test.cpp | 37 ++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 23 deletions(-) diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index 2a9293e7397..dd411c99cd2 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -502,14 +502,8 @@ class STObject::Proxy // Constraint += and -= ValueProxy operators // to value types that support arithmetic operations -template -concept CanAddSubstr = - (std::is_arithmetic_v && - (std::is_same_v< - std::make_unsigned_t, - std::make_unsigned_t> || - (std::is_same_v && - std::is_same_v))); +template +concept IsArithmetic = std::is_arithmetic_v || std::is_same_v; template class STObject::ValueProxy : private Proxy @@ -528,13 +522,13 @@ class STObject::ValueProxy : private Proxy // Convenience operators for value types supporting // arithmetic operations - template - requires CanAddSubstr ValueProxy& - operator+=(U const& t); + template + ValueProxy& + operator+=(U const& u); - template - requires CanAddSubstr ValueProxy& - operator-=(U const& t); + template + ValueProxy& + operator-=(U const& u); operator value_type() const; @@ -754,20 +748,20 @@ STObject::ValueProxy::operator=(U&& u) } template -template -requires CanAddSubstr STObject::ValueProxy& -STObject::ValueProxy::operator+=(U const& t) +template +STObject::ValueProxy& +STObject::ValueProxy::operator+=(U const& u) { - this->assign(this->value() + t); + this->assign(this->value() + u); return *this; } template -template -requires CanAddSubstr STObject::ValueProxy& -STObject::ValueProxy::operator-=(U const& t) +template +STObject::ValueProxy& +STObject::ValueProxy::operator-=(U const& u) { - this->assign(this->value() - t); + this->assign(this->value() - u); return *this; } diff --git a/src/test/protocol/STObject_test.cpp b/src/test/protocol/STObject_test.cpp index 39a5b9c2f65..13b5a5dd17e 100644 --- a/src/test/protocol/STObject_test.cpp +++ b/src/test/protocol/STObject_test.cpp @@ -396,6 +396,7 @@ class STObject_test : public beast::unit_test::suite auto const& sf1Outer = sfSequence; auto const& sf2Outer = sfExpiration; auto const& sf3Outer = sfQualityIn; + auto const& sf4Outer = sfAmount; auto const& sf4 = sfSignature; auto const& sf5 = sfPublicKey; @@ -427,6 +428,7 @@ class STObject_test : public beast::unit_test::suite {sf1Outer, soeREQUIRED}, {sf2Outer, soeOPTIONAL}, {sf3Outer, soeDEFAULT}, + {sf4Outer, soeOPTIONAL}, {sf4, soeOPTIONAL}, {sf5, soeDEFAULT}, }; @@ -494,6 +496,16 @@ class STObject_test : public beast::unit_test::suite BEAST_EXPECT(st[sf1Outer] == 4); BEAST_EXPECT(st[sf2Outer] == 4); BEAST_EXPECT(st[sf2Outer] == st[sf1Outer]); + st[sf1Outer] += 1; + BEAST_EXPECT(st[sf1Outer] == 5); + st[sf4Outer] = STAmount{1}; + BEAST_EXPECT(st[sf4Outer] == STAmount{1}); + st[sf4Outer] += STAmount{1}; + BEAST_EXPECT(st[sf4Outer] == STAmount{2}); + st[sf1Outer] -= 1; + BEAST_EXPECT(st[sf1Outer] == 4); + st[sf4Outer] -= STAmount{1}; + BEAST_EXPECT(st[sf4Outer] == STAmount{1}); } // Write templated object @@ -542,6 +554,16 @@ class STObject_test : public beast::unit_test::suite BEAST_EXPECT(st[sf3Outer] == 0); BEAST_EXPECT(*st[~sf3Outer] == 0); BEAST_EXPECT(!!st[~sf3Outer]); + st[sf1Outer] += 1; + BEAST_EXPECT(st[sf1Outer] == 1); + st[sf4Outer] = STAmount{1}; + BEAST_EXPECT(st[sf4Outer] == STAmount{1}); + st[sf4Outer] += STAmount{1}; + BEAST_EXPECT(st[sf4Outer] == STAmount{2}); + st[sf1Outer] -= 1; + BEAST_EXPECT(st[sf1Outer] == 0); + st[sf4Outer] -= STAmount{1}; + BEAST_EXPECT(st[sf4Outer] == STAmount{1}); } // coercion operator to std::optional @@ -716,4 +738,4 @@ run() override BEAST_DEFINE_TESTSUITE(STObject, protocol, ripple); -} // ripple +} // namespace ripple diff --git a/src/test/protocol/STTx_test.cpp b/src/test/protocol/STTx_test.cpp index 934bd8050c0..54037eaa5ba 100644 --- a/src/test/protocol/STTx_test.cpp +++ b/src/test/protocol/STTx_test.cpp @@ -1337,6 +1337,31 @@ class STTx_test : public beast::unit_test::suite 0x12, 0x12, 0xff}; constexpr unsigned char payload3[] = { + 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, 0x00, 0x59, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x68, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, + 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}; + + constexpr unsigned char payload4[] = { 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, 0x00, 0x59, 0x00, 0x00, 0x00, 0x00, @@ -1574,6 +1599,18 @@ class STTx_test : public beast::unit_test::suite fail("An exception should have been thrown"); } catch (std::exception const& ex) + { + BEAST_EXPECT( + strcmp(ex.what(), "gFID: uncommon name out of range 0") == 0); + } + + try + { + ripple::SerialIter sit{payload4}; + auto stx = std::make_shared(sit); + fail("An exception should have been thrown"); + } + catch (std::exception const& ex) { BEAST_EXPECT(strcmp(ex.what(), "Unknown field") == 0); } From 0bf5b424f713fd425fdb1434687f01631cc1d656 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Wed, 16 Oct 2024 11:18:17 -0400 Subject: [PATCH 35/58] Address reviewer's feedback on mpt uni-test helper --- src/test/app/MPToken_test.cpp | 300 ++++++++++++++++---------------- src/test/jtx/impl/mpt.cpp | 104 +++++------ src/test/jtx/mpt.h | 75 ++++---- src/test/rpc/LedgerRPC_test.cpp | 4 +- 4 files changed, 234 insertions(+), 249 deletions(-) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 94bf1405a7c..4fc100c7f99 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -164,7 +164,7 @@ class MPToken_test : public beast::unit_test::suite // MPTokenIssuanceDestroy (preclaim) { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.destroy( {.id = makeMptID(env.seq(alice), alice), @@ -174,13 +174,13 @@ class MPToken_test : public beast::unit_test::suite mptAlice.create({.ownerCount = 1}); // a non-issuer tries to destroy a mptissuance they didn't issue - mptAlice.destroy({.issuer = &bob, .err = tecNO_PERMISSION}); + 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}); + mptAlice.authorize({.account = bob, .holderCount = 1}); // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); @@ -220,10 +220,10 @@ class MPToken_test : public beast::unit_test::suite // Validate amendment enable in MPTokenAuthorize (preflight) { Env env{*this, features - featureMPTokensV1}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.authorize( - {.account = &bob, + {.account = bob, .id = makeMptID(env.seq(alice), alice), .err = temDISABLED}); } @@ -231,56 +231,56 @@ class MPToken_test : public beast::unit_test::suite // Validate fields in MPTokenAuthorize (preflight) { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1}); mptAlice.authorize( - {.account = &bob, .flags = 0x00000002, .err = temINVALID_FLAG}); + {.account = bob, .flags = 0x00000002, .err = temINVALID_FLAG}); mptAlice.authorize( - {.account = &bob, .holder = &bob, .err = temMALFORMED}); + {.account = bob, .holder = bob, .err = temMALFORMED}); - mptAlice.authorize({.holder = &alice, .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}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); auto const id = makeMptID(env.seq(alice), alice); mptAlice.authorize( - {.holder = &bob, .id = id, .err = tecOBJECT_NOT_FOUND}); + {.holder = bob, .id = id, .err = tecOBJECT_NOT_FOUND}); mptAlice.authorize( - {.account = &bob, .id = id, .err = tecOBJECT_NOT_FOUND}); + {.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}}); + 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}); + {.account = bob, .holder = alice, .err = tecNO_PERMISSION}); // alice tries to hold onto her own token - mptAlice.authorize({.account = &alice, .err = tecNO_PERMISSION}); + mptAlice.authorize({.account = alice, .err = tecNO_PERMISSION}); // the mpt does not enable allowlisting - mptAlice.authorize({.holder = &bob, .err = tecNO_AUTH}); + mptAlice.authorize({.holder = bob, .err = tecNO_AUTH}); // bob now holds a mptoken object - mptAlice.authorize({.account = &bob, .holderCount = 1}); + mptAlice.authorize({.account = bob, .holderCount = 1}); // bob cannot create the mptoken the second time - mptAlice.authorize({.account = &bob, .err = tecDUPLICATE}); + mptAlice.authorize({.account = bob, .err = tecDUPLICATE}); // Check that bob cannot delete MPToken when his balance is // non-zero @@ -291,7 +291,7 @@ class MPToken_test : public beast::unit_test::suite // bob tries to delete his MPToken, but fails since he still // holds tokens mptAlice.authorize( - {.account = &bob, + {.account = bob, .flags = tfMPTUnauthorize, .err = tecHAS_OBLIGATIONS}); @@ -300,12 +300,12 @@ class MPToken_test : public beast::unit_test::suite } // bob deletes/unauthorizes his MPToken - mptAlice.authorize({.account = &bob, .flags = tfMPTUnauthorize}); + 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, + {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize, .err = tecOBJECT_NOT_FOUND}); @@ -314,7 +314,7 @@ class MPToken_test : public beast::unit_test::suite // Test bad scenarios with allow-listing in MPTokenAuthorize (preclaim) { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth}); @@ -323,31 +323,31 @@ class MPToken_test : public beast::unit_test::suite // alice submits a tx to authorize a holder that hasn't created // a mptoken yet - mptAlice.authorize({.holder = &bob, .err = tecOBJECT_NOT_FOUND}); + mptAlice.authorize({.holder = bob, .err = tecOBJECT_NOT_FOUND}); // alice specifys a holder acct that doesn't exist - mptAlice.authorize({.holder = &cindy, .err = tecNO_DST}); + mptAlice.authorize({.holder = cindy, .err = tecNO_DST}); // bob now holds a mptoken object - mptAlice.authorize({.account = &bob, .holderCount = 1}); + 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}); + mptAlice.authorize({.holder = bob, .flags = tfMPTUnauthorize}); // alice authorizes bob // make sure bob's mptoken has set lsfMPTAuthorized - mptAlice.authorize({.holder = &bob}); + mptAlice.authorize({.holder = bob}); // alice tries authorizes bob again. // tx is successful, but bob is already authorized, // so no changes - mptAlice.authorize({.holder = &bob}); + mptAlice.authorize({.holder = bob}); // bob deletes his mptoken mptAlice.authorize( - {.account = &bob, .holderCount = 0, .flags = tfMPTUnauthorize}); + {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize}); } // Test mptoken reserve requirement - first two mpts free (doApply) @@ -359,7 +359,7 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice1( env, alice, - {.holders = {&bob}, + {.holders = {bob}, .xrpHolders = acctReserve + XRP(1).value().xrp()}); mptAlice1.create(); @@ -370,19 +370,19 @@ class MPToken_test : public beast::unit_test::suite mptAlice3.create({.ownerCount = 3}); // first mpt for free - mptAlice1.authorize({.account = &bob, .holderCount = 1}); + mptAlice1.authorize({.account = bob, .holderCount = 1}); // second mpt free - mptAlice2.authorize({.account = &bob, .holderCount = 2}); + mptAlice2.authorize({.account = bob, .holderCount = 2}); mptAlice3.authorize( - {.account = &bob, .err = tecINSUFFICIENT_RESERVE}); + {.account = bob, .err = tecINSUFFICIENT_RESERVE}); env(pay( env.master, bob, drops(incReserve + incReserve + incReserve))); env.close(); - mptAlice3.authorize({.account = &bob, .holderCount = 3}); + mptAlice3.authorize({.account = bob, .holderCount = 3}); } } @@ -399,16 +399,16 @@ class MPToken_test : public beast::unit_test::suite Env env{*this, features}; // alice create mptissuance without allowisting - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1}); // bob creates a mptoken - mptAlice.authorize({.account = &bob, .holderCount = 1}); + mptAlice.authorize({.account = bob, .holderCount = 1}); // bob deletes his mptoken mptAlice.authorize( - {.account = &bob, .holderCount = 0, .flags = tfMPTUnauthorize}); + {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize}); } // With allowlisting @@ -416,37 +416,37 @@ class MPToken_test : public beast::unit_test::suite Env env{*this, features}; // alice creates a mptokenissuance that requires authorization - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth}); // bob creates a mptoken - mptAlice.authorize({.account = &bob, .holderCount = 1}); + mptAlice.authorize({.account = bob, .holderCount = 1}); // alice authorizes bob - mptAlice.authorize({.account = &alice, .holder = &bob}); + mptAlice.authorize({.account = alice, .holder = bob}); // Unauthorize bob's mptoken mptAlice.authorize( - {.account = &alice, - .holder = &bob, + {.account = alice, + .holder = bob, .holderCount = 1, .flags = tfMPTUnauthorize}); mptAlice.authorize( - {.account = &bob, .holderCount = 0, .flags = tfMPTUnauthorize}); + {.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}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1}); // bob creates a mptoken - mptAlice.authorize({.account = &bob, .holderCount = 1}); + mptAlice.authorize({.account = bob, .holderCount = 1}); // alice deletes her issuance mptAlice.destroy({.ownerCount = 0}); @@ -454,7 +454,7 @@ class MPToken_test : public beast::unit_test::suite // bob can delete his mptoken even though issuance is no longer // existent mptAlice.authorize( - {.account = &bob, .holderCount = 0, .flags = tfMPTUnauthorize}); + {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize}); } } @@ -470,10 +470,10 @@ class MPToken_test : public beast::unit_test::suite // Validate fields in MPTokenIssuanceSet (preflight) { Env env{*this, features - featureMPTokensV1}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.set( - {.account = &bob, + {.account = bob, .id = makeMptID(env.seq(alice), alice), .err = temDISABLED}); @@ -481,25 +481,25 @@ class MPToken_test : public beast::unit_test::suite mptAlice.create({.ownerCount = 1, .holderCount = 0}); - mptAlice.authorize({.account = &bob, .holderCount = 1}); + mptAlice.authorize({.account = bob, .holderCount = 1}); // test invalid flag mptAlice.set( - {.account = &alice, + {.account = alice, .flags = 0x00000008, .err = temINVALID_FLAG}); // set both lock and unlock flags at the same time will fail mptAlice.set( - {.account = &alice, + {.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, + {.account = alice, + .holder = alice, .flags = tfMPTLock, .err = temMALFORMED}); } @@ -509,35 +509,35 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1}); // alice tries to lock a mptissuance that has disabled locking mptAlice.set( - {.account = &alice, + {.account = alice, .flags = tfMPTLock, .err = tecNO_PERMISSION}); // alice tries to unlock mptissuance that has disabled locking mptAlice.set( - {.account = &alice, + {.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, + {.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, + {.account = alice, + .holder = bob, .flags = tfMPTUnlock, .err = tecNO_PERMISSION}); } @@ -547,7 +547,7 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); // alice trying to set when the mptissuance doesn't exist yet mptAlice.set( @@ -560,17 +560,17 @@ class MPToken_test : public beast::unit_test::suite // a non-issuer acct tries to set the mptissuance mptAlice.set( - {.account = &bob, .flags = tfMPTLock, .err = tecNO_PERMISSION}); + {.account = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION}); // trying to set a holder who doesn't have a mptoken mptAlice.set( - {.holder = &bob, + {.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}); + {.holder = cindy, .flags = tfMPTLock, .err = tecNO_DST}); } } @@ -586,46 +586,46 @@ class MPToken_test : public beast::unit_test::suite Account const alice("alice"); // issuer Account const bob("bob"); // holder - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + 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}); + mptAlice.authorize({.account = bob, .holderCount = 1}); // locks bob's mptoken - mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTLock}); + 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}); + mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); // alice locks the mptissuance - mptAlice.set({.account = &alice, .flags = tfMPTLock}); + 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}); + 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}); + mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock}); // locks up bob's mptoken again - mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTLock}); + mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); // alice unlocks mptissuance - mptAlice.set({.account = &alice, .flags = tfMPTUnlock}); + mptAlice.set({.account = alice, .flags = tfMPTUnlock}); // alice unlocks bob's mptoken - mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTUnlock}); + 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}); + mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock}); + mptAlice.set({.account = alice, .flags = tfMPTUnlock}); } void @@ -676,12 +676,12 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); auto const MPT = mptAlice["MPT"]; - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); for (auto flags : {tfNoRippleDirect, tfLimitQuality}) env(pay(alice, bob, MPT(10)), @@ -695,11 +695,11 @@ class MPToken_test : public beast::unit_test::suite Account const alice("alice"); Account const carol("carol"); - MPTTester mptAlice(env, alice, {.holders = {&carol}}); + MPTTester mptAlice(env, alice, {.holders = {carol}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); - mptAlice.authorize({.account = &carol}); + mptAlice.authorize({.account = carol}); // sendMax and DeliverMin are valid XRP amount, // but is invalid combination with MPT amount @@ -740,12 +740,12 @@ class MPToken_test : public beast::unit_test::suite Account const alice("alice"); Account const carol("carol"); - MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); auto const MPT = mptAlice["MPT"]; - mptAlice.authorize({.account = &carol}); + mptAlice.authorize({.account = carol}); Json::Value payment; payment[jss::secret] = alice.name(); @@ -763,12 +763,12 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); auto const MPT = mptAlice["MPT"]; - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); mptAlice.pay(alice, bob, -1, temBAD_AMOUNT); @@ -779,11 +779,11 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); mptAlice.pay(bob, bob, 10, temREDUNDANT); } @@ -794,11 +794,11 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); Account const bad{"bad"}; env.memoize(bad); @@ -813,14 +813,14 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer}); - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); mptAlice.pay(alice, bob, 100, tecNO_AUTH); } @@ -830,7 +830,7 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.ownerCount = 1, @@ -838,17 +838,17 @@ class MPToken_test : public beast::unit_test::suite .flags = tfMPTRequireAuth | tfMPTCanTransfer}); // bob creates an empty MPToken - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); // alice authorizes bob to hold funds - mptAlice.authorize({.account = &alice, .holder = &bob}); + 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}); + {.account = alice, .holder = bob, .flags = tfMPTUnauthorize}); // bob fails to send back to alice because he is no longer // authorize to move his funds! @@ -862,16 +862,16 @@ class MPToken_test : public beast::unit_test::suite Account const bob{"bob"}; Account const cindy{"cindy"}; - MPTTester mptAlice(env, alice, {.holders = {&bob, &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}); + mptAlice.authorize({.account = bob}); // cindy creates a MPToken - mptAlice.authorize({.account = &cindy}); + mptAlice.authorize({.account = cindy}); // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); @@ -888,7 +888,7 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); @@ -907,12 +907,12 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer}); - mptAlice.authorize({.account = &bob}); - mptAlice.authorize({.account = &carol}); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); mptAlice.pay(alice, bob, 100); @@ -927,19 +927,19 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.ownerCount = 1, .flags = tfMPTCanLock | tfMPTCanTransfer}); - mptAlice.authorize({.account = &bob}); - mptAlice.authorize({.account = &carol}); + 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}); + mptAlice.set({.account = alice, .flags = tfMPTLock}); // Can't send between holders mptAlice.pay(bob, carol, 1, tecLOCKED); mptAlice.pay(carol, bob, 2, tecLOCKED); @@ -949,10 +949,9 @@ class MPToken_test : public beast::unit_test::suite mptAlice.pay(bob, alice, 4); // Global unlock - mptAlice.set({.account = &alice, .flags = tfMPTUnlock}); + mptAlice.set({.account = alice, .flags = tfMPTUnlock}); // Individual lock - mptAlice.set( - {.account = &alice, .holder = &bob, .flags = tfMPTLock}); + mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); // Can't send between holders mptAlice.pay(bob, carol, 5, tecLOCKED); mptAlice.pay(carol, bob, 6, tecLOCKED); @@ -966,7 +965,7 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); // Transfer fee is 10% mptAlice.create( @@ -976,8 +975,8 @@ class MPToken_test : public beast::unit_test::suite .flags = tfMPTCanTransfer}); // Holders create MPToken - mptAlice.authorize({.account = &bob}); - mptAlice.authorize({.account = &carol}); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); // Payment between the issuer and the holder, no transfer fee. mptAlice.pay(alice, bob, 2'000); @@ -1022,14 +1021,14 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); // Holders create MPToken - mptAlice.authorize({.account = &bob}); - mptAlice.authorize({.account = &carol}); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); mptAlice.pay(alice, bob, 1'000); auto const MPT = mptAlice["MPT"]; @@ -1055,14 +1054,14 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); // Holders create MPToken - mptAlice.authorize({.account = &bob}); - mptAlice.authorize({.account = &carol}); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); mptAlice.pay(alice, bob, 1'000); auto const MPT = mptAlice["MPT"]; @@ -1084,7 +1083,7 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.maxAmt = 100, @@ -1092,7 +1091,7 @@ class MPToken_test : public beast::unit_test::suite .holderCount = 0, .flags = tfMPTCanTransfer}); - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); // issuer sends holder the max amount allowed mptAlice.pay(alice, bob, 100); @@ -1106,11 +1105,11 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); // issuer sends holder the default max amount allowed mptAlice.pay(alice, bob, maxMPTokenAmount); @@ -1139,7 +1138,7 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.maxAmt = 10'000, @@ -1149,8 +1148,8 @@ class MPToken_test : public beast::unit_test::suite .flags = tfMPTCanTransfer}); auto const MPT = mptAlice["MPT"]; - mptAlice.authorize({.account = &bob}); - mptAlice.authorize({.account = &carol}); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); // issuer sends holder the max amount allowed mptAlice.pay(alice, bob, 10'000); @@ -1189,11 +1188,11 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); // alice destroys issuance mptAlice.destroy({.ownerCount = 0}); @@ -1218,7 +1217,7 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); @@ -1238,13 +1237,13 @@ class MPToken_test : public beast::unit_test::suite Account const carol("bob"); Account const bob("carol"); - MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.maxAmt = 100, .ownerCount = 1, .flags = tfMPTCanTransfer}); - mptAlice.authorize({.account = &bob}); - mptAlice.authorize({.account = &carol}); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); mptAlice.pay(alice, bob, 100); @@ -1256,13 +1255,13 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); - mptAlice.authorize({.account = &bob}); - mptAlice.authorize({.account = &carol}); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); // issuer to holder mptAlice.pay(alice, bob, 100); @@ -1701,7 +1700,7 @@ class MPToken_test : public beast::unit_test::suite Account const alice{"alice"}; Account const bob{"bob"}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); // enable asfAllowTrustLineClawback for alice env(fset(alice, asfAllowTrustLineClawback)); @@ -1711,7 +1710,7 @@ class MPToken_test : public beast::unit_test::suite // Create issuance without enabling clawback mptAlice.create({.ownerCount = 1, .holderCount = 0}); - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); mptAlice.pay(alice, bob, 100); @@ -1728,7 +1727,7 @@ class MPToken_test : public beast::unit_test::suite Account const carol{"carol"}; env.fund(XRP(1000), carol); env.close(); - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); auto const fakeMpt = ripple::test::jtx::MPT( alice.name(), makeMptID(env.seq(alice), alice)); @@ -1745,7 +1744,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.claw(alice, bob, 1, tecOBJECT_NOT_FOUND); // bob creates a MPToken - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); // clawback fails because bob currently has a balance of zero mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS); @@ -1793,14 +1792,14 @@ class MPToken_test : public beast::unit_test::suite Account const alice{"alice"}; Account const bob{"bob"}; - MPTTester mptAlice(env, alice, {.holders = {&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}); + mptAlice.authorize({.account = bob}); // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); @@ -1819,7 +1818,7 @@ class MPToken_test : public beast::unit_test::suite Account const alice{"alice"}; Account const bob{"bob"}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); // alice creates issuance mptAlice.create( @@ -1828,12 +1827,12 @@ class MPToken_test : public beast::unit_test::suite .flags = tfMPTCanLock | tfMPTCanClawback}); // bob creates a MPToken - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); - mptAlice.set({.account = &alice, .flags = tfMPTLock}); + mptAlice.set({.account = alice, .flags = tfMPTLock}); mptAlice.claw(alice, bob, 100); } @@ -1844,7 +1843,7 @@ class MPToken_test : public beast::unit_test::suite Account const alice{"alice"}; Account const bob{"bob"}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); // alice creates issuance mptAlice.create( @@ -1853,13 +1852,12 @@ class MPToken_test : public beast::unit_test::suite .flags = tfMPTCanLock | tfMPTCanClawback}); // bob creates a MPToken - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); - mptAlice.set( - {.account = &alice, .holder = &bob, .flags = tfMPTLock}); + mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); mptAlice.claw(alice, bob, 100); } @@ -1870,7 +1868,7 @@ class MPToken_test : public beast::unit_test::suite Account const alice{"alice"}; Account const bob{"bob"}; - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); // alice creates issuance mptAlice.create( @@ -1879,17 +1877,17 @@ class MPToken_test : public beast::unit_test::suite .flags = tfMPTCanClawback | tfMPTRequireAuth}); // bob creates a MPToken - mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = bob}); // alice authorizes bob - mptAlice.authorize({.account = &alice, .holder = &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}); + {.account = alice, .holder = bob, .flags = tfMPTUnauthorize}); mptAlice.claw(alice, bob, 100); } diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 18c151ea174..b2d55d4f500 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -32,9 +32,9 @@ mptflags::operator()(Env& env) const } void -mptpay::operator()(Env& env) const +mptbalance::operator()(Env& env) const { - env.test.expect(amount_ == tester_.getAmount(account_)); + env.test.expect(amount_ == tester_.getBalance(account_)); } void @@ -43,19 +43,19 @@ requireAny::operator()(Env& env) const env.test.expect(cb_()); } -std::unordered_map -MPTTester::makeHolders(std::vector const& holders) +std::unordered_map +MPTTester::makeHolders(std::vector const& holders) { - std::unordered_map accounts; + std::unordered_map accounts; for (auto const& h : holders) { - assert(h && accounts.find(h->human()) == accounts.cend()); - accounts.emplace(h->human(), h); + assert(accounts.find(h.human()) == accounts.cend()); + accounts.emplace(h.human(), h); } return accounts; } -MPTTester::MPTTester(Env& env, Account const& issuer, MPTConstr const& arg) +MPTTester::MPTTester(Env& env, Account const& issuer, MPTInit const& arg) : env_(env) , issuer_(issuer) , holders_(makeHolders(arg.holders)) @@ -65,7 +65,7 @@ MPTTester::MPTTester(Env& env, Account const& issuer, MPTConstr const& arg) { env_.fund(arg.xrp, issuer_); for (auto it : holders_) - env_.fund(arg.xrpHolders, *it.second); + env_.fund(arg.xrpHolders, it.second); } if (close_) env.close(); @@ -74,8 +74,8 @@ MPTTester::MPTTester(Env& env, Account const& issuer, MPTConstr const& arg) env_.require(owners(issuer_, 0)); for (auto it : holders_) { - assert(issuer_.id() != it.second->id()); - env_.require(owners(*it.second, 0)); + assert(issuer_.id() != it.second.id()); + env_.require(owners(it.second, 0)); } } } @@ -83,10 +83,9 @@ MPTTester::MPTTester(Env& env, Account const& issuer, MPTConstr const& arg) void MPTTester::create(const MPTCreate& arg) { - if (issuanceKey_) + if (id_) Throw("MPT can't be reused"); id_ = makeMptID(env_.seq(issuer_), issuer_); - issuanceKey_ = keylet::mptIssuance(*id_).key; Json::Value jv; jv[sfAccount.jsonName] = issuer_.human(); jv[sfTransactionType.jsonName] = jss::MPTokenIssuanceCreate; @@ -106,7 +105,6 @@ MPTTester::create(const MPTCreate& arg) })); id_.reset(); - issuanceKey_.reset(); } else if (arg.flags) env_.require(mptflags(*this, *arg.flags)); @@ -138,7 +136,7 @@ MPTTester::holder(std::string const& holder_) const assert(it != holders_.cend()); if (it == holders_.cend()) Throw("Holder is not found"); - return *it->second; + return it->second; } void @@ -162,7 +160,7 @@ MPTTester::authorize(MPTAuthorize const& arg) if (auto const result = submit(arg, jv); result == tesSUCCESS) { // Issuer authorizes - if (arg.account == nullptr || *arg.account == issuer_) + if (!arg.account || *arg.account == issuer_) { auto const flags = getFlags(arg.holder); // issuer un-authorizes the holder @@ -179,19 +177,19 @@ MPTTester::authorize(MPTAuthorize const& arg) auto const flags = getFlags(arg.account); // holder creates a token env_.require(mptflags(*this, flags, arg.account)); - env_.require(mptpay(*this, *arg.account, 0)); + env_.require(mptbalance(*this, *arg.account, 0)); } } else if ( - arg.account != nullptr && *arg.account != issuer_ && - arg.flags.value_or(0) == 0 && issuanceKey_) + arg.account && *arg.account != issuer_ && arg.flags.value_or(0) == 0 && + id_) { if (result == tecDUPLICATE) { // Verify that MPToken already exists env_.require(requireAny([&]() -> bool { - return env_.le(keylet::mptoken( - *issuanceKey_, arg.account->id())) != nullptr; + return env_.le(keylet::mptoken(*id_, arg.account->id())) != + nullptr; })); } else @@ -199,8 +197,8 @@ MPTTester::authorize(MPTAuthorize const& arg) // 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; + return env_.le(keylet::mptoken(*id_, arg.account->id())) == + nullptr; })); } } @@ -226,7 +224,8 @@ MPTTester::set(MPTSet const& arg) jv[sfMPTokenHolder.jsonName] = arg.holder->human(); if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0)) { - auto require = [&](AccountP holder, bool unchanged) { + auto require = [&](std::optional const& holder, + bool unchanged) { auto flags = getFlags(holder); if (!unchanged) { @@ -240,22 +239,22 @@ MPTTester::set(MPTSet const& arg) env_.require(mptflags(*this, flags, holder)); }; if (arg.account) - require(nullptr, arg.holder != nullptr); + require(std::nullopt, arg.holder.has_value()); if (arg.holder) - require(arg.holder, false); + require(*arg.holder, false); } } bool MPTTester::forObject( std::function const& cb, - AccountP holder_) const + std::optional const& holder_) const { - assert(issuanceKey_); + assert(id_); auto const key = [&]() { if (holder_) - return keylet::mptoken(*issuanceKey_, holder_->id()); - return keylet::mptIssuance(*issuanceKey_); + return keylet::mptoken(*id_, holder_->id()); + return keylet::mptIssuance(*id_); }(); if (auto const sle = env_.le(key)) return cb(sle); @@ -269,7 +268,7 @@ MPTTester::checkMPTokenAmount( { return forObject( [&](SLEP const& sle) { return expectedAmount == (*sle)[sfMPTAmount]; }, - &holder_); + holder_); } [[nodiscard]] bool @@ -281,7 +280,9 @@ MPTTester::checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const } [[nodiscard]] bool -MPTTester::checkFlags(uint32_t const expectedFlags, AccountP holder) const +MPTTester::checkFlags( + uint32_t const expectedFlags, + std::optional const& holder) const { return expectedFlags == getFlags(holder); } @@ -294,9 +295,9 @@ MPTTester::pay( std::optional err) { assert(id_); - auto const srcAmt = getAmount(src); - auto const destAmt = getAmount(dest); - auto const outstnAmt = getAmount(issuer_); + auto const srcAmt = getBalance(src); + auto const destAmt = getBalance(dest); + auto const outstnAmt = getBalance(issuer_); if (err) env_(jtx::pay(src, dest, mpt(amount)), ter(*err)); else @@ -307,13 +308,13 @@ MPTTester::pay( env_.close(); if (src == issuer_) { - env_.require(mptpay(*this, src, srcAmt + amount)); - env_.require(mptpay(*this, dest, destAmt + amount)); + env_.require(mptbalance(*this, src, srcAmt + amount)); + env_.require(mptbalance(*this, dest, destAmt + amount)); } else if (dest == issuer_) { - env_.require(mptpay(*this, src, srcAmt - amount)); - env_.require(mptpay(*this, dest, destAmt - amount)); + env_.require(mptbalance(*this, src, srcAmt - amount)); + env_.require(mptbalance(*this, dest, destAmt - amount)); } else { @@ -321,10 +322,10 @@ MPTTester::pay( STAmount const saActual = multiply(saAmount, transferRate(*env_.current(), *id_)); // Sender pays the transfer fee if any - env_.require(mptpay(*this, src, srcAmt - saActual.mpt().value())); - env_.require(mptpay(*this, dest, destAmt + amount)); + env_.require(mptbalance(*this, src, srcAmt - saActual.mpt().value())); + env_.require(mptbalance(*this, dest, destAmt + amount)); // Outstanding amount is reduced by the transfer fee if any - env_.require(mptpay( + env_.require(mptbalance( *this, issuer_, outstnAmt - (saActual - saAmount).mpt().value())); } } @@ -337,8 +338,8 @@ MPTTester::claw( std::optional err) { assert(id_); - auto const issuerAmt = getAmount(issuer); - auto const holderAmt = getAmount(holder); + auto const issuerAmt = getBalance(issuer); + auto const holderAmt = getBalance(holder); if (err) env_(jtx::claw(issuer, mpt(amount), holder), ter(*err)); else @@ -349,9 +350,9 @@ MPTTester::claw( env_.close(); env_.require( - mptpay(*this, issuer, issuerAmt - std::min(holderAmt, amount))); + mptbalance(*this, issuer, issuerAmt - std::min(holderAmt, amount))); env_.require( - mptpay(*this, holder, holderAmt - std::min(holderAmt, amount))); + mptbalance(*this, holder, holderAmt - std::min(holderAmt, amount))); } PrettyAmount @@ -362,25 +363,24 @@ MPTTester::mpt(std::int64_t amount) const } std::int64_t -MPTTester::getAmount(Account const& account) const +MPTTester::getBalance(Account const& account) const { - assert(issuanceKey_); + assert(id_); if (account == issuer_) { - if (auto const sle = env_.le(keylet::mptIssuance(*issuanceKey_))) + if (auto const sle = env_.le(keylet::mptIssuance(*id_))) return sle->getFieldU64(sfOutstandingAmount); } else { - if (auto const sle = - env_.le(keylet::mptoken(*issuanceKey_, account.id()))) + if (auto const sle = env_.le(keylet::mptoken(*id_, account.id()))) return sle->getFieldU64(sfMPTAmount); } return 0; } std::uint32_t -MPTTester::getFlags(ripple::test::jtx::AccountP holder) const +MPTTester::getFlags(std::optional const& holder) const { std::uint32_t flags = 0; if (!forObject( diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 49e2004fd77..aa47fcad8e2 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -30,10 +30,6 @@ namespace ripple { namespace test { namespace jtx { -namespace { -using AccountP = Account const*; -} - class MPTTester; // Check flags settings on MPT create @@ -42,10 +38,13 @@ class mptflags private: MPTTester& tester_; std::uint32_t flags_; - AccountP holder_; + std::optional holder_; public: - mptflags(MPTTester& tester, std::uint32_t flags, AccountP holder = nullptr) + mptflags( + MPTTester& tester, + std::uint32_t flags, + std::optional const& holder = std::nullopt) : tester_(tester), flags_(flags), holder_(holder) { } @@ -55,7 +54,7 @@ class mptflags }; // Check mptissuance or mptoken amount balances on payment -class mptpay +class mptbalance { private: MPTTester const& tester_; @@ -63,7 +62,7 @@ class mptpay std::int64_t const amount_; public: - mptpay(MPTTester& tester, Account const& account, std::int64_t amount) + mptbalance(MPTTester& tester, Account const& account, std::int64_t amount) : tester_(tester), account_(account), amount_(amount) { } @@ -86,9 +85,9 @@ class requireAny operator()(Env& env) const; }; -struct MPTConstr +struct MPTInit { - std::vector holders = {}; + std::vector holders = {}; PrettyAmount const& xrp = XRP(10'000); PrettyAmount const& xrpHolders = XRP(10'000); bool fund = true; @@ -110,7 +109,7 @@ struct MPTCreate struct MPTDestroy { - AccountP issuer = nullptr; + std::optional issuer = std::nullopt; std::optional id = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; @@ -120,8 +119,8 @@ struct MPTDestroy struct MPTAuthorize { - AccountP account = nullptr; - AccountP holder = nullptr; + std::optional account = std::nullopt; + std::optional holder = std::nullopt; std::optional id = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; @@ -131,8 +130,8 @@ struct MPTAuthorize struct MPTSet { - AccountP account = nullptr; - AccountP holder = nullptr; + std::optional account = std::nullopt; + std::optional holder = std::nullopt; std::optional id = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; @@ -144,13 +143,12 @@ class MPTTester { Env& env_; Account const& issuer_; - std::unordered_map const holders_; + std::unordered_map const holders_; std::optional id_; - std::optional issuanceKey_; bool close_; public: - MPTTester(Env& env, Account const& issuer, MPTConstr const& constr = {}); + MPTTester(Env& env, Account const& issuer, MPTInit const& constr = {}); void create(MPTCreate const& arg = MPTCreate{}); @@ -172,7 +170,9 @@ class MPTTester checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const; [[nodiscard]] bool - checkFlags(uint32_t const expectedFlags, AccountP holder = nullptr) const; + checkFlags( + uint32_t const expectedFlags, + std::optional const& holder = std::nullopt) const; Account const& issuer() const @@ -198,22 +198,16 @@ class MPTTester PrettyAmount mpt(std::int64_t amount) const; - uint256 const& - issuanceKey() const - { - assert(issuanceKey_); - return *issuanceKey_; - } - MPTID const& issuanceID() const { - assert(id_); + if (!env_.test.BEAST_EXPECT(id_)) + Throw("Uninitialized issuanceID"); return *id_; } std::int64_t - getAmount(Account const& account) const; + getBalance(Account const& account) const; MPT operator[](std::string const& name); @@ -223,23 +217,16 @@ class MPTTester bool forObject( std::function const& cb, - AccountP holder = nullptr) const; + std::optional const& holder = std::nullopt) 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); + env_( + jv, + txflags(arg.flags.value_or(0)), + ter(arg.err.value_or(tesSUCCESS))); auto const err = env_.ter(); if (close_) env_.close(); @@ -248,16 +235,16 @@ class MPTTester if (arg.holderCount) { for (auto it : holders_) - env_.require(owners(*it.second, *arg.holderCount)); + env_.require(owners(it.second, *arg.holderCount)); } return err; } - std::unordered_map - makeHolders(std::vector const& holders); + std::unordered_map + makeHolders(std::vector const& holders); std::uint32_t - getFlags(AccountP holder) const; + getFlags(std::optional const& holder) const; }; } // namespace jtx diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 73bb96d0817..891515b221e 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -2371,14 +2371,14 @@ class LedgerRPC_test : public beast::unit_test::suite Account const alice{"alice"}; Account const bob("bob"); - MPTTester mptAlice(env, alice, {.holders = {&bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.transferFee = 10, .metadata = "123", .ownerCount = 1, .flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback}); - mptAlice.authorize({.account = &bob, .holderCount = 1}); + mptAlice.authorize({.account = bob, .holderCount = 1}); std::string const ledgerHash{to_string(env.closed()->info().hash)}; From 924dd9c65d51aa6161791c14d989f3c91d222eb3 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Wed, 16 Oct 2024 14:42:47 -0400 Subject: [PATCH 36/58] refactor mpt_issuance_id into SLE::getJson (#42) * add mpt id * refactor --- src/libxrpl/protocol/STLedgerEntry.cpp | 4 ++++ src/test/rpc/LedgerRPC_test.cpp | 3 +++ src/xrpld/rpc/detail/RPCHelpers.cpp | 8 +------- src/xrpld/rpc/handlers/LedgerData.cpp | 4 ---- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/libxrpl/protocol/STLedgerEntry.cpp b/src/libxrpl/protocol/STLedgerEntry.cpp index 68d1455cb1d..1801149ab2a 100644 --- a/src/libxrpl/protocol/STLedgerEntry.cpp +++ b/src/libxrpl/protocol/STLedgerEntry.cpp @@ -125,6 +125,10 @@ STLedgerEntry::getJson(JsonOptions options) const ret[jss::index] = to_string(key_); + if (getType() == ltMPTOKEN_ISSUANCE) + ret[jss::mpt_issuance_id] = to_string( + makeMptID(getFieldU32(sfSequence), getAccountID(sfIssuer))); + return ret; } diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 891515b221e..792da88b5bc 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -2394,6 +2394,9 @@ class LedgerRPC_test : public beast::unit_test::suite BEAST_EXPECT( jrr[jss::node][sfMPTokenMetadata.jsonName] == strHex(std::string{"123"})); + BEAST_EXPECT( + jrr[jss::node][jss::mpt_issuance_id] == + strHex(mptAlice.issuanceID())); } { // Request an index that is not a MPTIssuance. diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index f3df4be263e..930f8d8a0e5 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -285,13 +285,7 @@ getAccountObjects( if (!typeFilter.has_value() || typeMatchesFilter(typeFilter.value(), sleNode->getType())) { - auto sleJson = sleNode->getJson(JsonOptions::none); - if (sleNode->getType() == ltMPTOKEN_ISSUANCE) - sleJson[jss::mpt_issuance_id] = to_string(makeMptID( - (*sleNode)[sfSequence], - sleNode->getAccountID(sfIssuer))); - - jvObjects.append(sleJson); + jvObjects.append(sleNode->getJson(JsonOptions::none)); } if (++i == mlimit) diff --git a/src/xrpld/rpc/handlers/LedgerData.cpp b/src/xrpld/rpc/handlers/LedgerData.cpp index 573f3f62f88..ad26b83b43b 100644 --- a/src/xrpld/rpc/handlers/LedgerData.cpp +++ b/src/xrpld/rpc/handlers/LedgerData.cpp @@ -124,10 +124,6 @@ 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(makeMptID( - (*sle)[sfSequence], sle->getAccountID(sfIssuer))); } } } From c3f2bd4ffca63aab8eabc94c49d01255eb469146 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Wed, 16 Oct 2024 14:58:48 -0400 Subject: [PATCH 37/58] Fix clang-format --- include/xrpl/basics/MPTAmount.h | 6 ++++-- include/xrpl/basics/Number.h | 9 ++++++--- include/xrpl/protocol/detail/STVar.h | 3 ++- src/libxrpl/protocol/STVar.cpp | 3 ++- src/test/jtx/amount.h | 9 ++++++--- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/include/xrpl/basics/MPTAmount.h b/include/xrpl/basics/MPTAmount.h index bd523c5abd7..34f747a21be 100644 --- a/include/xrpl/basics/MPTAmount.h +++ b/include/xrpl/basics/MPTAmount.h @@ -75,7 +75,8 @@ class MPTAmount : private boost::totally_ordered, operator<(MPTAmount const& other) const; /** Returns true if the amount is not zero */ - explicit constexpr operator bool() const noexcept; + explicit constexpr + operator bool() const noexcept; /** Return the sign of the amount */ constexpr int @@ -96,7 +97,8 @@ constexpr MPTAmount::MPTAmount(value_type value) : value_(value) { } -constexpr MPTAmount& MPTAmount::operator=(beast::Zero) +constexpr MPTAmount& +MPTAmount::operator=(beast::Zero) { value_ = 0; return *this; diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index bec24d21428..01b3adb22d4 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -95,9 +95,12 @@ class Number * as the preferred type for floating point arithmetic as it makes * "mixed mode" more convenient, e.g. MPTAmount + Number. */ - 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 + 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 operator==(Number const& x, Number const& y) noexcept diff --git a/include/xrpl/protocol/detail/STVar.h b/include/xrpl/protocol/detail/STVar.h index 4accfb3e20b..4a830cf8d7c 100644 --- a/include/xrpl/protocol/detail/STVar.h +++ b/include/xrpl/protocol/detail/STVar.h @@ -151,7 +151,8 @@ class STVar * depth is ignored in former case. */ template - requires ValidConstructSTArgs void + requires ValidConstructSTArgs + void constructST(SerializedTypeID id, int depth, Args&&... arg); bool diff --git a/src/libxrpl/protocol/STVar.cpp b/src/libxrpl/protocol/STVar.cpp index e44c37259b7..f185595eadb 100644 --- a/src/libxrpl/protocol/STVar.cpp +++ b/src/libxrpl/protocol/STVar.cpp @@ -136,7 +136,8 @@ STVar::destroy() } template -requires ValidConstructSTArgs void + requires ValidConstructSTArgs +void STVar::constructST(SerializedTypeID id, int depth, Args&&... args) { auto constructWithDepth = [&]() { diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index 1581568ed99..9468b791f3d 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -394,14 +394,17 @@ class MPT } template - requires(sizeof(T) >= sizeof(int) && std::is_arithmetic_v) PrettyAmount + requires(sizeof(T) >= sizeof(int) && std::is_arithmetic_v) + PrettyAmount operator()(T v) const { return {amountFromString(mpt(), std::to_string(v)), name}; } - PrettyAmount operator()(epsilon_t) const; - PrettyAmount operator()(detail::epsilon_multiple) const; + PrettyAmount + operator()(epsilon_t) const; + PrettyAmount + operator()(detail::epsilon_multiple) const; friend BookSpec operator~(MPT const& mpt) From bd97cc9844f9f4c8c1a840bcf05b67df43cd4730 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 17 Oct 2024 12:02:01 -0400 Subject: [PATCH 38/58] Address reviewer's feedback --- include/xrpl/protocol/TER.h | 56 +++++++++------------ include/xrpl/protocol/detail/features.macro | 4 +- src/libxrpl/protocol/STAmount.cpp | 2 +- src/test/jtx/impl/mpt.cpp | 46 ++++++++--------- src/xrpld/ledger/View.h | 36 ++++++++++++- src/xrpld/ledger/detail/View.cpp | 13 +++++ 6 files changed, 99 insertions(+), 58 deletions(-) diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index f0e10e910c9..4710ef8f8a9 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -140,7 +140,7 @@ enum TEMcodes : TERUnderlyingType { temARRAY_EMPTY, temARRAY_TOO_LARGE, - temBAD_TRANSFER_FEE + temBAD_TRANSFER_FEE, }; //------------------------------------------------------------------------------ @@ -484,66 +484,60 @@ class TERSubset // Only enabled if both arguments return int if TERtiInt is called with them. template constexpr auto -operator==(L const& lhs, R const& rhs) - -> std::enable_if_t< - std::is_same::value && - std::is_same::value, - bool> +operator==(L const& lhs, R const& rhs) -> std::enable_if_t< + std::is_same::value && + std::is_same::value, + bool> { return TERtoInt(lhs) == TERtoInt(rhs); } template constexpr auto -operator!=(L const& lhs, R const& rhs) - -> std::enable_if_t< - std::is_same::value && - std::is_same::value, - bool> +operator!=(L const& lhs, R const& rhs) -> std::enable_if_t< + std::is_same::value && + std::is_same::value, + bool> { return TERtoInt(lhs) != TERtoInt(rhs); } template constexpr auto -operator<(L const& lhs, R const& rhs) - -> std::enable_if_t< - std::is_same::value && - std::is_same::value, - bool> +operator<(L const& lhs, R const& rhs) -> std::enable_if_t< + std::is_same::value && + std::is_same::value, + bool> { return TERtoInt(lhs) < TERtoInt(rhs); } template constexpr auto -operator<=(L const& lhs, R const& rhs) - -> std::enable_if_t< - std::is_same::value && - std::is_same::value, - bool> +operator<=(L const& lhs, R const& rhs) -> std::enable_if_t< + std::is_same::value && + std::is_same::value, + bool> { return TERtoInt(lhs) <= TERtoInt(rhs); } template constexpr auto -operator>(L const& lhs, R const& rhs) - -> std::enable_if_t< - std::is_same::value && - std::is_same::value, - bool> +operator>(L const& lhs, R const& rhs) -> std::enable_if_t< + std::is_same::value && + std::is_same::value, + bool> { return TERtoInt(lhs) > TERtoInt(rhs); } template constexpr auto -operator>=(L const& lhs, R const& rhs) - -> std::enable_if_t< - std::is_same::value && - std::is_same::value, - bool> +operator>=(L const& lhs, R const& rhs) -> std::enable_if_t< + std::is_same::value && + std::is_same::value, + bool> { return TERtoInt(lhs) >= TERtoInt(rhs); } diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 85fab512101..3a8d77e2bab 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -31,7 +31,8 @@ // InvariantsV1_1 will be changes to Supported::yes when all the // invariants expected to be included under it are complete. -XRPL_FEATURE(InvariantsV1_1, Supported::no, VoteBehavior::DefaultNo) +XRPL_FEATURE(MPTokensV1, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(InvariantsV1_1, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (NFTokenPageLinks, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (InnerObjTemplate2, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (EnforceNFTokenTrustline, Supported::yes, VoteBehavior::DefaultNo) @@ -94,7 +95,6 @@ XRPL_FIX (1513, Supported::yes, VoteBehavior::DefaultYe XRPL_FEATURE(FlowCross, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(Flow, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(OwnerPaysFee, Supported::no, VoteBehavior::DefaultNo) -XRPL_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/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 558311a0822..fe13118d88c 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -277,7 +277,7 @@ IOUAmount STAmount::iou() const { if (native() || !holds()) - Throw("Cannot return native STAmount as IOUAmount"); + Throw("Cannot return non-IOU STAmount as IOUAmount"); auto mantissa = static_cast(mValue); auto exponent = mOffset; diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index b2d55d4f500..e3e65bad65b 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -87,16 +87,16 @@ MPTTester::create(const MPTCreate& arg) Throw("MPT can't be reused"); id_ = makeMptID(env_.seq(issuer_), issuer_); Json::Value jv; - jv[sfAccount.jsonName] = issuer_.human(); - jv[sfTransactionType.jsonName] = jss::MPTokenIssuanceCreate; + jv[sfAccount] = issuer_.human(); + jv[sfTransactionType] = jss::MPTokenIssuanceCreate; if (arg.assetScale) - jv[sfAssetScale.jsonName] = *arg.assetScale; + jv[sfAssetScale] = *arg.assetScale; if (arg.transferFee) - jv[sfTransferFee.jsonName] = *arg.transferFee; + jv[sfTransferFee] = *arg.transferFee; if (arg.metadata) - jv[sfMPTokenMetadata.jsonName] = strHex(*arg.metadata); + jv[sfMPTokenMetadata] = strHex(*arg.metadata); if (arg.maxAmt) - jv[sfMaximumAmount.jsonName] = std::to_string(*arg.maxAmt); + jv[sfMaximumAmount] = std::to_string(*arg.maxAmt); if (submit(arg, jv) != tesSUCCESS) { // Verify issuance doesn't exist @@ -115,17 +115,17 @@ MPTTester::destroy(MPTDestroy const& arg) { Json::Value jv; if (arg.issuer) - jv[sfAccount.jsonName] = arg.issuer->human(); + jv[sfAccount] = arg.issuer->human(); else - jv[sfAccount.jsonName] = issuer_.human(); + jv[sfAccount] = issuer_.human(); if (arg.id) - jv[sfMPTokenIssuanceID.jsonName] = to_string(*arg.id); + jv[sfMPTokenIssuanceID] = to_string(*arg.id); else { assert(id_); - jv[sfMPTokenIssuanceID.jsonName] = to_string(*id_); + jv[sfMPTokenIssuanceID] = to_string(*id_); } - jv[sfTransactionType.jsonName] = jss::MPTokenIssuanceDestroy; + jv[sfTransactionType] = jss::MPTokenIssuanceDestroy; submit(arg, jv); } @@ -144,19 +144,19 @@ MPTTester::authorize(MPTAuthorize const& arg) { Json::Value jv; if (arg.account) - jv[sfAccount.jsonName] = arg.account->human(); + jv[sfAccount] = arg.account->human(); else - jv[sfAccount.jsonName] = issuer_.human(); - jv[sfTransactionType.jsonName] = jss::MPTokenAuthorize; + jv[sfAccount] = issuer_.human(); + jv[sfTransactionType] = jss::MPTokenAuthorize; if (arg.id) - jv[sfMPTokenIssuanceID.jsonName] = to_string(*arg.id); + jv[sfMPTokenIssuanceID] = to_string(*arg.id); else { assert(id_); - jv[sfMPTokenIssuanceID.jsonName] = to_string(*id_); + jv[sfMPTokenIssuanceID] = to_string(*id_); } if (arg.holder) - jv[sfMPTokenHolder.jsonName] = arg.holder->human(); + jv[sfMPTokenHolder] = arg.holder->human(); if (auto const result = submit(arg, jv); result == tesSUCCESS) { // Issuer authorizes @@ -209,19 +209,19 @@ MPTTester::set(MPTSet const& arg) { Json::Value jv; if (arg.account) - jv[sfAccount.jsonName] = arg.account->human(); + jv[sfAccount] = arg.account->human(); else - jv[sfAccount.jsonName] = issuer_.human(); - jv[sfTransactionType.jsonName] = jss::MPTokenIssuanceSet; + jv[sfAccount] = issuer_.human(); + jv[sfTransactionType] = jss::MPTokenIssuanceSet; if (arg.id) - jv[sfMPTokenIssuanceID.jsonName] = to_string(*arg.id); + jv[sfMPTokenIssuanceID] = to_string(*arg.id); else { assert(id_); - jv[sfMPTokenIssuanceID.jsonName] = to_string(*id_); + jv[sfMPTokenIssuanceID] = to_string(*id_); } if (arg.holder) - jv[sfMPTokenHolder.jsonName] = arg.holder->human(); + jv[sfMPTokenHolder] = arg.holder->human(); if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0)) { auto require = [&](std::optional const& holder, diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 5ec7d598f75..3854609171e 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -88,6 +88,9 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer); [[nodiscard]] bool isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue); +[[nodiscard]] bool +isGlobalFrozen(ReadView const& view, Asset const& asset); + [[nodiscard]] bool isIndividualFrozen( ReadView const& view, @@ -104,12 +107,25 @@ isIndividualFrozen( return isIndividualFrozen(view, account, issue.currency, issue.account); } -[[nodiscard]] inline bool +[[nodiscard]] bool isIndividualFrozen( ReadView const& view, AccountID const& account, MPTIssue const& mptIssue); +[[nodiscard]] inline bool +isIndividualFrozen( + ReadView const& view, + AccountID const& account, + Asset const& asset) +{ + return std::visit( + [&](auto const& issue) { + return isIndividualFrozen(view, account, issue); + }, + asset.value()); +} + [[nodiscard]] bool isFrozen( ReadView const& view, @@ -129,6 +145,14 @@ isFrozen( AccountID const& account, MPTIssue const& mptIssue); +[[nodiscard]] bool +isFrozen(ReadView const& view, AccountID const& account, Asset const& asset) +{ + return std::visit( + [&](auto const& issue) { return isFrozen(view, account, issue); }, + asset.value()); +} + // Returns the amount an account can spend without going into debt. // // <-- saAmount: amount of currency held by account. May be negative. @@ -234,9 +258,19 @@ forEachItemAfter( return forEachItemAfter(view, keylet::ownerDir(id), after, hint, limit, f); } +/** Returns IOU issuer transfer fee as Rate. Rate specifies + * the fee as fractions of 1 billion. For example, 1% transfer rate + * is represented as 1,010,000,000. + * @param issuer The IOU issuer + */ [[nodiscard]] Rate transferRate(ReadView const& view, AccountID const& issuer); +/** Returns MPT transfer fee as Rate. Rate specifies + * the fee as fractions of 1 billion. For example, 1% transfer rate + * is represented as 1,010,000,000. + * @param issuanceID MPTokenIssuanceID of MPTTokenIssuance object + */ [[nodiscard]] Rate transferRate(ReadView const& view, MPTID const& issuanceID); diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 783d3c39b31..540165fd00b 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -185,6 +185,19 @@ isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue) return false; } +bool +isGlobalFrozen(ReadView const& view, Asset const& asset) +{ + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + return isGlobalFrozen(view, issue.getIssuer()); + else + return isGlobalFrozen(view, issue); + }, + asset.value()); +} + bool isIndividualFrozen( ReadView const& view, From 38781dfb2d4a95f5a98c6399db27cb03d5ab15c3 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 17 Oct 2024 12:29:27 -0400 Subject: [PATCH 39/58] Fix inline isFrozen definition --- src/xrpld/ledger/View.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 3854609171e..b27ba6082fd 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -145,7 +145,7 @@ isFrozen( AccountID const& account, MPTIssue const& mptIssue); -[[nodiscard]] bool +[[nodiscard]] inline bool isFrozen(ReadView const& view, AccountID const& account, Asset const& asset) { return std::visit( From e239fcbf64a4a628161a21217652652bef5d9949 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 17 Oct 2024 13:35:16 -0400 Subject: [PATCH 40/58] Address reviewer feedback * Add static functions rippleCredit[MPT/IOU]() and accountSend[MPT/IOU]() * Change rippleCredit() and accountSend() to work for any asset by calling the static versions --- src/xrpld/app/tx/detail/Clawback.cpp | 3 +- src/xrpld/app/tx/detail/Payment.cpp | 2 +- src/xrpld/ledger/View.h | 24 +--- src/xrpld/ledger/detail/View.cpp | 189 ++++++++++++++++----------- 4 files changed, 126 insertions(+), 92 deletions(-) diff --git a/src/xrpld/app/tx/detail/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index 2ea9878e590..b4e68ab7c73 100644 --- a/src/xrpld/app/tx/detail/Clawback.cpp +++ b/src/xrpld/app/tx/detail/Clawback.cpp @@ -271,11 +271,12 @@ applyHelper(ApplyContext& ctx) ahIGNORE_AUTH, ctx.journal); - return rippleCreditMPT( + return rippleCredit( ctx.view(), holder, issuer, std::min(spendableAmount, clawAmount), + /*checkIssuer*/ false, ctx.journal); } diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 40448e85837..25ec119d6ae 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -498,7 +498,7 @@ Payment::doApply() return tecPATH_PARTIAL; PaymentSandbox pv(&view()); - auto res = accountSendMPT( + auto res = accountSend( pv, account_, dstAccountID, amountDeliver, ctx_.journal); if (res == tesSUCCESS) pv.apply(ctx_.rawView()); diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index b27ba6082fd..80d41490912 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -475,6 +475,10 @@ offerDelete(ApplyView& view, std::shared_ptr const& sle, beast::Journal j); // - Create trust line of needed. // --> bCheckIssuer : normally require issuer to be involved. // [[nodiscard]] // nodiscard commented out so DirectStep.cpp compiles. + +/** Calls static rippleCreditIOU if saAmount represents Issue. + * Calls static rippleCreditMPT if saAmount represents MPTIssue. + */ TER rippleCredit( ApplyView& view, @@ -484,14 +488,9 @@ rippleCredit( bool bCheckIssuer, beast::Journal j); -[[nodiscard]] TER -rippleCreditMPT( - ApplyView& view, - AccountID const& uSenderID, - AccountID const& uReceiverID, - STAmount const& saAmount, - beast::Journal j); - +/** Calls static accountSendIOU if saAmount represents Issue. + * Calls static accountSendMPT if saAmount represents MPTIssue. + */ [[nodiscard]] TER accountSend( ApplyView& view, @@ -501,15 +500,6 @@ accountSend( beast::Journal j, WaiveTransferFee waiveFee = WaiveTransferFee::No); -[[nodiscard]] TER -accountSendMPT( - ApplyView& view, - AccountID const& from, - AccountID const& to, - STAmount const& saAmount, - beast::Journal j, - WaiveTransferFee waiveFee = WaiveTransferFee::No); - [[nodiscard]] TER issueIOU( ApplyView& view, diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 540165fd00b..fd687953fe4 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -189,7 +189,7 @@ bool isGlobalFrozen(ReadView const& view, Asset const& asset) { return std::visit( - [&](TIss const& issue) { + [&](TIss const& issue) { if constexpr (std::is_same_v) return isGlobalFrozen(view, issue.getIssuer()); else @@ -1039,8 +1039,8 @@ offerDelete(ApplyView& view, std::shared_ptr const& sle, beast::Journal j) // - Redeeming IOUs and/or sending sender's own IOUs. // - Create trust line if needed. // --> bCheckIssuer : normally require issuer to be involved. -TER -rippleCredit( +static TER +rippleCreditIOU( ApplyView& view, AccountID const& uSenderID, AccountID const& uReceiverID, @@ -1078,7 +1078,7 @@ rippleCredit( saBalance -= saAmount; - JLOG(j.trace()) << "rippleCredit: " << to_string(uSenderID) << " -> " + JLOG(j.trace()) << "rippleCreditIOU: " << to_string(uSenderID) << " -> " << to_string(uReceiverID) << " : before=" << saBefore.getFullText() << " amount=" << saAmount.getFullText() @@ -1153,7 +1153,7 @@ rippleCredit( saBalance.setIssuer(noAccount()); - JLOG(j.debug()) << "rippleCredit: " + JLOG(j.debug()) << "rippleCreditIOU: " "create line: " << to_string(uSenderID) << " -> " << to_string(uReceiverID) << " : " << saAmount.getFullText(); @@ -1185,7 +1185,7 @@ rippleCredit( // --> saAmount: Amount/currency/issuer to deliver to receiver. // <-- saActual: Amount actually cost. Sender pays fees. static TER -rippleSend( +rippleSendIOU( ApplyView& view, AccountID const& uSenderID, AccountID const& uReceiverID, @@ -1203,7 +1203,7 @@ rippleSend( { // Direct send: redeeming IOUs and/or sending own IOUs. auto const ter = - rippleCredit(view, uSenderID, uReceiverID, saAmount, false, j); + rippleCreditIOU(view, uSenderID, uReceiverID, saAmount, false, j); if (view.rules().enabled(featureDeletableAccounts) && ter != tesSUCCESS) return ter; saActual = saAmount; @@ -1218,21 +1218,22 @@ rippleSend( ? saAmount : multiply(saAmount, transferRate(view, issuer)); - JLOG(j.debug()) << "rippleSend> " << to_string(uSenderID) << " - > " + JLOG(j.debug()) << "rippleSendIOU> " << to_string(uSenderID) << " - > " << to_string(uReceiverID) << " : deliver=" << saAmount.getFullText() << " cost=" << saActual.getFullText(); - TER terResult = rippleCredit(view, issuer, uReceiverID, saAmount, true, j); + TER terResult = + rippleCreditIOU(view, issuer, uReceiverID, saAmount, true, j); if (tesSUCCESS == terResult) - terResult = rippleCredit(view, uSenderID, issuer, saActual, true, j); + terResult = rippleCreditIOU(view, uSenderID, issuer, saActual, true, j); return terResult; } -TER -accountSend( +static TER +accountSendIOU( ApplyView& view, AccountID const& uSenderID, AccountID const& uReceiverID, @@ -1262,11 +1263,11 @@ accountSend( { STAmount saActual; - JLOG(j.trace()) << "accountSend: " << to_string(uSenderID) << " -> " + JLOG(j.trace()) << "accountSendIOU: " << to_string(uSenderID) << " -> " << to_string(uReceiverID) << " : " << saAmount.getFullText(); - return rippleSend( + return rippleSendIOU( view, uSenderID, uReceiverID, saAmount, saActual, j, waiveFee); } @@ -1295,9 +1296,9 @@ accountSend( if (receiver) receiver_bal = receiver->getFieldAmount(sfBalance).getFullText(); - stream << "accountSend> " << to_string(uSenderID) << " (" << sender_bal - << ") -> " << to_string(uReceiverID) << " (" << receiver_bal - << ") : " << saAmount.getFullText(); + stream << "accountSendIOU> " << to_string(uSenderID) << " (" + << sender_bal << ") -> " << to_string(uReceiverID) << " (" + << receiver_bal << ") : " << saAmount.getFullText(); } if (sender) @@ -1341,14 +1342,74 @@ accountSend( if (receiver) receiver_bal = receiver->getFieldAmount(sfBalance).getFullText(); - stream << "accountSend< " << to_string(uSenderID) << " (" << sender_bal - << ") -> " << to_string(uReceiverID) << " (" << receiver_bal - << ") : " << saAmount.getFullText(); + stream << "accountSendIOU< " << to_string(uSenderID) << " (" + << sender_bal << ") -> " << to_string(uReceiverID) << " (" + << receiver_bal << ") : " << saAmount.getFullText(); } return terResult; } +static TER +rippleCreditMPT( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STAmount const& saAmount, + beast::Journal j) +{ + auto const mptID = keylet::mptIssuance(saAmount.get().getMptID()); + auto const issuer = saAmount.getIssuer(); + auto sleIssuance = view.peek(mptID); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + if (uSenderID == issuer) + { + (*sleIssuance)[sfOutstandingAmount] += saAmount.mpt().value(); + view.update(sleIssuance); + } + 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.mpt().value(); + if (amt < pay) + return tecINSUFFICIENT_FUNDS; + (*sle)[sfMPTAmount] = amt - pay; + view.update(sle); + } + else + return tecNO_AUTH; + } + + if (uReceiverID == issuer) + { + auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount); + auto const redeem = saAmount.mpt().value(); + if (outstanding >= redeem) + { + sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem); + view.update(sleIssuance); + } + else + return tecINTERNAL; + } + else + { + auto const mptokenID = keylet::mptoken(mptID.key, uReceiverID); + if (auto sle = view.peek(mptokenID)) + { + (*sle)[sfMPTAmount] += saAmount.mpt().value(); + view.update(sle); + } + else + return tecNO_AUTH; + } + return tesSUCCESS; +} + static TER rippleSendMPT( ApplyView& view, @@ -1400,7 +1461,7 @@ rippleSendMPT( saAmount, transferRate(view, saAmount.get().getMptID())); - JLOG(j.debug()) << "rippleSend> " << to_string(uSenderID) << " - > " + JLOG(j.debug()) << "rippleSendMPT> " << to_string(uSenderID) << " - > " << to_string(uReceiverID) << " : deliver=" << saAmount.getFullText() << " cost=" << saActual.getFullText(); @@ -1413,7 +1474,7 @@ rippleSendMPT( return rippleCreditMPT(view, uSenderID, issuer, saActual, j); } -TER +static TER accountSendMPT( ApplyView& view, AccountID const& uSenderID, @@ -1436,6 +1497,27 @@ accountSendMPT( view, uSenderID, uReceiverID, saAmount, saActual, j, waiveFee); } +TER +accountSend( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STAmount const& saAmount, + beast::Journal j, + WaiveTransferFee waiveFee) +{ + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + return accountSendIOU( + view, uSenderID, uReceiverID, saAmount, j, waiveFee); + else + return accountSendMPT( + view, uSenderID, uReceiverID, saAmount, j, waiveFee); + }, + saAmount.asset().value()); +} + static bool updateTrustLine( ApplyView& view, @@ -1896,63 +1978,24 @@ deleteAMMTrustLine( } TER -rippleCreditMPT( +rippleCredit( ApplyView& view, AccountID const& uSenderID, AccountID const& uReceiverID, STAmount const& saAmount, + bool bCheckIssuer, beast::Journal j) { - auto const mptID = keylet::mptIssuance(saAmount.get().getMptID()); - auto const issuer = saAmount.getIssuer(); - auto sleIssuance = view.peek(mptID); - if (!sleIssuance) - return tecOBJECT_NOT_FOUND; - if (uSenderID == issuer) - { - (*sleIssuance)[sfOutstandingAmount] += saAmount.mpt().value(); - view.update(sleIssuance); - } - 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.mpt().value(); - if (amt < pay) - return tecINSUFFICIENT_FUNDS; - (*sle)[sfMPTAmount] = amt - pay; - view.update(sle); - } - else - return tecNO_AUTH; - } - - if (uReceiverID == issuer) - { - auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount); - auto const redeem = saAmount.mpt().value(); - if (outstanding >= redeem) - { - sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem); - view.update(sleIssuance); - } - else - return tecINTERNAL; - } - else - { - auto const mptokenID = keylet::mptoken(mptID.key, uReceiverID); - if (auto sle = view.peek(mptokenID)) - { - (*sle)[sfMPTAmount] += saAmount.mpt().value(); - view.update(sle); - } - else - return tecNO_AUTH; - } - return tesSUCCESS; + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + return rippleCreditIOU( + view, uSenderID, uReceiverID, saAmount, bCheckIssuer, j); + else + return rippleCreditMPT( + view, uSenderID, uReceiverID, saAmount, j); + }, + saAmount.asset().value()); } } // namespace ripple From fe6fa603459f1c0f9d630f40014f55dc934a1626 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 17 Oct 2024 20:14:32 -0400 Subject: [PATCH 41/58] Fix clang-format --- include/xrpl/protocol/TER.h | 54 ++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 4710ef8f8a9..cf297b0c37b 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -484,60 +484,66 @@ class TERSubset // Only enabled if both arguments return int if TERtiInt is called with them. template constexpr auto -operator==(L const& lhs, R const& rhs) -> std::enable_if_t< - std::is_same::value && - std::is_same::value, - bool> +operator==(L const& lhs, R const& rhs) + -> std::enable_if_t< + std::is_same::value && + std::is_same::value, + bool> { return TERtoInt(lhs) == TERtoInt(rhs); } template constexpr auto -operator!=(L const& lhs, R const& rhs) -> std::enable_if_t< - std::is_same::value && - std::is_same::value, - bool> +operator!=(L const& lhs, R const& rhs) + -> std::enable_if_t< + std::is_same::value && + std::is_same::value, + bool> { return TERtoInt(lhs) != TERtoInt(rhs); } template constexpr auto -operator<(L const& lhs, R const& rhs) -> std::enable_if_t< - std::is_same::value && - std::is_same::value, - bool> +operator<(L const& lhs, R const& rhs) + -> std::enable_if_t< + std::is_same::value && + std::is_same::value, + bool> { return TERtoInt(lhs) < TERtoInt(rhs); } template constexpr auto -operator<=(L const& lhs, R const& rhs) -> std::enable_if_t< - std::is_same::value && - std::is_same::value, - bool> +operator<=(L const& lhs, R const& rhs) + -> std::enable_if_t< + std::is_same::value && + std::is_same::value, + bool> { return TERtoInt(lhs) <= TERtoInt(rhs); } template constexpr auto -operator>(L const& lhs, R const& rhs) -> std::enable_if_t< - std::is_same::value && - std::is_same::value, - bool> +operator>(L const& lhs, R const& rhs) + -> std::enable_if_t< + std::is_same::value && + std::is_same::value, + bool> { return TERtoInt(lhs) > TERtoInt(rhs); } template constexpr auto -operator>=(L const& lhs, R const& rhs) -> std::enable_if_t< - std::is_same::value && - std::is_same::value, - bool> +operator>=(L const& lhs, R const& rhs) + -> std::enable_if_t< + std::is_same::value && + std::is_same::value, + bool> { return TERtoInt(lhs) >= TERtoInt(rhs); } From f4bc120a8d8161f0a691a3392379d27e487e371d Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Fri, 18 Oct 2024 09:39:52 -0400 Subject: [PATCH 42/58] Address reviewer's feedback * Add assetFromJson, mptIssueFromJson * Minor refactoring --- include/xrpl/protocol/Asset.h | 3 ++ include/xrpl/protocol/MPTIssue.h | 3 ++ .../xrpl/protocol/detail/transactions.macro | 2 +- include/xrpl/protocol/jss.h | 4 +-- src/libxrpl/protocol/Asset.cpp | 12 +++++++ src/libxrpl/protocol/Issue.cpp | 6 ++++ src/libxrpl/protocol/MPTIssue.cpp | 35 ++++++++++++++++++- 7 files changed, 61 insertions(+), 4 deletions(-) diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index b787c153180..1eff2d8ed6c 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -169,6 +169,9 @@ to_string(Asset const& asset); bool validJSONAsset(Json::Value const& jv); +Asset +assetFromJson(Json::Value const& jv); + } // namespace ripple #endif // RIPPLE_PROTOCOL_ASSET_H_INCLUDED diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index 941a3be5cbd..06f55686caf 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -90,6 +90,9 @@ to_json(MPTIssue const& mptIssue); std::string to_string(MPTIssue const& mptIssue); +MPTIssue +mptIssueFromJson(Json::Value const& jv); + } // namespace ripple #endif // RIPPLE_PROTOCOL_MPTISSUE_H_INCLUDED diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 25f0fb22e2c..89f50497215 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -400,7 +400,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, ({ {sfMPTokenIssuanceID, soeREQUIRED}, })) -/** This transaction type sets a MPTokensIssuance instance */ +/** This transaction type sets flags on a MPTokensIssuance instance */ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfMPTokenHolder, soeOPTIONAL}, diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 539f1c29ae8..bafdde4fbcc 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -58,6 +58,8 @@ JSS(AssetPrice); // in: Oracle JSS(AuthAccount); // in: AMM Auction Slot JSS(AuthAccounts); // in: AMM Auction Slot JSS(BaseAsset); // in: Oracle +JSS(BidMax); // in: AMM Bid +JSS(BidMin); // in: AMM Bid JSS(Bridge); // ledger type. JSS(Check); // ledger type. JSS(ClearFlag); // field. @@ -76,8 +78,6 @@ 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(NetworkID); // field. diff --git a/src/libxrpl/protocol/Asset.cpp b/src/libxrpl/protocol/Asset.cpp index b265d4a504a..67323f8614b 100644 --- a/src/libxrpl/protocol/Asset.cpp +++ b/src/libxrpl/protocol/Asset.cpp @@ -58,4 +58,16 @@ validJSONAsset(Json::Value const& jv) return jv.isMember(jss::currency); } +Asset +assetFromJson(Json::Value const& v) +{ + if (!v.isMember(jss::currency) && !v.isMember(jss::mpt_issuance_id)) + Throw( + "assetFromJson must contain currency or mpt_issuance_id"); + + if (v.isMember(jss::currency)) + return issueFromJson(v); + return mptIssueFromJson(v); +} + } // namespace ripple diff --git a/src/libxrpl/protocol/Issue.cpp b/src/libxrpl/protocol/Issue.cpp index ff6b2b8767a..179cb1eb14a 100644 --- a/src/libxrpl/protocol/Issue.cpp +++ b/src/libxrpl/protocol/Issue.cpp @@ -95,6 +95,12 @@ issueFromJson(Json::Value const& v) "issueFromJson can only be specified with an 'object' Json value"); } + if (v.isMember(jss::mpt_issuance_id)) + { + Throw( + "issueFromJson, Issue should not have mpt_issuance_id"); + } + Json::Value const curStr = v[jss::currency]; Json::Value const issStr = v[jss::issuer]; diff --git a/src/libxrpl/protocol/MPTIssue.cpp b/src/libxrpl/protocol/MPTIssue.cpp index c68c26a40cd..4c712d0695f 100644 --- a/src/libxrpl/protocol/MPTIssue.cpp +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -17,7 +17,7 @@ */ //============================================================================== -#include +#include #include #include @@ -71,4 +71,37 @@ to_string(MPTIssue const& mptIssue) return to_string(mptIssue.getMptID()); } +MPTIssue +mptIssueFromJson(Json::Value const& v) +{ + if (!v.isObject()) + { + Throw( + "mptIssueFromJson can only be specified with an 'object' Json " + "value"); + } + + if (v.isMember(jss::currency) || v.isMember(jss::issuer)) + { + Throw( + "mptIssueFromJson, MPTIssue should not have currency or issuer"); + } + + Json::Value const idStr = v[jss::mpt_issuance_id]; + + if (!idStr.isString()) + { + Throw( + "mptIssueFromJson MPTID must be a string Json value"); + } + + MPTID id; + if (!id.parseHex(idStr.asString())) + { + Throw("mptIssueFromJson MPTID is invalid"); + } + + return MPTIssue{id}; +} + } // namespace ripple From 780af7adb69321edb441e3c589126734d4c70374 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:07:24 -0400 Subject: [PATCH 43/58] removed mptHolders (#43) * removed unused declarations * comments --- src/xrpld/rpc/MPTokenIssuanceID.h | 8 ++++++++ src/xrpld/rpc/detail/MPTokenIssuanceID.cpp | 4 ++-- src/xrpld/rpc/detail/Tuning.h | 3 --- src/xrpld/rpc/handlers/Handlers.h | 2 -- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h b/src/xrpld/rpc/MPTokenIssuanceID.h index f7f45fded3b..ef194bd398c 100644 --- a/src/xrpld/rpc/MPTokenIssuanceID.h +++ b/src/xrpld/rpc/MPTokenIssuanceID.h @@ -32,6 +32,14 @@ namespace ripple { namespace RPC { +/** + Add a `mpt_issuance_id` field to the `meta` input/output parameter. + The field is only added to successful MPTokenIssuanceCreate transactions. + The mpt_issuance_id is parsed from the sequence and the issuer in the + MPTokenIssuance object. + + @{ + */ bool canHaveMPTokenIssuanceID( std::shared_ptr const& serializedTx, diff --git a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp index 551f88deb55..721be652622 100644 --- a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp +++ b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2023 Ripple Labs Inc. + 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 @@ -75,7 +75,7 @@ insertMPTokenIssuanceID( return; std::optional result = getIDFromCreatedIssuance(transactionMeta); - if (result.has_value()) + if (result) response[jss::mpt_issuance_id] = to_string(result.value()); } diff --git a/src/xrpld/rpc/detail/Tuning.h b/src/xrpld/rpc/detail/Tuning.h index d4557a9fcfa..4f4a8be1bf7 100644 --- a/src/xrpld/rpc/detail/Tuning.h +++ b/src/xrpld/rpc/detail/Tuning.h @@ -57,9 +57,6 @@ 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/Handlers.h b/src/xrpld/rpc/handlers/Handlers.h index b1f65cdea57..0085f51465a 100644 --- a/src/xrpld/rpc/handlers/Handlers.h +++ b/src/xrpld/rpc/handlers/Handlers.h @@ -51,8 +51,6 @@ doBlackList(RPC::JsonContext&); Json::Value doCanDelete(RPC::JsonContext&); Json::Value -doMPTHolders(RPC::JsonContext&); -Json::Value doChannelAuthorize(RPC::JsonContext&); Json::Value doChannelVerify(RPC::JsonContext&); From e4009c8661bcf071f1a1dfd46f0b457fa42e08fa Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Fri, 18 Oct 2024 15:51:03 -0400 Subject: [PATCH 44/58] address comments (#44) --- src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 21 ++++++++++++++----- src/xrpld/app/tx/detail/MPTokenAuthorize.h | 2 +- .../app/tx/detail/MPTokenIssuanceCreate.cpp | 4 ++-- .../app/tx/detail/MPTokenIssuanceDestroy.cpp | 12 ++++++----- .../app/tx/detail/MPTokenIssuanceDestroy.h | 4 ++-- .../app/tx/detail/MPTokenIssuanceSet.cpp | 4 ++-- src/xrpld/app/tx/detail/MPTokenIssuanceSet.h | 2 +- 7 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index 7aea2020a47..0ad9252cb6d 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2023 Ripple Labs Inc. + 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 @@ -63,9 +63,11 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) 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) + // There is an edge case where all holders have zero balance, issuance + // is legally destroyed, then outstanding MPT(s) are deleted afterwards. + // Thus, there is no need to check for the existence of the issuance if + // the MPT is being deleted with a zero balance. Check for unauthorize + // before fetching the MPTIssuance object. // if holder wants to delete/unauthorize a mpt if (ctx.tx.getFlags() & tfMPTUnauthorize) @@ -74,7 +76,15 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) return tecOBJECT_NOT_FOUND; if ((*sleMpt)[sfMPTAmount] != 0) + { + auto const sleMptIssuance = ctx.view.read( + keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + assert(sleMptIssuance); + if (!sleMptIssuance) + return tefINTERNAL; + return tecHAS_OBLIGATIONS; + } return tesSUCCESS; } @@ -118,6 +128,7 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) if (!(mptIssuanceFlags & lsfMPTRequireAuth)) return tecNO_AUTH; + // The holder must create the MPT before the issuer can authorize it. if (!ctx.view.exists( keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], *holderID))) return tecOBJECT_NOT_FOUND; @@ -148,7 +159,7 @@ MPTokenAuthorize::authorize( auto const mptokenKey = keylet::mptoken(args.mptIssuanceID, args.account); auto const sleMpt = view.peek(mptokenKey); - if (!sleMpt) + if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0) return tecINTERNAL; if (!view.dirRemove( diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index 94451a61c88..79dc1734b5b 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.h +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2023 Ripple Labs Inc. + 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 diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp index a164584aa81..1297a918e1d 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2023 Ripple Labs Inc. + 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 @@ -55,7 +55,7 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) return temMALFORMED; } - // Check if maximumAmount is within 63 bit range + // Check if maximumAmount is within unsigned 63 bit range if (auto const maxAmt = ctx.tx[~sfMaximumAmount]) { if (maxAmt == 0) diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp index 4eb6225c0b4..a0f0b9d8602 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2023 Ripple Labs Inc. + 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 @@ -18,6 +18,7 @@ //============================================================================== #include + #include #include #include @@ -66,17 +67,18 @@ MPTokenIssuanceDestroy::doApply() { auto const mpt = view().peek(keylet::mptIssuance(ctx_.tx[sfMPTokenIssuanceID])); - auto const issuer = (*mpt)[sfIssuer]; + if (account_ != mpt->getAccountID(sfIssuer)) + return tecINTERNAL; if (!view().dirRemove( - keylet::ownerDir(issuer), (*mpt)[sfOwnerNode], mpt->key(), false)) + keylet::ownerDir(account_), (*mpt)[sfOwnerNode], mpt->key(), false)) return tefBAD_LEDGER; view().erase(mpt); - adjustOwnerCount(view(), view().peek(keylet::account(issuer)), -1, j_); + adjustOwnerCount(view(), view().peek(keylet::account(account_)), -1, j_); return tesSUCCESS; } -} // namespace ripple \ No newline at end of file +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h index 3e9f9b7e5cf..69abb99feb0 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2023 Ripple Labs Inc. + 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 @@ -45,4 +45,4 @@ class MPTokenIssuanceDestroy : public Transactor } // namespace ripple -#endif \ No newline at end of file +#endif diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index 2b4ff2bcb8a..352fa674cb8 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2023 Ripple Labs Inc. + 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 @@ -115,4 +115,4 @@ MPTokenIssuanceSet::doApply() return tesSUCCESS; } -} // namespace ripple \ No newline at end of file +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h index 36080d46667..895be973120 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2023 Ripple Labs Inc. + 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 From 5f33c804ec7a74e2ff201f9c619853301f5c204f Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:02:47 -0400 Subject: [PATCH 45/58] move flags (#45) --- include/xrpl/protocol/TxFlags.h | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 5b26488ab12..4894f48a7f9 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -141,13 +141,20 @@ 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; +constexpr std::uint32_t const tfMPTokenIssuanceCreateMask = + ~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback); // MPTokenAuthorize flags: constexpr std::uint32_t const tfMPTUnauthorize = 0x00000001; +constexpr std::uint32_t const tfMPTokenAuthorizeMask = ~(tfUniversal | tfMPTUnauthorize); // MPTokenIssuanceSet flags: constexpr std::uint32_t const tfMPTLock = 0x00000001; constexpr std::uint32_t const tfMPTUnlock = 0x00000002; +constexpr std::uint32_t const tfMPTokenIssuanceSetMask = ~(tfUniversal | tfMPTLock | tfMPTUnlock); + +// MPTokenIssuanceDestroy flags: +constexpr std::uint32_t const tfMPTokenIssuanceDestroyMask = ~tfUniversal; // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between // accounts allowed a TrustLine to be added to the issuer of that token @@ -203,19 +210,6 @@ constexpr std::uint32_t tfDepositMask = ~(tfUniversal | tfDepositSubTx); // BridgeModify flags: 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 From e8c8329437f7ea683bf4699f32bfcbbd82f8b980 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Mon, 21 Oct 2024 13:15:11 -0400 Subject: [PATCH 46/58] Address reviewer's feedback - refactor mpt.[h,cpp] --- src/libxrpl/protocol/MPTIssue.cpp | 2 +- src/test/jtx/impl/mpt.cpp | 72 +++++++++++++++++-------------- src/test/jtx/mpt.h | 4 +- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/libxrpl/protocol/MPTIssue.cpp b/src/libxrpl/protocol/MPTIssue.cpp index 4c712d0695f..38022a0ed3a 100644 --- a/src/libxrpl/protocol/MPTIssue.cpp +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -87,7 +87,7 @@ mptIssueFromJson(Json::Value const& v) "mptIssueFromJson, MPTIssue should not have currency or issuer"); } - Json::Value const idStr = v[jss::mpt_issuance_id]; + Json::Value const& idStr = v[jss::mpt_issuance_id]; if (!idStr.isString()) { diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index e3e65bad65b..93bef83721a 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2023 Ripple Labs Inc. + 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 @@ -106,8 +106,8 @@ MPTTester::create(const MPTCreate& arg) id_.reset(); } - else if (arg.flags) - env_.require(mptflags(*this, *arg.flags)); + else + env_.require(mptflags(*this, arg.flags.value_or(0))); } void @@ -122,7 +122,8 @@ MPTTester::destroy(MPTDestroy const& arg) jv[sfMPTokenIssuanceID] = to_string(*arg.id); else { - assert(id_); + if (!id_) + Throw("MPT has not been created"); jv[sfMPTokenIssuanceID] = to_string(*id_); } jv[sfTransactionType] = jss::MPTokenIssuanceDestroy; @@ -133,7 +134,6 @@ 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; @@ -152,7 +152,8 @@ MPTTester::authorize(MPTAuthorize const& arg) jv[sfMPTokenIssuanceID] = to_string(*arg.id); else { - assert(id_); + if (!id_) + Throw("MPT has not been created"); jv[sfMPTokenIssuanceID] = to_string(*id_); } if (arg.holder) @@ -172,17 +173,24 @@ MPTTester::authorize(MPTAuthorize const& arg) mptflags(*this, flags | lsfMPTAuthorized, arg.holder)); } // Holder authorizes - else if (arg.flags.value_or(0) == 0) + else if (arg.flags.value_or(0) != tfMPTUnauthorize) { auto const flags = getFlags(arg.account); // holder creates a token env_.require(mptflags(*this, flags, arg.account)); env_.require(mptbalance(*this, *arg.account, 0)); } + else + { + // Verify that the MPToken doesn't exist. + forObject( + [&](SLEP const& sle) { return env_.test.BEAST_EXPECT(!sle); }, + arg.account); + } } else if ( - arg.account && *arg.account != issuer_ && arg.flags.value_or(0) == 0 && - id_) + arg.account && *arg.account != issuer_ && + arg.flags.value_or(0) != tfMPTUnauthorize && id_) { if (result == tecDUPLICATE) { @@ -217,7 +225,8 @@ MPTTester::set(MPTSet const& arg) jv[sfMPTokenIssuanceID] = to_string(*arg.id); else { - assert(id_); + if (!id_) + Throw("MPT has not been created"); jv[sfMPTokenIssuanceID] = to_string(*id_); } if (arg.holder) @@ -250,12 +259,10 @@ MPTTester::forObject( std::function const& cb, std::optional const& holder_) const { - assert(id_); - auto const key = [&]() { - if (holder_) - return keylet::mptoken(*id_, holder_->id()); - return keylet::mptIssuance(*id_); - }(); + if (!id_) + Throw("MPT has not been created"); + auto const key = holder_ ? keylet::mptoken(*id_, holder_->id()) + : keylet::mptIssuance(*id_); if (auto const sle = env_.le(key)) return cb(sle); return false; @@ -294,14 +301,12 @@ MPTTester::pay( std::int64_t amount, std::optional err) { - assert(id_); + if (!id_) + Throw("MPT has not been created"); auto const srcAmt = getBalance(src); auto const destAmt = getBalance(dest); auto const outstnAmt = getBalance(issuer_); - if (err) - env_(jtx::pay(src, dest, mpt(amount)), ter(*err)); - else - env_(jtx::pay(src, dest, mpt(amount))); + env_(jtx::pay(src, dest, mpt(amount)), ter(err.value_or(tesSUCCESS))); if (env_.ter() != tesSUCCESS) amount = 0; if (close_) @@ -319,14 +324,15 @@ MPTTester::pay( else { STAmount const saAmount = {*id_, amount}; - STAmount const saActual = - multiply(saAmount, transferRate(*env_.current(), *id_)); + auto const actual = + multiply(saAmount, transferRate(*env_.current(), *id_)) + .mpt() + .value(); // Sender pays the transfer fee if any - env_.require(mptbalance(*this, src, srcAmt - saActual.mpt().value())); + env_.require(mptbalance(*this, src, srcAmt - actual)); env_.require(mptbalance(*this, dest, destAmt + amount)); // Outstanding amount is reduced by the transfer fee if any - env_.require(mptbalance( - *this, issuer_, outstnAmt - (saActual - saAmount).mpt().value())); + env_.require(mptbalance(*this, issuer_, outstnAmt - (actual - amount))); } } @@ -337,13 +343,11 @@ MPTTester::claw( std::int64_t amount, std::optional err) { - assert(id_); + if (!id_) + Throw("MPT has not been created"); auto const issuerAmt = getBalance(issuer); auto const holderAmt = getBalance(holder); - if (err) - env_(jtx::claw(issuer, mpt(amount), holder), ter(*err)); - else - env_(jtx::claw(issuer, mpt(amount), holder)); + env_(jtx::claw(issuer, mpt(amount), holder), ter(err.value_or(tesSUCCESS))); if (env_.ter() != tesSUCCESS) amount = 0; if (close_) @@ -358,14 +362,16 @@ MPTTester::claw( PrettyAmount MPTTester::mpt(std::int64_t amount) const { - assert(id_); + if (!id_) + Throw("MPT has not been created"); return ripple::test::jtx::MPT(issuer_.name(), *id_)(amount); } std::int64_t MPTTester::getBalance(Account const& account) const { - assert(id_); + if (!id_) + Throw("MPT has not been created"); if (account == issuer_) { if (auto const sle = env_.le(keylet::mptIssuance(*id_))) diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index aa47fcad8e2..16a08d8bad9 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2023 Ripple Labs Inc. + 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 @@ -240,7 +240,7 @@ class MPTTester return err; } - std::unordered_map + static std::unordered_map makeHolders(std::vector const& holders); std::uint32_t From c53df59fdbe261699cd29c29ac8008e65a86e795 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Mon, 21 Oct 2024 13:31:59 -0400 Subject: [PATCH 47/58] Apply suggestions from code review - update MPT unit-tests Co-authored-by: Ed Hennis --- src/test/app/MPToken_test.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 4fc100c7f99..9ec78f62d51 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -100,7 +100,7 @@ class MPToken_test : public beast::unit_test::suite .metadata = "test", .err = temMALFORMED}); mptAlice.create( - {.maxAmt = 0x8000'0000'0000'0000, // 9'223'372'036'854'775'808 + {.maxAmt = maxMPTokenAmount + 1, // 9'223'372'036'854'775'808 .assetScale = 0, .transferFee = 0, .metadata = "test", @@ -122,7 +122,7 @@ class MPToken_test : public beast::unit_test::suite Env env{*this, features}; MPTTester mptAlice(env, alice); mptAlice.create( - {.maxAmt = 0x7FFF'FFFF'FFFF'FFFF, // 9'223'372'036'854'775'807 + {.maxAmt = maxMPTokenAmount, // 9'223'372'036'854'775'807 .assetScale = 1, .transferFee = 10, .metadata = "123", @@ -235,6 +235,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.create({.ownerCount = 1}); + // The only valid MPTokenAuthorize flag is tfMPTUnauthorize, which has a value of 1 mptAlice.authorize( {.account = bob, .flags = 0x00000002, .err = temINVALID_FLAG}); @@ -356,11 +357,13 @@ class MPToken_test : public beast::unit_test::suite auto const acctReserve = env.current()->fees().accountReserve(0); auto const incReserve = env.current()->fees().increment; + // 1 drop + BEAST_EXPECT(incReserve > XRPAmount(1)); MPTTester mptAlice1( env, alice, {.holders = {bob}, - .xrpHolders = acctReserve + XRP(1).value().xrp()}); + .xrpHolders = acctReserve + (incReserve - 1)}); mptAlice1.create(); MPTTester mptAlice2(env, alice, {.fund = false}); @@ -483,7 +486,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.authorize({.account = bob, .holderCount = 1}); - // test invalid flag + // test invalid flag - only valid flags are tfMPTLock (1) and Unlock (2) mptAlice.set( {.account = alice, .flags = 0x00000008, From b8f3b83f814ba7d54594486f74bbe7dd119877df Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Mon, 21 Oct 2024 13:34:46 -0400 Subject: [PATCH 48/58] Fix clang-format --- src/test/app/MPToken_test.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 9ec78f62d51..6f71ab61afc 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -235,7 +235,8 @@ class MPToken_test : public beast::unit_test::suite mptAlice.create({.ownerCount = 1}); - // The only valid MPTokenAuthorize flag is tfMPTUnauthorize, which has a value of 1 + // The only valid MPTokenAuthorize flag is tfMPTUnauthorize, which + // has a value of 1 mptAlice.authorize( {.account = bob, .flags = 0x00000002, .err = temINVALID_FLAG}); @@ -486,7 +487,8 @@ class MPToken_test : public beast::unit_test::suite mptAlice.authorize({.account = bob, .holderCount = 1}); - // test invalid flag - only valid flags are tfMPTLock (1) and Unlock (2) + // test invalid flag - only valid flags are tfMPTLock (1) and Unlock + // (2) mptAlice.set( {.account = alice, .flags = 0x00000008, From 5349827e6da7776fa7d0a89a9a7763258d982018 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Mon, 21 Oct 2024 14:20:15 -0400 Subject: [PATCH 49/58] Address reviewer's feedback - add MPT unit-tests --- src/test/app/MPToken_test.cpp | 37 ++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 6f71ab61afc..0b209b2bc0a 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -768,15 +768,20 @@ class MPToken_test : public beast::unit_test::suite { Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {bob}}); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); auto const MPT = mptAlice["MPT"]; mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); mptAlice.pay(alice, bob, -1, temBAD_AMOUNT); + mptAlice.pay(bob, carol, -1, temBAD_AMOUNT); + + mptAlice.pay(bob, alice, -1, temBAD_AMOUNT); + env(pay(alice, bob, MPT(10)), sendmax(MPT(-1)), ter(temBAD_AMOUNT)); } @@ -1189,6 +1194,36 @@ class MPToken_test : public beast::unit_test::suite ter(tecPATH_PARTIAL)); } + // Pay maximum allowed amount + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create( + {.maxAmt = maxMPTokenAmount, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer}); + auto const MPT = mptAlice["MPT"]; + + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + + // issuer sends holder the max amount allowed + mptAlice.pay(alice, bob, maxMPTokenAmount); + BEAST_EXPECT( + mptAlice.checkMPTokenOutstandingAmount(maxMPTokenAmount)); + + // payment between the holders + mptAlice.pay(bob, carol, maxMPTokenAmount); + BEAST_EXPECT( + mptAlice.checkMPTokenOutstandingAmount(maxMPTokenAmount)); + // holder pays back to the issuer + mptAlice.pay(carol, alice, maxMPTokenAmount); + BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(0)); + } + // Issuer fails trying to send fund after issuance was destroyed { Env env{*this, features}; From 0905e37080318d4025b566d65ed8956c3f59b4d5 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Tue, 22 Oct 2024 15:09:37 -0400 Subject: [PATCH 50/58] Apply suggestions from code review Co-authored-by: John Freeman --- include/xrpl/protocol/Asset.h | 4 ++-- include/xrpl/protocol/detail/transactions.macro | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index 1eff2d8ed6c..bfb72ab61fc 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -32,8 +32,8 @@ concept ValidIssueType = /* Asset is an abstraction of three different issue types: XRP, IOU, MPT. * For historical reasons, two issue types XRP and IOU are wrapped in Issue - * type. Asset replaces Issue where any issue type is expected. For instance, - * STAmount replaces Issue with Asset to represent any issue amount. + * type. Many functions and classes there were first written for Issue + * have been rewritten for Asset. */ class Asset { diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 89f50497215..3696405b430 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -400,7 +400,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, ({ {sfMPTokenIssuanceID, soeREQUIRED}, })) -/** This transaction type sets flags on a MPTokensIssuance instance */ +/** This transaction type sets flags on a MPTokensIssuance or MPToken instance */ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfMPTokenHolder, soeOPTIONAL}, From 9d84f3b99e942a67767f0ff8b5b4e34260077f20 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Tue, 22 Oct 2024 18:05:22 -0400 Subject: [PATCH 51/58] Address reviewer's feedback --- src/xrpld/ledger/View.h | 2 +- src/xrpld/ledger/detail/View.cpp | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 80d41490912..74027752486 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -524,7 +524,7 @@ transferXRP( STAmount const& amount, beast::Journal j); -/** Check if the account requires authorization. +/** Check if the account lacks required authorization. * Return tecNO_AUTH or tecNO_LINE if it does * and tesSUCCESS otherwise. */ diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index fd687953fe4..ae4eb095017 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -1989,11 +1989,16 @@ rippleCredit( return std::visit( [&](TIss const& issue) { if constexpr (std::is_same_v) + { return rippleCreditIOU( view, uSenderID, uReceiverID, saAmount, bCheckIssuer, j); + } else + { + assert(!bCheckIssuer); return rippleCreditMPT( view, uSenderID, uReceiverID, saAmount, j); + } }, saAmount.asset().value()); } From e8201f36d563c97cd22a44284cae0c402f7148a2 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:25:33 -0400 Subject: [PATCH 52/58] address comments (#46) --- src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index 0ad9252cb6d..b63d111a207 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -49,9 +49,6 @@ 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 @@ -106,6 +103,9 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } + if (!ctx.view.exists(keylet::account(*holderID))) + return tecNO_DST; + auto const sleMptIssuance = ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); if (!sleMptIssuance) @@ -178,6 +178,12 @@ MPTokenAuthorize::authorize( // 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 + + // The reserve that is required to create the MPToken. Note + // that although the reserve increases with every item + // an account owns, in the case of MPTokens we only + // *enforce* a reserve if the user owns more than two + // items. This is similar to the reserve requirements of trust lines. std::uint32_t const uOwnerCount = sleAcct->getFieldU32(sfOwnerCount); XRPAmount const reserveCreate( (uOwnerCount < 2) ? XRPAmount(beast::zero) From 5aa36e4f8c472362bb46e1879fd8f0d3d836b0a4 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 24 Oct 2024 18:48:05 -0400 Subject: [PATCH 53/58] Apply suggestions from code review Co-authored-by: Ed Hennis --- src/test/app/MPToken_test.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 0b209b2bc0a..1848315d51c 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -671,8 +671,8 @@ class MPToken_test : public beast::unit_test::suite 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); + jv[jss::tx_json][jss::Fee] = to_string(env.current()->fees().base); auto const jrr = env.rpc("json", "submit", to_string(jv)); BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "temDISABLED"); } @@ -1376,6 +1376,9 @@ class MPToken_test : public beast::unit_test::suite jv1[jss::tx_json] = jv; jrr = env.rpc("json", "submit", to_string(jv1)); BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); + + jrr = env.rpc("json", "sign", 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 From 7a6272e068f6c040dee935bf50f676cc62c9c490 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 24 Oct 2024 18:50:31 -0400 Subject: [PATCH 54/58] Address reviewer's feedback * Update MPT unit-test --- src/test/app/MPToken_test.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 1848315d51c..5d68541cc1c 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1331,10 +1331,8 @@ class MPToken_test : public beast::unit_test::suite 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. + // Exclude pseudo-transaction SetFee. Don't consider + // the Fee field since it's included in every transaction. if (e.supportMPT() == soeMPTNotSupported && e.sField().getName() != jss::Fee && format.getName() != jss::SetFee) @@ -1410,9 +1408,9 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Flags] = tfSingleAsset; test(jv, field.fieldName); }; - ammDeposit(sfAmount); for (SField const& field : - {std::ref(sfAmount2), + {std::ref(sfAmount), + std::ref(sfAmount2), std::ref(sfEPrice), std::ref(sfLPTokenOut)}) ammDeposit(field); From 3439bfac7cfd75255f35dbb209937b5a999675b3 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Fri, 25 Oct 2024 13:14:38 -0400 Subject: [PATCH 55/58] comments (#48) --- src/test/app/MPToken_test.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 5d68541cc1c..803ccd21458 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1649,7 +1649,6 @@ class MPToken_test : public beast::unit_test::suite 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; @@ -1663,13 +1662,18 @@ class MPToken_test : public beast::unit_test::suite std::string const txHash{ env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; - + BEAST_EXPECTS( + txHash == + "E11F0E0CA14219922B7881F060B9CEE67CFBC87E4049A441ED2AE348FF8FAC" + "0E", + txHash); Json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta]; - + auto const id = meta[jss::mpt_issuance_id].asString(); // Expect mpt_issuance_id field BEAST_EXPECT(meta.isMember(jss::mpt_issuance_id)); - BEAST_EXPECT( - meta[jss::mpt_issuance_id] == to_string(mptAlice.issuanceID())); + BEAST_EXPECT(id == to_string(mptAlice.issuanceID())); + BEAST_EXPECTS( + id == "00000004AE123A8556F3CF91154711376AFB0F894F832B3D", id); } void From b636836a47b48f758d64ca4ea96ee42a5035fde3 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:42:40 -0400 Subject: [PATCH 56/58] Rename mpt holder (#49) * rename * space --- include/xrpl/protocol/detail/sfields.macro | 2 +- include/xrpl/protocol/detail/transactions.macro | 6 +++--- src/test/app/MPToken_test.cpp | 2 +- src/test/jtx/impl/mpt.cpp | 4 ++-- src/test/jtx/impl/trust.cpp | 2 +- src/xrpld/app/tx/detail/Clawback.cpp | 11 +++++------ src/xrpld/app/tx/detail/InvariantCheck.cpp | 2 +- src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 6 +++--- src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp | 6 +++--- 9 files changed, 20 insertions(+), 21 deletions(-) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 3d75593a40d..e3a93fc7f46 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -270,7 +270,7 @@ TYPED_SFIELD(sfUnauthorize, ACCOUNT, 6) TYPED_SFIELD(sfRegularKey, ACCOUNT, 8) TYPED_SFIELD(sfNFTokenMinter, ACCOUNT, 9) TYPED_SFIELD(sfEmitCallback, ACCOUNT, 10) -TYPED_SFIELD(sfMPTokenHolder, ACCOUNT, 11) +TYPED_SFIELD(sfHolder, ACCOUNT, 11) // account (uncommon) TYPED_SFIELD(sfHookAccount, ACCOUNT, 16) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 3696405b430..30e27da4167 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -224,7 +224,7 @@ TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, ({ /** This transaction claws back issued tokens. */ TRANSACTION(ttCLAWBACK, 30, Clawback, ({ {sfAmount, soeREQUIRED, soeMPTSupported}, - {sfMPTokenHolder, soeOPTIONAL}, + {sfHolder, soeOPTIONAL}, })) /** This transaction type creates an AMM instance */ @@ -403,13 +403,13 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, ({ /** This transaction type sets flags on a MPTokensIssuance or MPToken instance */ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, ({ {sfMPTokenIssuanceID, soeREQUIRED}, - {sfMPTokenHolder, soeOPTIONAL}, + {sfHolder, soeOPTIONAL}, })) /** This transaction type authorizes a MPToken instance */ TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, ({ {sfMPTokenIssuanceID, soeREQUIRED}, - {sfMPTokenHolder, soeOPTIONAL}, + {sfHolder, soeOPTIONAL}, })) /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 803ccd21458..e666abbd2bd 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1648,7 +1648,7 @@ class MPToken_test : public beast::unit_test::suite void testTxJsonMetaFields(FeatureBitset features) { - // checks synthetically parsed mptissuanceid from `tx` response + // checks synthetically injected mptissuanceid from `tx` response testcase("Test synthetic fields from tx response"); using namespace test::jtx; diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 93bef83721a..b68fd4446de 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -157,7 +157,7 @@ MPTTester::authorize(MPTAuthorize const& arg) jv[sfMPTokenIssuanceID] = to_string(*id_); } if (arg.holder) - jv[sfMPTokenHolder] = arg.holder->human(); + jv[sfHolder] = arg.holder->human(); if (auto const result = submit(arg, jv); result == tesSUCCESS) { // Issuer authorizes @@ -230,7 +230,7 @@ MPTTester::set(MPTSet const& arg) jv[sfMPTokenIssuanceID] = to_string(*id_); } if (arg.holder) - jv[sfMPTokenHolder] = arg.holder->human(); + jv[sfHolder] = arg.holder->human(); if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0)) { auto require = [&](std::optional const& holder, diff --git a/src/test/jtx/impl/trust.cpp b/src/test/jtx/impl/trust.cpp index 320c7d05c7c..dee4b282367 100644 --- a/src/test/jtx/impl/trust.cpp +++ b/src/test/jtx/impl/trust.cpp @@ -75,7 +75,7 @@ claw( jv[jss::TransactionType] = jss::Clawback; if (mptHolder) - jv[sfMPTokenHolder.jsonName] = mptHolder->human(); + jv[sfHolder.jsonName] = mptHolder->human(); return jv; } diff --git a/src/xrpld/app/tx/detail/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index b4e68ab7c73..f1040790a42 100644 --- a/src/xrpld/app/tx/detail/Clawback.cpp +++ b/src/xrpld/app/tx/detail/Clawback.cpp @@ -36,7 +36,7 @@ template <> NotTEC preflightHelper(PreflightContext const& ctx) { - if (ctx.tx.isFieldPresent(sfMPTokenHolder)) + if (ctx.tx.isFieldPresent(sfHolder)) return temMALFORMED; AccountID const issuer = ctx.tx[sfAccount]; @@ -58,7 +58,7 @@ preflightHelper(PreflightContext const& ctx) if (!ctx.rules.enabled(featureMPTokensV1)) return temDISABLED; - auto const mptHolder = ctx.tx[~sfMPTokenHolder]; + auto const mptHolder = ctx.tx[~sfHolder]; auto const clawAmount = ctx.tx[sfAmount]; if (!mptHolder) @@ -199,9 +199,8 @@ Clawback::preclaim(PreclaimContext const& ctx) { AccountID const issuer = ctx.tx[sfAccount]; auto const clawAmount = ctx.tx[sfAmount]; - AccountID const holder = clawAmount.holds() - ? clawAmount.getIssuer() - : ctx.tx[sfMPTokenHolder]; + AccountID const holder = + clawAmount.holds() ? clawAmount.getIssuer() : ctx.tx[sfHolder]; auto const sleIssuer = ctx.view.read(keylet::account(issuer)); auto const sleHolder = ctx.view.read(keylet::account(holder)); @@ -260,7 +259,7 @@ applyHelper(ApplyContext& ctx) { AccountID const issuer = ctx.tx[sfAccount]; auto clawAmount = ctx.tx[sfAmount]; - AccountID const holder = ctx.tx[sfMPTokenHolder]; + AccountID const holder = ctx.tx[sfHolder]; // Get the spendable balance. Must use `accountHolds`. STAmount const spendableAmount = accountHolds( diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 625f8c7b284..e8bbd0283b5 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -1031,7 +1031,7 @@ ValidMPTIssuance::finalize( if (tx.getTxnType() == ttMPTOKEN_AUTHORIZE) { - bool const submittedByIssuer = tx.isFieldPresent(sfMPTokenHolder); + bool const submittedByIssuer = tx.isFieldPresent(sfHolder); if (mptIssuancesCreated_ > 0) { diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index b63d111a207..5b589530cfb 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -37,7 +37,7 @@ MPTokenAuthorize::preflight(PreflightContext const& ctx) if (ctx.tx.getFlags() & tfMPTokenAuthorizeMask) return temINVALID_FLAG; - if (ctx.tx[sfAccount] == ctx.tx[~sfMPTokenHolder]) + if (ctx.tx[sfAccount] == ctx.tx[~sfHolder]) return temMALFORMED; return preflight2(ctx); @@ -47,7 +47,7 @@ TER MPTokenAuthorize::preclaim(PreclaimContext const& ctx) { auto const accountID = ctx.tx[sfAccount]; - auto const holderID = ctx.tx[~sfMPTokenHolder]; + auto const holderID = ctx.tx[~sfHolder]; // if non-issuer account submits this tx, then they are trying either: // 1. Unauthorize/delete MPToken @@ -262,7 +262,7 @@ MPTokenAuthorize::doApply() .mptIssuanceID = tx[sfMPTokenIssuanceID], .account = account_, .flags = tx.getFlags(), - .holderID = tx[~sfMPTokenHolder]}); + .holderID = tx[~sfHolder]}); } } // namespace ripple diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index 352fa674cb8..4e395c30be6 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -44,7 +44,7 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) return temINVALID_FLAG; auto const accountID = ctx.tx[sfAccount]; - auto const holderID = ctx.tx[~sfMPTokenHolder]; + auto const holderID = ctx.tx[~sfHolder]; if (holderID && accountID == holderID) return temMALFORMED; @@ -68,7 +68,7 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) if ((*sleMptIssuance)[sfIssuer] != ctx.tx[sfAccount]) return tecNO_PERMISSION; - if (auto const holderID = ctx.tx[~sfMPTokenHolder]) + if (auto const holderID = ctx.tx[~sfHolder]) { // make sure holder account exists if (!ctx.view.exists(keylet::account(*holderID))) @@ -88,7 +88,7 @@ MPTokenIssuanceSet::doApply() { auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; auto const txFlags = ctx_.tx.getFlags(); - auto const holderID = ctx_.tx[~sfMPTokenHolder]; + auto const holderID = ctx_.tx[~sfHolder]; std::shared_ptr sle; if (holderID) From 7c08b588b281960c96969d101defec4edf0e6b4f Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Tue, 29 Oct 2024 12:37:38 -0400 Subject: [PATCH 57/58] Address reviewer's feedback * Remove asserts in tests * Update copyright --- src/test/app/MPToken_test.cpp | 2 +- src/test/jtx/impl/mpt.cpp | 8 +++++--- src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index e666abbd2bd..fa888faea17 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2023 Ripple Labs Inc. + 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 diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index b68fd4446de..d3611efe462 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -49,7 +49,8 @@ MPTTester::makeHolders(std::vector const& holders) std::unordered_map accounts; for (auto const& h : holders) { - assert(accounts.find(h.human()) == accounts.cend()); + if (accounts.find(h.human()) != accounts.cend()) + Throw("Duplicate holder"); accounts.emplace(h.human(), h); } return accounts; @@ -74,7 +75,8 @@ MPTTester::MPTTester(Env& env, Account const& issuer, MPTInit const& arg) env_.require(owners(issuer_, 0)); for (auto it : holders_) { - assert(issuer_.id() != it.second.id()); + if (issuer_.id() == it.second.id()) + Throw("Issuer can't be holder"); env_.require(owners(it.second, 0)); } } @@ -243,7 +245,7 @@ MPTTester::set(MPTSet const& arg) else if (*arg.flags & tfMPTUnlock) flags &= ~lsfMPTLocked; else - assert(0); + Throw("Invalid flags"); } env_.require(mptflags(*this, flags, holder)); }; diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h index 2fdf2bdd152..1346c3e31d7 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2023 Ripple Labs Inc. + 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 From 9ce20612847137d1865dde2ccdd525e2aebaf6bf Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:11:02 -0400 Subject: [PATCH 58/58] Ed's comments (#50) --- src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 1 - src/xrpld/rpc/detail/TransactionSign.cpp | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index 5b589530cfb..8042c9c6982 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -76,7 +76,6 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) { auto const sleMptIssuance = ctx.view.read( keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); - assert(sleMptIssuance); if (!sleMptIssuance) return tefINTERNAL; diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 401f808f56a..2f10387bc81 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -213,8 +213,8 @@ checkPayment( if (!dstAccountID) return RPC::invalid_field_error("tx_json.Destination"); - if (((doPath == false) && params.isMember(jss::build_path)) || - (params.isMember(jss::build_path) && amount.holds())) + if (params.isMember(jss::build_path) && + ((doPath == false) || amount.holds())) return RPC::make_error( rpcINVALID_PARAMS, "Field 'build_path' not allowed in this context.");