From bb8dd7296260a8a731622313ea12a2d4cf82ab7b Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Sun, 8 Dec 2024 09:55:49 -0500 Subject: [PATCH] MPT integration into DEX (merge with develop) --- include/xrpl/basics/MPTAmount.h | 27 + include/xrpl/protocol/AMMCore.h | 20 +- include/xrpl/protocol/AmountConversions.h | 87 +- include/xrpl/protocol/Asset.h | 76 +- include/xrpl/protocol/Book.h | 93 +- include/xrpl/protocol/Concepts.h | 72 ++ include/xrpl/protocol/Feature.h | 2 +- include/xrpl/protocol/Indexes.h | 2 +- include/xrpl/protocol/MPTIssue.h | 51 +- include/xrpl/protocol/PathAsset.h | 150 +++ include/xrpl/protocol/STAmount.h | 5 - include/xrpl/protocol/STIssue.h | 74 +- include/xrpl/protocol/STObject.h | 2 + include/xrpl/protocol/STPathSet.h | 144 ++- include/xrpl/protocol/STXChainBridge.h | 4 +- include/xrpl/protocol/TER.h | 1 + include/xrpl/protocol/detail/features.macro | 1 + .../xrpl/protocol/detail/ledger_entries.macro | 2 + include/xrpl/protocol/detail/sfields.macro | 2 + .../xrpl/protocol/detail/transactions.macro | 24 +- src/libxrpl/basics/MPTAmount.cpp | 17 + src/libxrpl/protocol/AMMCore.cpp | 44 +- src/libxrpl/protocol/Asset.cpp | 10 + src/libxrpl/protocol/Indexes.cpp | 85 +- src/libxrpl/protocol/MPTIssue.cpp | 12 + src/libxrpl/protocol/PathAsset.cpp | 85 ++ src/libxrpl/protocol/STAmount.cpp | 2 +- src/libxrpl/protocol/STIssue.cpp | 85 +- src/libxrpl/protocol/STObject.cpp | 6 + src/libxrpl/protocol/STParsedJSON.cpp | 82 +- src/libxrpl/protocol/STPathSet.cpp | 50 +- src/libxrpl/protocol/TER.cpp | 1 + src/test/app/AMM_test.cpp | 2 +- src/test/app/CrossingLimits_test.cpp | 39 +- src/test/app/Discrepancy_test.cpp | 1 - src/test/app/Flow_test.cpp | 4 - src/test/app/Freeze_test.cpp | 1 - src/test/app/MPToken_test.cpp | 1108 +++++++++++++++-- src/test/app/Offer_test.cpp | 93 +- src/test/app/PayStrand_test.cpp | 9 +- src/test/app/SetAuth_test.cpp | 1 - src/test/app/TheoreticalQuality_test.cpp | 9 +- src/test/app/TrustAndBalance_test.cpp | 1 - src/test/jtx/AMM.h | 32 +- src/test/jtx/PathSet.h | 14 +- src/test/jtx/TestHelpers.h | 60 + src/test/jtx/amount.h | 22 +- src/test/jtx/impl/AMM.cpp | 79 +- src/test/jtx/impl/TestHelpers.cpp | 162 ++- src/test/jtx/impl/amount.cpp | 7 + src/test/jtx/impl/paths.cpp | 13 +- src/xrpld/app/ledger/AcceptedLedgerTx.cpp | 3 +- src/xrpld/app/ledger/OrderBookDB.cpp | 54 +- src/xrpld/app/ledger/OrderBookDB.h | 10 +- src/xrpld/app/misc/AMMHelpers.h | 30 +- src/xrpld/app/misc/AMMUtils.h | 20 +- src/xrpld/app/misc/MPTUtils.h | 52 + src/xrpld/app/misc/NetworkOPs.cpp | 23 +- src/xrpld/app/misc/detail/AMMHelpers.cpp | 4 +- src/xrpld/app/misc/detail/AMMUtils.cpp | 105 +- src/xrpld/app/misc/detail/MPTUtils.cpp | 105 ++ src/xrpld/app/paths/AMMLiquidity.h | 23 +- src/xrpld/app/paths/AMMOffer.h | 10 +- src/xrpld/app/paths/AccountCurrencies.cpp | 4 +- src/xrpld/app/paths/AccountCurrencies.h | 6 +- .../{RippleLineCache.cpp => AssetCache.cpp} | 50 +- .../paths/{RippleLineCache.h => AssetCache.h} | 11 +- src/xrpld/app/paths/Flow.cpp | 148 +-- src/xrpld/app/paths/PathRequest.cpp | 216 ++-- src/xrpld/app/paths/PathRequest.h | 23 +- src/xrpld/app/paths/PathRequests.cpp | 38 +- src/xrpld/app/paths/PathRequests.h | 10 +- src/xrpld/app/paths/Pathfinder.cpp | 393 +++--- src/xrpld/app/paths/Pathfinder.h | 21 +- src/xrpld/app/paths/RippleCalc.cpp | 2 +- src/xrpld/app/paths/detail/AMMLiquidity.cpp | 36 +- src/xrpld/app/paths/detail/AMMOffer.cpp | 37 +- src/xrpld/app/paths/detail/AmountSpec.h | 206 ++- src/xrpld/app/paths/detail/BookStep.cpp | 211 ++-- src/xrpld/app/paths/detail/DirectStep.cpp | 11 +- src/xrpld/app/paths/detail/FlowDebugInfo.h | 4 +- .../app/paths/detail/MPTEndpointStep.cpp | 957 ++++++++++++++ src/xrpld/app/paths/detail/PathfinderUtils.h | 4 +- src/xrpld/app/paths/detail/PaySteps.cpp | 304 +++-- src/xrpld/app/paths/detail/Steps.h | 61 +- src/xrpld/app/paths/detail/StrandFlow.h | 15 +- .../app/paths/detail/XRPEndpointStep.cpp | 9 +- src/xrpld/app/tx/detail/AMMBid.cpp | 5 + src/xrpld/app/tx/detail/AMMClawback.cpp | 15 +- src/xrpld/app/tx/detail/AMMCreate.cpp | 144 ++- src/xrpld/app/tx/detail/AMMDelete.cpp | 5 + src/xrpld/app/tx/detail/AMMDeposit.cpp | 63 +- src/xrpld/app/tx/detail/AMMVote.cpp | 5 + src/xrpld/app/tx/detail/AMMWithdraw.cpp | 57 +- src/xrpld/app/tx/detail/AMMWithdraw.h | 4 +- src/xrpld/app/tx/detail/CashCheck.cpp | 356 ++++-- src/xrpld/app/tx/detail/CreateCheck.cpp | 85 +- src/xrpld/app/tx/detail/CreateOffer.cpp | 635 ++-------- src/xrpld/app/tx/detail/CreateOffer.h | 60 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 79 +- src/xrpld/app/tx/detail/MPTokenAuthorize.h | 2 +- src/xrpld/app/tx/detail/Offer.h | 94 +- src/xrpld/app/tx/detail/OfferStream.cpp | 138 +- src/xrpld/app/tx/detail/OfferStream.h | 1 + src/xrpld/app/tx/detail/Payment.cpp | 31 +- src/xrpld/ledger/View.h | 29 + src/xrpld/ledger/detail/View.cpp | 36 + src/xrpld/rpc/MPTokenIssuanceID.h | 2 +- src/xrpld/rpc/detail/MPTokenIssuanceID.cpp | 4 +- src/xrpld/rpc/detail/TransactionSign.cpp | 8 +- src/xrpld/rpc/handlers/AMMInfo.cpp | 5 +- src/xrpld/rpc/handlers/BookOffers.cpp | 184 ++- src/xrpld/rpc/handlers/Subscribe.cpp | 109 +- src/xrpld/rpc/handlers/Unsubscribe.cpp | 108 +- 114 files changed, 6117 insertions(+), 2338 deletions(-) create mode 100644 include/xrpl/protocol/Concepts.h create mode 100644 include/xrpl/protocol/PathAsset.h create mode 100644 src/libxrpl/protocol/PathAsset.cpp create mode 100644 src/xrpld/app/misc/MPTUtils.h create mode 100644 src/xrpld/app/misc/detail/MPTUtils.cpp rename src/xrpld/app/paths/{RippleLineCache.cpp => AssetCache.cpp} (81%) rename src/xrpld/app/paths/{RippleLineCache.h => AssetCache.h} (93%) create mode 100644 src/xrpld/app/paths/detail/MPTEndpointStep.cpp diff --git a/include/xrpl/basics/MPTAmount.h b/include/xrpl/basics/MPTAmount.h index 34f747a21be..432f243765c 100644 --- a/include/xrpl/basics/MPTAmount.h +++ b/include/xrpl/basics/MPTAmount.h @@ -49,6 +49,7 @@ class MPTAmount : private boost::totally_ordered, public: MPTAmount() = default; constexpr MPTAmount(MPTAmount const& other) = default; + constexpr MPTAmount(beast::Zero); constexpr MPTAmount& operator=(MPTAmount const& other) = default; @@ -82,6 +83,9 @@ 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. @@ -89,6 +93,9 @@ class MPTAmount : private boost::totally_ordered, constexpr value_type value() const; + friend std::istream& + operator>>(std::istream& s, MPTAmount& val); + static MPTAmount minPositiveAmount(); }; @@ -97,6 +104,11 @@ constexpr MPTAmount::MPTAmount(value_type value) : value_(value) { } +constexpr MPTAmount::MPTAmount(beast::Zero) +{ + *this = beast::zero; +} + constexpr MPTAmount& MPTAmount::operator=(beast::Zero) { @@ -127,6 +139,21 @@ 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) { diff --git a/include/xrpl/protocol/AMMCore.h b/include/xrpl/protocol/AMMCore.h index 32988af5fc7..c446d800a13 100644 --- a/include/xrpl/protocol/AMMCore.h +++ b/include/xrpl/protocol/AMMCore.h @@ -22,7 +22,7 @@ #include #include -#include +#include #include #include @@ -59,14 +59,14 @@ ammAccountID( /** Calculate Liquidity Provider Token (LPT) Currency. */ Currency -ammLPTCurrency(Currency const& cur1, Currency const& cur2); +ammLPTCurrency(Asset const& issue1, Asset const& issue2); /** Calculate LPT Issue from AMM asset pair. */ Issue ammLPTIssue( - Currency const& cur1, - Currency const& cur2, + Asset const& issue1, + Asset const& issue2, AccountID const& ammAccountID); /** Validate the amount. @@ -77,19 +77,19 @@ ammLPTIssue( NotTEC invalidAMMAmount( STAmount const& amount, - std::optional> const& pair = std::nullopt, + std::optional> const& pair = std::nullopt, bool validZero = false); NotTEC invalidAMMAsset( - Issue const& issue, - std::optional> const& pair = std::nullopt); + Asset const& issue, + std::optional> const& pair = std::nullopt); NotTEC invalidAMMAssetPair( - Issue const& issue1, - Issue const& issue2, - std::optional> const& pair = std::nullopt); + Asset const& issue1, + Asset const& issue2, + std::optional> const& pair = std::nullopt); /** Get time slot of the auction slot. */ diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h index 20a985d1190..ff46745413e 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -29,8 +30,9 @@ namespace ripple { inline STAmount -toSTAmount(IOUAmount const& iou, Issue const& iss) +toSTAmount(IOUAmount const& iou, Asset const& iss) { + ASSERT(iss.holds(), "ripple::toSTAmount : is Issue"); bool const isNeg = iou.signum() < 0; std::uint64_t const umant = isNeg ? -iou.mantissa() : iou.mantissa(); return STAmount(iss, umant, iou.exponent(), isNeg, STAmount::unchecked()); @@ -51,14 +53,25 @@ toSTAmount(XRPAmount const& xrp) } inline STAmount -toSTAmount(XRPAmount const& xrp, Issue const& iss) +toSTAmount(XRPAmount const& xrp, Asset const& iss) { - ASSERT( - isXRP(iss.account) && isXRP(iss.currency), - "ripple::toSTAmount : is XRP"); + ASSERT(isXRP(iss), "ripple::toSTAmount : is XRP"); return toSTAmount(xrp); } +inline STAmount +toSTAmount(MPTAmount const& mpt) +{ + return STAmount(mpt, noMPT()); +} + +inline STAmount +toSTAmount(MPTAmount const& mpt, Asset const& iss) +{ + ASSERT(iss.holds(), "ripple::toSTAmount : is MPT"); + return STAmount(mpt, iss.get()); +} + template T toAmount(STAmount const& amt) = delete; @@ -100,6 +113,20 @@ toAmount(STAmount const& amt) return XRPAmount(sMant); } +template <> +inline MPTAmount +toAmount(STAmount const& amt) +{ + ASSERT( + amt.holds() && amt.mantissa() <= maxMPTokenAmount, + "ripple::toAmount : maximum mantissa"); + bool const isNeg = amt.negative(); + std::int64_t const sMant = + isNeg ? -std::int64_t(amt.mantissa()) : amt.mantissa(); + + return MPTAmount(sMant); +} + template T toAmount(IOUAmount const& amt) = delete; @@ -122,10 +149,21 @@ toAmount(XRPAmount const& amt) return amt; } +template +T +toAmount(MPTAmount const& amt) = delete; + +template <> +inline MPTAmount +toAmount(MPTAmount const& amt) +{ + return amt; +} + template T toAmount( - Issue const& issue, + Asset const& issue, Number const& n, Number::rounding_mode mode = Number::getround()) { @@ -137,6 +175,8 @@ toAmount( return IOUAmount(n); else if constexpr (std::is_same_v) return XRPAmount(static_cast(n)); + else if constexpr (std::is_same_v) + return MPTAmount(static_cast(n)); else if constexpr (std::is_same_v) { if (isXRP(issue)) @@ -152,18 +192,31 @@ toAmount( template T -toMaxAmount(Issue const& issue) +toMaxAmount(Asset const& issue) { if constexpr (std::is_same_v) return IOUAmount(STAmount::cMaxValue, STAmount::cMaxOffset); else if constexpr (std::is_same_v) return XRPAmount(static_cast(STAmount::cMaxNativeN)); + else if constexpr (std::is_same_v) + return MPTAmount(maxMPTokenAmount); else if constexpr (std::is_same_v) { - if (isXRP(issue)) - return STAmount( - issue, static_cast(STAmount::cMaxNativeN)); - return STAmount(issue, STAmount::cMaxValue, STAmount::cMaxOffset); + return std::visit( + [](TIss const& issue_) { + if constexpr (std::is_same_v) + { + if (isXRP(issue_)) + return STAmount( + issue_, + static_cast(STAmount::cMaxNativeN)); + return STAmount( + issue_, STAmount::cMaxValue, STAmount::cMaxOffset); + } + else + return STAmount(issue_, maxMPTokenAmount); + }, + issue.value()); } else { @@ -174,7 +227,7 @@ toMaxAmount(Issue const& issue) inline STAmount toSTAmount( - Issue const& issue, + Asset const& issue, Number const& n, Number::rounding_mode mode = Number::getround()) { @@ -182,15 +235,17 @@ toSTAmount( } template -Issue -getIssue(T const& amt) +Asset +getAsset(T const& amt) { if constexpr (std::is_same_v) return noIssue(); else if constexpr (std::is_same_v) return xrpIssue(); + else if constexpr (std::is_same_v) + return noMPT(); else if constexpr (std::is_same_v) - return amt.issue(); + return amt.asset(); else { constexpr bool alwaysFalse = !std::is_same_v; @@ -206,6 +261,8 @@ get(STAmount const& a) return a.iou(); else if constexpr (std::is_same_v) return a.xrp(); + else if constexpr (std::is_same_v) + return a.mpt(); else if constexpr (std::is_same_v) return a; else diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index 2cccc28bd41..b5f6d3fd39b 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -21,15 +21,12 @@ #define RIPPLE_PROTOCOL_ASSET_H_INCLUDED #include +#include #include #include namespace ripple { -template -concept ValidIssueType = - std::is_same_v || std::is_same_v; - /* 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. Many functions and classes there were first written for Issue @@ -59,6 +56,12 @@ class Asset { } + explicit + operator Issue() const; + + explicit + operator MPTIssue() const; + AccountID const& getIssuer() const; @@ -95,6 +98,9 @@ class Asset friend constexpr bool operator!=(Asset const& lhs, Asset const& rhs); + friend constexpr std::weak_ordering + operator<=>(Asset const& lhs, Asset const& rhs); + friend constexpr bool operator==(Currency const& lhs, Asset const& rhs); @@ -163,6 +169,24 @@ operator==(Currency const& lhs, Asset const& rhs) return rhs.holds() && rhs.get().currency == lhs; } +constexpr std::weak_ordering +operator<=>(Asset const& lhs, Asset const& rhs) +{ + return std::visit( + []( + TLhs const& lhs_, TRhs const& rhs_) { + if constexpr (std::is_same_v) + return lhs_ <=> rhs_; + else if constexpr ( + std::is_same_v && std::is_same_v) + return std::weak_ordering::greater; + else + return std::weak_ordering::less; + }, + lhs.issue_, + rhs.issue_); +} + constexpr bool equalTokens(Asset const& lhs, Asset const& rhs) { @@ -201,6 +225,50 @@ assetFromJson(Json::Value const& jv); Json::Value to_json(Asset const& asset); +inline bool +isConsistent(Asset const& issue) +{ + return std::visit( + [&](TIss const& issue_) { + if constexpr (std::is_same_v) + return isConsistent(issue_); + else + return true; + }, + issue.value()); +} + +inline bool +validAsset(Asset const& issue) +{ + return std::visit( + [&](TIss const& issue_) { + if constexpr (std::is_same_v) + return isConsistent(issue_) && issue_.currency != badCurrency(); + else + return true; + }, + issue.value()); +} + +template +void +hash_append(Hasher& h, Asset const& r) +{ + using beast::hash_append; + std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + hash_append(h, issue); + else + hash_append(h, issue); + }, + r.value()); +} + +std::ostream& +operator<<(std::ostream& os, Asset const& x); + } // namespace ripple #endif // RIPPLE_PROTOCOL_ASSET_H_INCLUDED diff --git a/include/xrpl/protocol/Book.h b/include/xrpl/protocol/Book.h index 164a5ccfa99..e6b89e5c9b6 100644 --- a/include/xrpl/protocol/Book.h +++ b/include/xrpl/protocol/Book.h @@ -21,7 +21,7 @@ #define RIPPLE_PROTOCOL_BOOK_H_INCLUDED #include -#include +#include #include namespace ripple { @@ -33,14 +33,14 @@ namespace ripple { class Book final : public CountedObject { public: - Issue in; - Issue out; + Asset in; + Asset out; Book() { } - Book(Issue const& in_, Issue const& out_) : in(in_), out(out_) + Book(Asset const& in_, Asset const& out_) : in(in_), out(out_) { } }; @@ -119,13 +119,80 @@ struct hash } }; +template <> +struct hash + : private boost::base_from_member, 0> +{ +private: + using id_hash_type = boost::base_from_member, 0>; + +public: + explicit hash() = default; + + using value_type = std::size_t; + using argument_type = ripple::MPTIssue; + + value_type + operator()(argument_type const& value) const + { + value_type result(id_hash_type::member(value.getMptID())); + return result; + } +}; + +template <> +struct hash + : private boost::base_from_member, 0>, + private boost::base_from_member, 1>, + private boost::base_from_member, 2> +{ +private: + using currency_hash_type = + boost::base_from_member, 0>; + using issuer_hash_type = + boost::base_from_member, 1>; + using mpt_hash_type = boost::base_from_member, 2>; + +public: + explicit hash() = default; + + using value_type = std::size_t; + using argument_type = ripple::Asset; + + value_type + operator()(argument_type const& issue) const + { + return std::visit( + [&](TIss const& issue_) { + if constexpr (std::is_same_v) + { + value_type result(currency_hash_type::member( + issue.get().currency)); + if (!isXRP(issue.get().currency)) + boost::hash_combine( + result, + issuer_hash_type::member( + issue.get().account)); + return result; + } + else if constexpr (std::is_same_v) + { + value_type result(mpt_hash_type::member( + issue.get().getMptID())); + return result; + } + }, + issue.value()); + } +}; + //------------------------------------------------------------------------------ template <> struct hash { private: - using hasher = std::hash; + using hasher = std::hash; hasher m_hasher; @@ -160,6 +227,22 @@ struct hash : std::hash // using Base::Base; // inherit ctors }; +template <> +struct hash : std::hash +{ + explicit hash() = default; + + using Base = std::hash; +}; + +template <> +struct hash : std::hash +{ + explicit hash() = default; + + using Base = std::hash; +}; + template <> struct hash : std::hash { diff --git a/include/xrpl/protocol/Concepts.h b/include/xrpl/protocol/Concepts.h new file mode 100644 index 00000000000..d7b8cd527f8 --- /dev/null +++ b/include/xrpl/protocol/Concepts.h @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +/* + 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_CONCEPTS_H_INCLUDED +#define RIPPLE_PROTOCOL_CONCEPTS_H_INCLUDED + +#include + +#include + +namespace ripple { + +class STAmount; +class Asset; +class Issue; +class MPTIssue; +class IOUAmount; +class XRPAmount; +class MPTAmount; + +// clang-format off +template +concept OfferAmount = ! +std::is_same_v; + +template +concept ValidIssueType = + std::is_same_v || std::is_same_v; + +template +concept AssetType = std::is_same_v || + std::is_convertible_v || std::is_convertible_v; + +template +concept StepAsset = ! +std::is_same_v; + +template +concept ValidPathAsset = + (std::is_same_v || std::is_same_v); + +template +concept ValidTaker = + ((std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) && + (!std::is_same_v || + !std::is_same_v)); +// clang-format on + +} // namespace ripple + +#endif // RIPPLE_PROTOCOL_CONCEPTS_H_INCLUDED diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 219230e69a1..b5a92659d7a 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 = 83; +static constexpr std::size_t numFeatures = 84; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 6b5024a1f03..9d34887f916 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -273,7 +273,7 @@ nft_sells(uint256 const& id) noexcept; /** AMM entry */ Keylet -amm(Issue const& issue1, Issue const& issue2) noexcept; +amm(Asset const& issue1, Asset const& issue2) noexcept; Keylet amm(uint256 const& amm) noexcept; diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index 06f55686caf..6e277a5435b 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -37,7 +37,9 @@ class MPTIssue public: MPTIssue() = default; - explicit MPTIssue(MPTID const& issuanceID); + MPTIssue(MPTID const& issuanceID); + + MPTIssue(std::uint32_t sequence, AccountID const& account); AccountID const& getIssuer() const; @@ -62,6 +64,9 @@ class MPTIssue { return false; } + + friend constexpr std::weak_ordering + operator<=>(MPTIssue const& lhs, MPTIssue const& rhs); }; constexpr bool @@ -76,6 +81,14 @@ operator!=(MPTIssue const& lhs, MPTIssue const& rhs) return !(lhs == rhs); } +constexpr std::weak_ordering +operator<=>(MPTIssue const& lhs, MPTIssue const& rhs) +{ + if (auto const c{lhs.mptID_ <=> rhs.mptID_}; c != 0) + return c; + return lhs.mptID_ <=> rhs.mptID_; +} + /** MPT is a non-native token. */ inline bool @@ -84,6 +97,29 @@ isXRP(MPTID const&) return false; } +inline AccountID const& +getMPTIssuer(MPTID const& mptid) +{ + AccountID const* accountId = reinterpret_cast( + mptid.data() + sizeof(std::uint32_t)); + return *accountId; +} + +inline MPTID +noMPT() +{ + static MPTIssue mpt{0, noAccount()}; + return mpt.getMptID(); +} + +template +void +hash_append(Hasher& h, MPTIssue const& r) +{ + using beast::hash_append; + hash_append(h, r.getMptID()); +} + Json::Value to_json(MPTIssue const& mptIssue); @@ -93,6 +129,19 @@ to_string(MPTIssue const& mptIssue); MPTIssue mptIssueFromJson(Json::Value const& jv); +std::ostream& +operator<<(std::ostream& os, MPTIssue const& x); + } // namespace ripple +namespace std { + +template <> +struct hash : ripple::MPTID::hasher +{ + explicit hash() = default; +}; + +} // namespace std + #endif // RIPPLE_PROTOCOL_MPTISSUE_H_INCLUDED diff --git a/include/xrpl/protocol/PathAsset.h b/include/xrpl/protocol/PathAsset.h new file mode 100644 index 00000000000..9a148ad2b17 --- /dev/null +++ b/include/xrpl/protocol/PathAsset.h @@ -0,0 +1,150 @@ +//------------------------------------------------------------------------------ +/* + 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_APP_PATHASSET_H_INCLUDED +#define RIPPLE_APP_PATHASSET_H_INCLUDED + +#include +#include + +namespace ripple { + +class PathAsset +{ +private: + std::variant easset_; + +public: + PathAsset() = default; + PathAsset(Asset const& asset); + PathAsset(Currency const& currency) : easset_(currency) + { + } + PathAsset(MPTID const& mpt) : easset_(mpt) + { + } + + template + constexpr bool + holds() const; + + constexpr bool + isXRP() const; + + template + T const& + get() const; + + constexpr std::variant const& + value() const; + + static PathAsset + toPathAsset(Asset const& asset); + + static std::optional + toPathAsset(std::optional const& asset); + + friend constexpr bool + operator==(PathAsset const& lhs, PathAsset const& rhs); +}; + +inline PathAsset::PathAsset(Asset const& asset) +{ + std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + easset_ = issue.currency; + else + easset_ = issue.getMptID(); + }, + asset.value()); +} + +template +constexpr bool +PathAsset::holds() const +{ + return std::holds_alternative(easset_); +} + +template +T const& +PathAsset::get() const +{ + if (!holds()) + Throw("PathAsset doesn't hold requested asset."); + return std::get(easset_); +} + +constexpr std::variant const& +PathAsset::value() const +{ + return easset_; +} + +constexpr bool +PathAsset::isXRP() const +{ + return holds() && get() == xrpCurrency(); +} + +constexpr bool +operator==(PathAsset const& lhs, PathAsset const& rhs) +{ + return std::visit( + []( + TLhs const& lhs_, TRhs const& rhs_) { + if constexpr (std::is_same_v) + return lhs_ == rhs_; + else + return false; + }, + lhs.value(), + rhs.value()); +} + +template +void +hash_append(Hasher& h, PathAsset const& pathAsset) +{ + std::visit( + [&](T const& e) { hash_append(h, e); }, pathAsset.value()); +} + +inline bool +isXRP(PathAsset const& asset) +{ + return asset.isXRP(); +} + +std::string +to_string(PathAsset const& asset); + +std::ostream& +operator<<(std::ostream& os, PathAsset const& x); + +bool +equalAssets(PathAsset const& asset1, Asset const& asset2); + +bool +equalAssets(Asset const& asset1, PathAsset const& asset2); + +} // namespace ripple + +#endif // RIPPLE_APP_PATHASSET_H_INCLUDED diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index f3dde91cf7e..eb1560317c3 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -35,11 +35,6 @@ namespace ripple { -template -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 // 2: Otherwise: diff --git a/include/xrpl/protocol/STIssue.h b/include/xrpl/protocol/STIssue.h index a0dfbd4faec..e97c2ab38ae 100644 --- a/include/xrpl/protocol/STIssue.h +++ b/include/xrpl/protocol/STIssue.h @@ -21,7 +21,7 @@ #define RIPPLE_PROTOCOL_STISSUE_H_INCLUDED #include -#include +#include #include #include #include @@ -31,31 +31,40 @@ namespace ripple { class STIssue final : public STBase, CountedObject { private: - Issue issue_{xrpIssue()}; + Asset asset_{xrpIssue()}; public: - using value_type = Issue; + using value_type = Asset; STIssue() = default; explicit STIssue(SerialIter& sit, SField const& name); - explicit STIssue(SField const& name, Issue const& issue); + template + explicit STIssue(SField const& name, A const& issue); explicit STIssue(SField const& name); - Issue const& - issue() const; + template + TIss const& + get() const; - Issue const& + template + bool + holds() const; + + value_type const& value() const noexcept; void - setIssue(Issue const& issue); + setIssue(Asset const& issue); SerializedTypeID getSType() const override; + std::string + getText() const override; + Json::Value getJson(JsonOptions) const override; void @@ -76,35 +85,54 @@ class STIssue final : public STBase, CountedObject friend class detail::STVar; }; +template +STIssue::STIssue(SField const& name, A const& asset) + : STBase{name}, asset_{asset} +{ + if (holds() && !isConsistent(asset_.get())) + Throw( + "Invalid asset: currency and account native mismatch"); +} + STIssue issueFromJson(SField const& name, Json::Value const& v); -inline Issue const& -STIssue::issue() const +template +bool +STIssue::holds() const +{ + return std::holds_alternative(asset_.value()); +} + +template +TIss const& +STIssue::get() const { - return issue_; + if (!holds(asset_)) + Throw("Asset doesn't hold the requested issue"); + return std::get(asset_); } -inline Issue const& +inline STIssue::value_type const& STIssue::value() const noexcept { - return issue_; + return asset_; } inline void -STIssue::setIssue(Issue const& issue) +STIssue::setIssue(Asset const& asset) { - if (isXRP(issue_.currency) != isXRP(issue_.account)) + if (holds() && !isConsistent(asset_.get())) Throw( - "invalid issue: currency and account native mismatch"); + "Invalid asset: currency and account native mismatch"); - issue_ = issue; + asset_ = asset; } inline bool operator==(STIssue const& lhs, STIssue const& rhs) { - return lhs.issue() == rhs.issue(); + return lhs.value() == rhs.value(); } inline bool @@ -116,19 +144,19 @@ operator!=(STIssue const& lhs, STIssue const& rhs) inline bool operator<(STIssue const& lhs, STIssue const& rhs) { - return lhs.issue() < rhs.issue(); + return lhs.value() < rhs.value(); } inline bool -operator==(STIssue const& lhs, Issue const& rhs) +operator==(STIssue const& lhs, Asset const& rhs) { - return lhs.issue() == rhs; + return lhs.value() == rhs; } inline bool -operator<(STIssue const& lhs, Issue const& rhs) +operator<(STIssue const& lhs, Asset const& rhs) { - return lhs.issue() < rhs; + return lhs.value() < rhs; } } // namespace ripple diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index a31058413f0..4b5b9fbc675 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -362,6 +362,8 @@ class STObject : public STBase, public CountedObject void setFieldH128(SField const& field, uint128 const&); void + setFieldH192(SField const& field, uint192 const&); + void setFieldH256(SField const& field, uint256 const&); void setFieldVL(SField const& field, Blob const&); diff --git a/include/xrpl/protocol/STPathSet.h b/include/xrpl/protocol/STPathSet.h index 754bb279238..a9cc9c889aa 100644 --- a/include/xrpl/protocol/STPathSet.h +++ b/include/xrpl/protocol/STPathSet.h @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include #include #include @@ -35,21 +37,26 @@ class STPathElement final : public CountedObject { unsigned int mType; AccountID mAccountID; - Currency mCurrencyID; + PathAsset mAssetID; AccountID mIssuerID; bool is_offer_; std::size_t hash_value_; public: + struct PathAssetTag + { + }; enum Type { typeNone = 0x00, typeAccount = 0x01, // Rippling through an account (vs taking an offer). typeCurrency = 0x10, // Currency follows. typeIssuer = 0x20, // Issuer follows. + typeMPT = 0x40, // MPT follows. typeBoundary = 0xFF, // Boundary between alternate paths. - typeAll = typeAccount | typeCurrency | typeIssuer, + typeAsset = typeCurrency | typeMPT, + typeAll = typeAccount | typeCurrency | typeIssuer | typeMPT, // Combination of all types. }; @@ -60,19 +67,37 @@ class STPathElement final : public CountedObject STPathElement( std::optional const& account, - std::optional const& currency, + std::optional const& asset, std::optional const& issuer); + STPathElement( + std::optional const& account, + std::optional const& asset, + std::optional const& issuer, + PathAssetTag); + + STPathElement( + AccountID const& account, + Asset const& asset, + AccountID const& issuer, + bool forceCurrency = false); + STPathElement( AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer, bool forceCurrency = false); STPathElement( unsigned int uType, AccountID const& account, - Currency const& currency, + Asset const& asset, + AccountID const& issuer); + + STPathElement( + unsigned int uType, + AccountID const& account, + PathAsset const& asset, AccountID const& issuer); auto @@ -90,6 +115,12 @@ class STPathElement final : public CountedObject bool hasCurrency() const; + bool + hasMPT() const; + + bool + hasAsset() const; + bool isNone() const; @@ -98,9 +129,15 @@ class STPathElement final : public CountedObject AccountID const& getAccountID() const; + PathAsset const& + getPathAsset() const; + Currency const& getCurrency() const; + MPTID const& + getMPTID() const; + AccountID const& getIssuerID() const; @@ -140,7 +177,13 @@ class STPath final : public CountedObject bool hasSeen( AccountID const& account, - Currency const& currency, + Asset const& asset, + AccountID const& issuer) const; + + bool + hasSeen( + AccountID const& account, + PathAsset const& asset, AccountID const& issuer) const; Json::Value getJson(JsonOptions) const; @@ -244,8 +287,21 @@ inline STPathElement::STPathElement() : mType(typeNone), is_offer_(true) inline STPathElement::STPathElement( std::optional const& account, - std::optional const& currency, + std::optional const& asset, std::optional const& issuer) + : STPathElement( + account, + PathAsset::toPathAsset(asset), + issuer, + PathAssetTag{}) +{ +} + +inline STPathElement::STPathElement( + std::optional const& account, + std::optional const& asset, + std::optional const& issuer, + PathAssetTag) : mType(typeNone) { if (!account) @@ -262,10 +318,10 @@ inline STPathElement::STPathElement( "ripple::STPathElement::STPathElement : account is set"); } - if (currency) + if (asset) { - mCurrencyID = *currency; - mType |= typeCurrency; + mAssetID = *asset; + mType |= mAssetID.holds() ? typeCurrency : typeMPT; } if (issuer) @@ -282,38 +338,68 @@ inline STPathElement::STPathElement( inline STPathElement::STPathElement( AccountID const& account, - Currency const& currency, + Asset const& asset, + AccountID const& issuer, + bool forceCurrency) + : STPathElement( + account, + PathAsset::toPathAsset(asset), + issuer, + forceCurrency) +{ +} + +inline STPathElement::STPathElement( + AccountID const& account, + PathAsset const& asset, AccountID const& issuer, bool forceCurrency) : mType(typeNone) , mAccountID(account) - , mCurrencyID(currency) + , mAssetID(asset) , mIssuerID(issuer) , is_offer_(isXRP(mAccountID)) { if (!is_offer_) mType |= typeAccount; - if (forceCurrency || !isXRP(currency)) + if (!asset.holds() && + (forceCurrency || !isXRP(mAssetID.get()))) mType |= typeCurrency; if (!isXRP(issuer)) mType |= typeIssuer; + if (asset.holds()) + mType |= typeMPT; + hash_value_ = get_hash(*this); } inline STPathElement::STPathElement( unsigned int uType, AccountID const& account, - Currency const& currency, + Asset const& asset, + AccountID const& issuer) + : STPathElement(uType, account, PathAsset::toPathAsset(asset), issuer) +{ +} + +inline STPathElement::STPathElement( + unsigned int uType, + AccountID const& account, + PathAsset const& asset, AccountID const& issuer) : mType(uType) , mAccountID(account) - , mCurrencyID(currency) + , mAssetID(asset) , mIssuerID(issuer) , is_offer_(isXRP(mAccountID)) { + if (!asset.holds()) + mType = mType & (~Type::typeMPT); + else if (mAssetID.holds() && isXRP(mAssetID.get())) + mType = mType & (~Type::typeCurrency); hash_value_ = get_hash(*this); } @@ -347,6 +433,18 @@ STPathElement::hasCurrency() const return getNodeType() & STPathElement::typeCurrency; } +inline bool +STPathElement::hasMPT() const +{ + return getNodeType() & STPathElement::typeMPT; +} + +inline bool +STPathElement::hasAsset() const +{ + return getNodeType() & STPathElement::typeAsset; +} + inline bool STPathElement::isNone() const { @@ -361,10 +459,22 @@ STPathElement::getAccountID() const return mAccountID; } +inline PathAsset const& +STPathElement::getPathAsset() const +{ + return mAssetID; +} + inline Currency const& STPathElement::getCurrency() const { - return mCurrencyID; + return mAssetID.get(); +} + +inline MPTID const& +STPathElement::getMPTID() const +{ + return mAssetID.get(); } inline AccountID const& @@ -378,7 +488,7 @@ STPathElement::operator==(const STPathElement& t) const { return (mType & typeAccount) == (t.mType & typeAccount) && hash_value_ == t.hash_value_ && mAccountID == t.mAccountID && - mCurrencyID == t.mCurrencyID && mIssuerID == t.mIssuerID; + mAssetID == t.mAssetID && mIssuerID == t.mIssuerID; } inline bool diff --git a/include/xrpl/protocol/STXChainBridge.h b/include/xrpl/protocol/STXChainBridge.h index 38db1912c70..813bcc44437 100644 --- a/include/xrpl/protocol/STXChainBridge.h +++ b/include/xrpl/protocol/STXChainBridge.h @@ -170,7 +170,7 @@ STXChainBridge::lockingChainDoor() const inline Issue const& STXChainBridge::lockingChainIssue() const { - return lockingChainIssue_.value(); + return lockingChainIssue_.value().get(); }; inline AccountID const& @@ -182,7 +182,7 @@ STXChainBridge::issuingChainDoor() const inline Issue const& STXChainBridge::issuingChainIssue() const { - return issuingChainIssue_.value(); + return issuingChainIssue_.value().get(); }; inline STXChainBridge::value_type const& diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 317e9c2c978..ff7514b9aa9 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -92,6 +92,7 @@ enum TEMcodes : TERUnderlyingType { temBAD_FEE, temBAD_ISSUER, temBAD_LIMIT, + temBAD_MPT, temBAD_OFFER, temBAD_PATH, temBAD_PATH_LOOP, diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 31fc90cef80..fb174906b49 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -29,6 +29,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(MPTokensV2, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Credentials, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(AMMClawback, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (AMMv1_2, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 0cb1ec3416a..5222813307a 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -166,8 +166,10 @@ LEDGER_ENTRY(ltDIR_NODE, 0x0064, DirectoryNode, ({ {sfOwner, soeOPTIONAL}, // for owner directories {sfTakerPaysCurrency, soeOPTIONAL}, // order book directories {sfTakerPaysIssuer, soeOPTIONAL}, // order book directories + {sfTakerPaysMPT, soeOPTIONAL}, // order book directories {sfTakerGetsCurrency, soeOPTIONAL}, // order book directories {sfTakerGetsIssuer, soeOPTIONAL}, // order book directories + {sfTakerGetsMPT, soeOPTIONAL}, // order book directories {sfExchangeRate, soeOPTIONAL}, // order book directories {sfIndexes, soeREQUIRED}, {sfRootIndex, soeREQUIRED}, diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 8384025ee3b..07e374588ad 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -154,6 +154,8 @@ TYPED_SFIELD(sfTakerGetsIssuer, UINT160, 4) // 192-bit (common) TYPED_SFIELD(sfMPTokenIssuanceID, UINT192, 1) +TYPED_SFIELD(sfTakerPaysMPT, UINT192, 2) +TYPED_SFIELD(sfTakerGetsMPT, UINT192, 3) // 256-bit (common) TYPED_SFIELD(sfLedgerHash, UINT256, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 4f4c8f12595..aa1100f8f01 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -89,8 +89,8 @@ TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, ({ /** This transaction type creates an offer to trade one asset for another. */ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, ({ - {sfTakerPays, soeREQUIRED}, - {sfTakerGets, soeREQUIRED}, + {sfTakerPays, soeREQUIRED, soeMPTSupported}, + {sfTakerGets, soeREQUIRED, soeMPTSupported}, {sfExpiration, soeOPTIONAL}, {sfOfferSequence, soeOPTIONAL}, })) @@ -147,7 +147,7 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, ({ /** This transaction type creates a new check. */ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({ {sfDestination, soeREQUIRED}, - {sfSendMax, soeREQUIRED}, + {sfSendMax, soeREQUIRED, soeMPTSupported}, {sfExpiration, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, {sfInvoiceID, soeOPTIONAL}, @@ -156,8 +156,8 @@ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({ /** This transaction type cashes an existing check. */ TRANSACTION(ttCHECK_CASH, 17, CheckCash, ({ {sfCheckID, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, - {sfDeliverMin, soeOPTIONAL}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, + {sfDeliverMin, soeOPTIONAL, soeMPTSupported}, })) /** This transaction type cancels an existing check. */ @@ -238,13 +238,13 @@ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, ({ {sfHolder, soeREQUIRED}, {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, })) /** This transaction type creates an AMM instance */ TRANSACTION(ttAMM_CREATE, 35, AMMCreate, ({ - {sfAmount, soeREQUIRED}, - {sfAmount2, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, + {sfAmount2, soeREQUIRED, soeMPTSupported}, {sfTradingFee, soeREQUIRED}, })) @@ -252,8 +252,8 @@ TRANSACTION(ttAMM_CREATE, 35, AMMCreate, ({ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, - {sfAmount2, soeOPTIONAL}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, + {sfAmount2, soeOPTIONAL, soeMPTSupported}, {sfEPrice, soeOPTIONAL}, {sfLPTokenOut, soeOPTIONAL}, {sfTradingFee, soeOPTIONAL}, @@ -263,8 +263,8 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, ({ TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, - {sfAmount2, soeOPTIONAL}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, + {sfAmount2, soeOPTIONAL, soeMPTSupported}, {sfEPrice, soeOPTIONAL}, {sfLPTokenIn, soeOPTIONAL}, })) diff --git a/src/libxrpl/basics/MPTAmount.cpp b/src/libxrpl/basics/MPTAmount.cpp index 0481da67711..6c9a50e4730 100644 --- a/src/libxrpl/basics/MPTAmount.cpp +++ b/src/libxrpl/basics/MPTAmount.cpp @@ -59,6 +59,23 @@ 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/AMMCore.cpp b/src/libxrpl/protocol/AMMCore.cpp index 6661ab69a74..7879dbe0dc4 100644 --- a/src/libxrpl/protocol/AMMCore.cpp +++ b/src/libxrpl/protocol/AMMCore.cpp @@ -39,12 +39,23 @@ ammAccountID( } Currency -ammLPTCurrency(Currency const& cur1, Currency const& cur2) +ammLPTCurrency(Asset const& issue1, Asset const& issue2) { // AMM LPToken is 0x03 plus 19 bytes of the hash std::int32_t constexpr AMMCurrencyCode = 0x03; - auto const [minC, maxC] = std::minmax(cur1, cur2); - auto const hash = sha512Half(minC, maxC); + auto const [minI, maxI] = std::minmax(issue1, issue2); + uint256 const hash = std::visit( + [](auto&& issue1_, auto&& issue2_) { + auto fromIss = [](T const& iss) { + if constexpr (std::is_same_v) + return iss.currency; + if constexpr (std::is_same_v) + return iss.getMptID(); + }; + return sha512Half(fromIss(issue1_), fromIss(issue2_)); + }, + minI.value(), + maxI.value()); Currency currency; *currency.begin() = AMMCurrencyCode; std::copy( @@ -54,21 +65,24 @@ ammLPTCurrency(Currency const& cur1, Currency const& cur2) Issue ammLPTIssue( - Currency const& cur1, - Currency const& cur2, + Asset const& issue1, + Asset const& issue2, AccountID const& ammAccountID) { - return Issue(ammLPTCurrency(cur1, cur2), ammAccountID); + return Issue(ammLPTCurrency(issue1, issue2), ammAccountID); } NotTEC invalidAMMAsset( - Issue const& issue, - std::optional> const& pair) + Asset const& issue, + std::optional> const& pair) { - if (badCurrency() == issue.currency) + if (issue.holds() && + issue.get().getIssuer() == beast::zero) + return temBAD_MPT; + if (issue.holds() && badCurrency() == issue.get().currency) return temBAD_CURRENCY; - if (isXRP(issue) && issue.account.isNonZero()) + if (isXRP(issue) && issue.getIssuer().isNonZero()) return temBAD_ISSUER; if (pair && issue != pair->first && issue != pair->second) return temBAD_AMM_TOKENS; @@ -77,9 +91,9 @@ invalidAMMAsset( NotTEC invalidAMMAssetPair( - Issue const& issue1, - Issue const& issue2, - std::optional> const& pair) + Asset const& issue1, + Asset const& issue2, + std::optional> const& pair) { if (issue1 == issue2) return temBAD_AMM_TOKENS; @@ -93,10 +107,10 @@ invalidAMMAssetPair( NotTEC invalidAMMAmount( STAmount const& amount, - std::optional> const& pair, + std::optional> const& pair, bool validZero) { - if (auto const res = invalidAMMAsset(amount.issue(), pair)) + if (auto const res = invalidAMMAsset(amount.asset(), pair)) return res; if (amount < beast::zero || (!validZero && amount == beast::zero)) return temBAD_AMOUNT; diff --git a/src/libxrpl/protocol/Asset.cpp b/src/libxrpl/protocol/Asset.cpp index 5a496352840..4162eb70c26 100644 --- a/src/libxrpl/protocol/Asset.cpp +++ b/src/libxrpl/protocol/Asset.cpp @@ -77,4 +77,14 @@ to_json(Asset const& asset) [&](auto const& issue) { return to_json(issue); }, asset.value()); } +std::ostream& +operator<<(std::ostream& os, Asset const& x) +{ + if (x.holds()) + os << x.get(); + else + os << x.get(); + return os; +} + } // namespace ripple diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 1519673ad4a..c7be5a30951 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -97,12 +98,37 @@ getBookBase(Book const& book) { ASSERT(isConsistent(book), "ripple::getBookBase : input is consistent"); - auto const index = indexHash( - LedgerNameSpace::BOOK_DIR, - book.in.currency, - book.out.currency, - book.in.account, - book.out.account); + auto const index = std::visit( + [&]( + TIn const& in, TOut const& out) { + if constexpr ( + std::is_same_v && std::is_same_v) + return indexHash( + LedgerNameSpace::BOOK_DIR, + in.currency, + out.currency, + in.account, + out.account); + else if constexpr ( + std::is_same_v && std::is_same_v) + return indexHash( + LedgerNameSpace::BOOK_DIR, + in.currency, + out.getMptID(), + in.account); + else if constexpr ( + std::is_same_v && std::is_same_v) + return indexHash( + LedgerNameSpace::BOOK_DIR, + in.getMptID(), + out.currency, + out.account); + else + return indexHash( + LedgerNameSpace::BOOK_DIR, in.getMptID(), out.getMptID()); + }, + book.in.value(), + book.out.value()); // Return with quality 0. auto k = keylet::quality({ltDIR_NODE, index}, 0); @@ -414,15 +440,44 @@ nft_sells(uint256 const& id) noexcept } Keylet -amm(Issue const& issue1, Issue const& issue2) noexcept -{ - auto const& [minI, maxI] = std::minmax(issue1, issue2); - return amm(indexHash( - LedgerNameSpace::AMM, - minI.account, - minI.currency, - maxI.account, - maxI.currency)); +amm(Asset const& issue1, Asset const& issue2) noexcept +{ + auto const& [minA, maxA] = std::minmax(issue1, issue2); + return std::visit( + []( + TIss1 const& issue1_, TIss2 const& issue2_) { + if constexpr ( + std::is_same_v && std::is_same_v) + return amm(indexHash( + LedgerNameSpace::AMM, + issue1_.account, + issue1_.currency, + issue2_.account, + issue2_.currency)); + else if constexpr ( + std::is_same_v && std::is_same_v) + return amm(indexHash( + LedgerNameSpace::AMM, + issue1_.account, + issue1_.currency, + issue2_.getMptID())); + else if constexpr ( + std::is_same_v && std::is_same_v) + return amm(indexHash( + LedgerNameSpace::AMM, + issue1_.getMptID(), + issue2_.account, + issue2_.currency)); + else if constexpr ( + std::is_same_v && + std::is_same_v) + return amm(indexHash( + LedgerNameSpace::AMM, + issue1_.getMptID(), + issue2_.getMptID())); + }, + minA.value(), + maxA.value()); } Keylet diff --git a/src/libxrpl/protocol/MPTIssue.cpp b/src/libxrpl/protocol/MPTIssue.cpp index 38022a0ed3a..14f2f5fe796 100644 --- a/src/libxrpl/protocol/MPTIssue.cpp +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -27,6 +27,11 @@ MPTIssue::MPTIssue(MPTID const& issuanceID) : mptID_(issuanceID) { } +MPTIssue::MPTIssue(std::uint32_t sequence, AccountID const& account) + : MPTIssue(ripple::makeMptID(sequence, account)) +{ +} + AccountID const& MPTIssue::getIssuer() const { @@ -104,4 +109,11 @@ mptIssueFromJson(Json::Value const& v) return MPTIssue{id}; } +std::ostream& +operator<<(std::ostream& os, MPTIssue const& x) +{ + os << to_string(x); + return os; +} + } // namespace ripple diff --git a/src/libxrpl/protocol/PathAsset.cpp b/src/libxrpl/protocol/PathAsset.cpp new file mode 100644 index 00000000000..47966a1a3e5 --- /dev/null +++ b/src/libxrpl/protocol/PathAsset.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 +#include + +namespace ripple { + +PathAsset +PathAsset::toPathAsset(const Asset& asset) +{ + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + return PathAsset{issue.currency}; + else + return PathAsset{issue.getMptID()}; + }, + asset.value()); +} + +std::optional +PathAsset::toPathAsset(std::optional const& asset) +{ + if (asset) + return toPathAsset(*asset); + return std::nullopt; +} + +std::string +to_string(PathAsset const& asset) +{ + return std::visit( + [&](auto const& issue) { return to_string(issue); }, asset.value()); +} + +std::ostream& +operator<<(std::ostream& os, PathAsset const& x) +{ + os << to_string(x); + return os; +} + +bool +equalAssets(PathAsset const& asset1, Asset const& asset2) +{ + return std::visit( + [&]( + TPa const& element, TIss const& issue) { + if constexpr ( + std::is_same_v && std::is_same_v) + return element == issue.currency; + else if constexpr ( + std::is_same_v && std::is_same_v) + return element == issue.getMptID(); + else + return false; + }, + asset1.value(), + asset2.value()); +} + +bool +equalAssets(Asset const& asset1, PathAsset const& asset2) +{ + return equalAssets(asset2, asset1); +} + +} // namespace ripple diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index c33b2b40aa3..94c6f31f973 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -973,7 +973,7 @@ amountFromJson(SField const& name, Json::Value const& v) if (isMPT) { // sequence (32 bits) + account (160 bits) - uint192 u; + MPTID u; if (!u.parseHex(currencyOrMPTID.asString())) Throw("invalid MPTokenIssuanceID"); asset = u; diff --git a/src/libxrpl/protocol/STIssue.cpp b/src/libxrpl/protocol/STIssue.cpp index 00fe86b3287..4f07f130f24 100644 --- a/src/libxrpl/protocol/STIssue.cpp +++ b/src/libxrpl/protocol/STIssue.cpp @@ -40,23 +40,44 @@ STIssue::STIssue(SField const& name) : STBase{name} STIssue::STIssue(SerialIter& sit, SField const& name) : STBase{name} { - issue_.currency = sit.get160(); - if (!isXRP(issue_.currency)) - issue_.account = sit.get160(); - else - issue_.account = xrpAccount(); - - if (isXRP(issue_.currency) != isXRP(issue_.account)) - Throw( - "invalid issue: currency and account native mismatch"); -} + auto const currencyOrAccount = sit.get160(); -STIssue::STIssue(SField const& name, Issue const& issue) - : STBase{name}, issue_{issue} -{ - if (isXRP(issue_.currency) != isXRP(issue_.account)) - Throw( - "invalid issue: currency and account native mismatch"); + if (isXRP(static_cast(currencyOrAccount))) + { + asset_ = xrpIssue(); + } + // Check if MPT + else + { + // MPT is serialized as: + // - 160 bits MPT issuer account + // - 160 bits black hole account + // - 32 bits sequence + AccountID account = static_cast(sit.get160()); + // MPT + if (noAccount() == account) + { + MPTID mptID; + std::uint32_t sequence = sit.get32(); + memcpy(mptID.data(), &sequence, sizeof(sequence)); + memcpy( + mptID.data() + sizeof(sequence), + account.data(), + sizeof(account)); + MPTIssue issue{mptID}; + asset_ = issue; + } + else + { + Issue issue; + issue.currency = currencyOrAccount; + issue.account = account; + if (!isConsistent(issue)) + Throw( + "invalid issue: currency and account native mismatch"); + asset_ = issue; + } + } } SerializedTypeID @@ -65,18 +86,40 @@ STIssue::getSType() const return STI_ISSUE; } +std::string +STIssue::getText() const +{ + return asset_.getText(); +} + Json::Value STIssue::getJson(JsonOptions) const { - return to_json(issue_); + Json::Value jv; + asset_.setJson(jv); + return jv; } void STIssue::add(Serializer& s) const { - s.addBitString(issue_.currency); - if (!isXRP(issue_.currency)) - s.addBitString(issue_.account); + if (holds()) + { + s.addBitString(asset_.get().currency); + if (!isXRP(asset_.get().currency)) + s.addBitString(asset_.get().account); + } + else + { + s.addBitString(asset_.get().getIssuer()); + s.addBitString(noAccount()); + std::uint32_t sequence; + memcpy( + &sequence, + asset_.get().getMptID().data(), + sizeof(sequence)); + s.add32(sequence); + } } bool @@ -89,7 +132,7 @@ STIssue::isEquivalent(const STBase& t) const bool STIssue::isDefault() const { - return issue_ == xrpIssue(); + return holds() && asset_.get() == xrpIssue(); } STBase* diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 520dc53b0de..226686aaadb 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -725,6 +725,12 @@ STObject::setFieldH128(SField const& field, uint128 const& v) setFieldUsingSetValue(field, v); } +void +STObject::setFieldH192(SField const& field, uint192 const& v) +{ + setFieldUsingSetValue(field, v); +} + void STObject::setFieldH256(SField const& field, uint256 const& v) { diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index 7d08993a8ba..3c1bf09dee0 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -632,7 +632,7 @@ parseLeaf( json_name + "." + ss.str()); // each element in this path has some combination of - // account, currency, or issuer + // account, asset, or issuer Json::Value pathEl = value[i][j]; @@ -642,20 +642,32 @@ parseLeaf( return ret; } - Json::Value const& account = pathEl["account"]; - Json::Value const& currency = pathEl["currency"]; - Json::Value const& issuer = pathEl["issuer"]; + if (pathEl.isMember(jss::currency) && + pathEl.isMember(jss::mpt_issuance_id)) + { + error = RPC::make_error( + rpcINVALID_PARAMS, "Invalid Asset."); + return ret; + } + + bool const isMPT = + pathEl.isMember(jss::mpt_issuance_id); + auto const assetName = + isMPT ? jss::mpt_issuance_id : jss::currency; + Json::Value const& account = pathEl[jss::account]; + Json::Value const& asset = pathEl[assetName]; + Json::Value const& issuer = pathEl[jss::issuer]; bool hasCurrency = false; AccountID uAccount, uIssuer; - Currency uCurrency; + PathAsset uAsset; if (account) { // human account id if (!account.isString()) { - error = - string_expected(element_name, "account"); + error = string_expected( + element_name, jss::account.c_str()); return ret; } @@ -667,35 +679,57 @@ parseLeaf( parseBase58(account.asString()); if (!a) { - error = - invalid_data(element_name, "account"); + error = invalid_data( + element_name, jss::account.c_str()); return ret; } uAccount = *a; } } - if (currency) + if (asset) { - // human currency - if (!currency.isString()) + // human asset + if (!asset.isString()) { - error = - string_expected(element_name, "currency"); + error = string_expected( + element_name, assetName.c_str()); return ret; } hasCurrency = true; - if (!uCurrency.parseHex(currency.asString())) + if (isMPT) { - if (!to_currency( - uCurrency, currency.asString())) + MPTID u; + if (!u.parseHex(asset.asString())) { - error = - invalid_data(element_name, "currency"); + error = invalid_data( + element_name, assetName.c_str()); return ret; } + uAsset = u; + if (getMPTIssuer(u) == beast::zero) + { + error = invalid_data( + element_name, jss::account.c_str()); + return ret; + } + } + else + { + Currency currency; + if (!currency.parseHex(asset.asString())) + { + if (!to_currency( + currency, asset.asString())) + { + error = invalid_data( + element_name, assetName.c_str()); + return ret; + } + } + uAsset = currency; } } @@ -704,7 +738,8 @@ parseLeaf( // human account id if (!issuer.isString()) { - error = string_expected(element_name, "issuer"); + error = string_expected( + element_name, jss::issuer.c_str()); return ret; } @@ -714,16 +749,15 @@ parseLeaf( parseBase58(issuer.asString()); if (!a) { - error = - invalid_data(element_name, "issuer"); + error = invalid_data( + element_name, jss::issuer.c_str()); return ret; } uIssuer = *a; } } - p.emplace_back( - uAccount, uCurrency, uIssuer, hasCurrency); + p.emplace_back(uAccount, uAsset, uIssuer, hasCurrency); } tail.push_back(p); diff --git a/src/libxrpl/protocol/STPathSet.cpp b/src/libxrpl/protocol/STPathSet.cpp index 3409299d7cb..5be549a2091 100644 --- a/src/libxrpl/protocol/STPathSet.cpp +++ b/src/libxrpl/protocol/STPathSet.cpp @@ -40,8 +40,18 @@ STPathElement::get_hash(STPathElement const& element) for (auto const x : element.getAccountID()) hash_account += (hash_account * 257) ^ x; - for (auto const x : element.getCurrency()) - hash_currency += (hash_currency * 509) ^ x; + // Check pathAsset type instead of element's mType + // In some cases mType might be account but the asset + // is still set to either MPT or currency (see Pathfinder::addLink()) + if (element.getPathAsset().holds()) + { + hash_currency += beast::uhash<>{}(element.getPathAsset().get()); + } + else + { + for (auto const x : element.getPathAsset().get()) + hash_currency += (hash_currency * 509) ^ x; + } for (auto const x : element.getIssuerID()) hash_issuer += (hash_issuer * 911) ^ x; @@ -82,21 +92,28 @@ STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name) auto hasAccount = iType & STPathElement::typeAccount; auto hasCurrency = iType & STPathElement::typeCurrency; auto hasIssuer = iType & STPathElement::typeIssuer; + auto hasMPT = iType & STPathElement::typeMPT; AccountID account; - Currency currency; + PathAsset asset{}; AccountID issuer; if (hasAccount) account = sit.get160(); + ASSERT( + !(hasCurrency && hasMPT), + "ripple::STPathSet::STPathSet : not has Currency and MPT"); if (hasCurrency) - currency = sit.get160(); + asset = static_cast(sit.get160()); + + if (hasMPT) + asset = sit.get192(); if (hasIssuer) issuer = sit.get160(); - path.emplace_back(account, currency, issuer, hasCurrency); + path.emplace_back(account, asset, issuer, hasCurrency); } } } @@ -150,12 +167,21 @@ STPathSet::isDefault() const bool STPath::hasSeen( AccountID const& account, - Currency const& currency, + Asset const& asset, + AccountID const& issuer) const +{ + return hasSeen(account, PathAsset::toPathAsset(asset), issuer); +} + +bool +STPath::hasSeen( + AccountID const& account, + PathAsset const& asset, AccountID const& issuer) const { for (auto& p : mPath) { - if (p.getAccountID() == account && p.getCurrency() == currency && + if (p.getAccountID() == account && p.getPathAsset() == asset && p.getIssuerID() == issuer) return true; } @@ -178,9 +204,16 @@ STPath::getJson(JsonOptions) const if (iType & STPathElement::typeAccount) elem[jss::account] = to_string(it.getAccountID()); + ASSERT( + !(iType & STPathElement::typeCurrency && + iType & STPathElement::typeMPT), + "ripple::STPath::getJson : not type Currency and MPT"); if (iType & STPathElement::typeCurrency) elem[jss::currency] = to_string(it.getCurrency()); + if (iType & STPathElement::typeMPT) + elem[jss::mpt_issuance_id] = to_string(it.getMPTID()); + if (iType & STPathElement::typeIssuer) elem[jss::issuer] = to_string(it.getIssuerID()); @@ -229,6 +262,9 @@ STPathSet::add(Serializer& s) const if (iType & STPathElement::typeAccount) s.addBitString(speElement.getAccountID()); + if (iType & STPathElement::typeMPT) + s.addBitString(speElement.getMPTID()); + if (iType & STPathElement::typeCurrency) s.addBitString(speElement.getCurrency()); diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 815b27c0018..ee7db39424c 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -167,6 +167,7 @@ transResults() MAKE_ERROR(temBAD_FEE, "Invalid fee, negative or not XRP."), MAKE_ERROR(temBAD_ISSUER, "Malformed: Bad issuer."), MAKE_ERROR(temBAD_LIMIT, "Limits must be non-negative."), + MAKE_ERROR(temBAD_MPT, "Malformed: Bad MPT."), MAKE_ERROR(temBAD_OFFER, "Malformed: Bad offer."), MAKE_ERROR(temBAD_PATH, "Malformed: Bad path."), MAKE_ERROR(temBAD_PATH_LOOP, "Malformed: Loop in path."), diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index f1e81132c5e..c5877dd13cc 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -6234,7 +6234,7 @@ struct AMM_test : public jtx::AMMTest takerGets}; } auto const takerPays = toAmount( - getIssue(poolIn), Number{1, -10} * poolIn); + getAsset(poolIn), Number{1, -10} * poolIn); return Amounts{ takerPays, swapAssetIn( diff --git a/src/test/app/CrossingLimits_test.cpp b/src/test/app/CrossingLimits_test.cpp index 6f6a7eb3e7f..d075d9aebfd 100644 --- a/src/test/app/CrossingLimits_test.cpp +++ b/src/test/app/CrossingLimits_test.cpp @@ -76,10 +76,8 @@ class CrossingLimits_test : public beast::unit_test::suite auto const gw = Account("gateway"); auto const USD = gw["USD"]; - // The number of allowed offers to cross is different between - // Taker and FlowCross. Taker allows 850 and FlowCross allows 1000. - // Accommodate that difference in the test. - int const maxConsumed = features[featureFlowCross] ? 1000 : 850; + // FlowCross allows 1000. + int const maxConsumed = 1000; env.fund(XRP(100000000), gw, "alice", "bob", "carol"); int const bobsOfferCount = maxConsumed + 150; @@ -118,11 +116,8 @@ class CrossingLimits_test : public beast::unit_test::suite env.fund(XRP(100000000), gw, "alice", "bob", "carol", "dan", "evita"); - // The number of offers allowed to cross is different between - // Taker and FlowCross. Taker allows 850 and FlowCross allows 1000. - // Accommodate that difference in the test. - bool const isFlowCross{features[featureFlowCross]}; - int const maxConsumed = isFlowCross ? 1000 : 850; + // FlowCross allows 1000. + int const maxConsumed = 1000; int const evitasOfferCount{maxConsumed + 49}; env.trust(USD(1000), "alice"); @@ -132,14 +127,11 @@ class CrossingLimits_test : public beast::unit_test::suite env.trust(USD(evitasOfferCount + 1), "evita"); env(pay(gw, "evita", USD(evitasOfferCount + 1))); - // Taker and FlowCross have another difference we must accommodate. - // Taker allows a total of 1000 unfunded offers to be consumed - // beyond the 850 offers it can take. FlowCross draws no such - // distinction; its limit is 1000 funded or unfunded. + // FlowCross limit is 1000 funded or unfunded. // // Give carol an extra 150 (unfunded) offers when we're using Taker // to accommodate that difference. - int const carolsOfferCount{isFlowCross ? 700 : 850}; + int const carolsOfferCount{700}; n_offers(env, 400, "alice", XRP(1), USD(1)); n_offers(env, carolsOfferCount, "carol", XRP(1), USD(1)); n_offers(env, evitasOfferCount, "evita", XRP(1), USD(1)); @@ -454,21 +446,10 @@ class CrossingLimits_test : public beast::unit_test::suite void testAutoBridgedLimits(FeatureBitset features) { - // Taker and FlowCross are too different in the way they handle - // autobridging to make one test suit both approaches. - // - // o Taker alternates between books, completing one full increment - // before returning to make another pass. - // // o FlowCross extracts as much as possible in one book at one Quality // before proceeding to the other book. This reduces the number of // times we change books. - // - // So the tests for the two forms of autobridging are separate. - if (features[featureFlowCross]) - testAutoBridgedLimitsFlowCross(features); - else - testAutoBridgedLimitsTaker(features); + testAutoBridgedLimitsFlowCross(features); } void @@ -521,11 +502,10 @@ class CrossingLimits_test : public beast::unit_test::suite n_offers(env, 998, alice, XRP(0.96), USD(1)); n_offers(env, 998, alice, XRP(0.95), USD(1)); - bool const withFlowCross = features[featureFlowCross]; bool const withSortStrands = features[featureFlowSortStrands]; auto const expectedTER = [&]() -> TER { - if (withFlowCross && !withSortStrands) + if (!withSortStrands) return TER{tecOVERSIZE}; return tesSUCCESS; }(); @@ -534,8 +514,6 @@ class CrossingLimits_test : public beast::unit_test::suite env.close(); auto const expectedUSD = [&] { - if (!withFlowCross) - return USD(850); if (!withSortStrands) return USD(0); return USD(1996); @@ -558,7 +536,6 @@ class CrossingLimits_test : public beast::unit_test::suite auto const sa = supported_amendments(); testAll(sa); testAll(sa - featureFlowSortStrands); - testAll(sa - featureFlowCross - featureFlowSortStrands); } }; diff --git a/src/test/app/Discrepancy_test.cpp b/src/test/app/Discrepancy_test.cpp index 1eaa1ad86dd..16e9e1c8951 100644 --- a/src/test/app/Discrepancy_test.cpp +++ b/src/test/app/Discrepancy_test.cpp @@ -146,7 +146,6 @@ class Discrepancy_test : public beast::unit_test::suite { using namespace test::jtx; auto const sa = supported_amendments(); - testXRPDiscrepancy(sa - featureFlowCross); testXRPDiscrepancy(sa); } }; diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index 4d1397eab83..77d0a874533 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -1435,7 +1435,6 @@ struct Flow_test : public beast::unit_test::suite using namespace jtx; auto const sa = supported_amendments(); - testWithFeats(sa - featureFlowCross); testWithFeats(sa); testEmptyStrand(sa); } @@ -1448,11 +1447,8 @@ struct Flow_manual_test : public Flow_test { using namespace jtx; auto const all = supported_amendments(); - FeatureBitset const flowCross{featureFlowCross}; FeatureBitset const f1513{fix1513}; - testWithFeats(all - flowCross - f1513); - testWithFeats(all - flowCross); testWithFeats(all - f1513); testWithFeats(all); diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index 0c54f0e1f39..1ea9f1b2530 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -518,7 +518,6 @@ class Freeze_test : public beast::unit_test::suite }; using namespace test::jtx; auto const sa = supported_amendments(); - testAll(sa - featureFlowCross); testAll(sa); } }; diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 796a3f14c88..afad56f1ee3 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -18,6 +18,10 @@ //============================================================================== #include +#include +#include +#include +#include #include #include #include @@ -689,10 +693,12 @@ class MPToken_test : public beast::unit_test::suite mptAlice.authorize({.account = bob}); - for (auto flags : {tfNoRippleDirect, tfLimitQuality}) - env(pay(alice, bob, MPT(10)), - txflags(flags), - ter(temINVALID_FLAG)); + auto err = !features[featureMPTokensV2] ? ter(temINVALID_FLAG) + : ter(temRIPPLE_EMPTY); + env(pay(alice, bob, MPT(10)), txflags(tfNoRippleDirect), err); + err = !features[featureMPTokensV2] ? ter(temINVALID_FLAG) + : ter(tesSUCCESS); + env(pay(alice, bob, MPT(10)), txflags(tfLimitQuality), err); } // Invalid combination of send, sendMax, deliverMin, paths @@ -703,27 +709,28 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {carol}}); - mptAlice.create({.ownerCount = 1, .holderCount = 0}); + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); 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)); + auto const MPTokensV2 = features[featureMPTokensV2]; + auto err = !MPTokensV2 ? ter(temMALFORMED) : ter(tecPATH_PARTIAL); + env(pay(alice, carol, MPT(100)), sendmax(XRP(100)), err); env(pay(alice, carol, MPT(100)), delivermin(XRP(100)), ter(temBAD_AMOUNT)); // 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)); + err = !MPTokensV2 ? ter(temMALFORMED) : ter(tecPATH_DRY); + env(pay(alice, carol, USD(100)), sendmax(MPT(100)), err); + err = !MPTokensV2 ? ter(temMALFORMED) : ter(tecPATH_PARTIAL); + env(pay(alice, carol, XRP(100)), sendmax(MPT(100)), err); env(pay(alice, carol, USD(100)), delivermin(MPT(100)), ter(temBAD_AMOUNT)); @@ -733,16 +740,16 @@ class MPToken_test : public beast::unit_test::suite // 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)); + err = !MPTokensV2 ? ter(temMALFORMED) : ter(tecOBJECT_NOT_FOUND); + env(pay(alice, carol, MPT1(100)), sendmax(MPT(100)), err); + // "paths" is invalid in V1 + err = !MPTokensV2 ? ter(temDISABLED) : ter(tesSUCCESS); + env(pay(alice, carol, MPT(100)), path(~USD), err); } // build_path is invalid if MPT { - Env env{*this, features}; + Env env{*this, features - featureMPTokensV2}; Account const alice("alice"); Account const carol("carol"); @@ -1022,10 +1029,13 @@ class MPToken_test : public beast::unit_test::suite 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) + // 82 to carol, 8 to issuer (90 / 1.1 ~ 81.81 (rounded to nearest in + // v1) = 82) BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 690)); - BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 282)); + // In V2 the payments are executed via the payment engine and + // the rounding results in a higher quality trade + BEAST_EXPECT(mptAlice.checkMPTokenAmount( + carol, !features[featureMPTokensV2] ? 282 : 281)); } // Insufficient SendMax with no transfer fee @@ -1169,6 +1179,7 @@ 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]; @@ -1250,7 +1261,10 @@ class MPToken_test : public beast::unit_test::suite env.fund(XRP(1'000), alice, bob); STAmount const mpt{MPTID{0}, 100}; - env(pay(alice, bob, mpt), ter(tecOBJECT_NOT_FOUND)); + auto const err = !features[featureMPTokensV2] + ? ter(tecOBJECT_NOT_FOUND) + : ter(temBAD_PATH); + env(pay(alice, bob, mpt), err); } // Issuer fails trying to send to an account, which doesn't own MPT for @@ -1317,7 +1331,7 @@ class MPToken_test : public beast::unit_test::suite } void - testDepositPreauth() + testDepositPreauth(FeatureBitset features) { testcase("DepositPreauth"); @@ -1330,7 +1344,7 @@ class MPToken_test : public beast::unit_test::suite const char credType[] = "abcde"; { - Env env(*this); + Env env(*this, features); env.fund(XRP(50000), diana, dpIssuer); env.close(); @@ -1405,7 +1419,7 @@ class MPToken_test : public beast::unit_test::suite testcase("DepositPreauth disabled featureCredentials"); { - Env env(*this, supported_amendments() - featureCredentials); + Env env(*this, features - featureCredentials); std::string const credIdx = "D007AE4B6E1274B4AF872588267B810C2F82716726351D1C7D38D3E5499FC6" @@ -1532,23 +1546,6 @@ class MPToken_test : public beast::unit_test::suite }; // 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, field.fieldName); - }; - ammCreate(sfAmount); - ammCreate(sfAmount2); // AMMDeposit auto ammDeposit = [&](SField const& field) { Json::Value jv; @@ -1561,10 +1558,7 @@ class MPToken_test : public beast::unit_test::suite test(jv, field.fieldName); }; for (SField const& field : - {std::ref(sfAmount), - std::ref(sfAmount2), - std::ref(sfEPrice), - std::ref(sfLPTokenOut)}) + {std::ref(sfEPrice), std::ref(sfLPTokenOut)}) ammDeposit(field); // AMMWithdraw auto ammWithdraw = [&](SField const& field) { @@ -1577,11 +1571,8 @@ class MPToken_test : public beast::unit_test::suite jv[field.fieldName] = mpt.getJson(JsonOptions::none); test(jv, field.fieldName); }; - ammWithdraw(sfAmount); for (SField const& field : - {std::ref(sfAmount2), - std::ref(sfEPrice), - std::ref(sfLPTokenIn)}) + {std::ref(sfEPrice), std::ref(sfLPTokenIn)}) ammWithdraw(field); // AMMBid auto ammBid = [&](SField const& field) { @@ -1595,37 +1586,6 @@ class MPToken_test : public beast::unit_test::suite }; ammBid(sfBidMin); ammBid(sfBidMax); - // AMMClawback - { - Json::Value jv; - jv[jss::TransactionType] = jss::AMMClawback; - jv[jss::Account] = alice.human(); - jv[jss::Holder] = carol.human(); - jv[jss::Asset] = to_json(xrpIssue()); - jv[jss::Asset2] = to_json(USD.issue()); - jv[jss::Amount] = mpt.getJson(JsonOptions::none); - test(jv, jss::Amount.c_str()); - } - // 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, field.fieldName); - }; - 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, jss::SendMax.c_str()); - } // EscrowCreate { Json::Value jv; @@ -1635,13 +1595,6 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Amount] = mpt.getJson(JsonOptions::none); test(jv, jss::Amount.c_str()); } - // OfferCreate - { - 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 { Json::Value jv; @@ -1806,6 +1759,8 @@ class MPToken_test : public beast::unit_test::suite } } BEAST_EXPECT(txWithAmounts.empty()); + for (auto tx : txWithAmounts) + std::cout << tx << std::endl; } void @@ -2227,6 +2182,955 @@ class MPToken_test : public beast::unit_test::suite } } + void + testOfferCrossing(FeatureBitset features) + { + testcase("Offer Crossing"); + using namespace test::jtx; + Account const gw = Account("gw"); + Account const alice = Account("alice"); + Account const carol = Account("carol"); + auto const USD = gw["USD"]; + + // Blocking flags + for (auto flags : + {tfMPTCanLock | + tfMPTCanTransfer, // locked, issuer and holder fails + tfMPTRequireAuth | + tfMPTCanTransfer, // not authorized, holder fails + tfMPTCanTrade, // can't transfer, holder fails + tfMPTCanLock}) // lock mptoken, holder fails + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice}}); + + auto const lockMPToken = + (flags & (tfMPTCanLock | tfMPTCanTransfer)) == tfMPTCanLock; + auto const lockMPTIssue = + (flags & (tfMPTCanLock | tfMPTCanTransfer)) == + (tfMPTCanLock | tfMPTCanTransfer); + flags = lockMPToken ? (flags | tfMPTCanTransfer) : flags; + + mpt.create({.ownerCount = 1, .holderCount = 0, .flags = flags}); + auto const MPT = mpt["MPT"]; + + if ((flags & tfMPTRequireAuth) == 0) + { + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + } + if (lockMPToken) + mpt.set({.holder = alice, .flags = tfMPTLock}); + else if (lockMPTIssue) + mpt.set({.flags = tfMPTLock}); + + auto const err = + flags & tfMPTRequireAuth ? tecUNFUNDED_OFFER : tecNO_PERMISSION; + + env(offer(alice, XRP(100), MPT(101)), ter(err)); + env.close(); + } + + // MPTokenV2 is disabled + { + Env env{*this, features - featureMPTokensV2}; + + MPTTester mpt(env, gw, {.holders = {alice}}); + + mpt.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + + env(offer(alice, XRP(100), mpt.mpt(101)), ter(temDISABLED)); + env.close(); + } + + // XRP/MPT + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice, carol}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + env(offer(alice, XRP(100), MPT(101))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{XRP(100), MPT(101)}}})); + + env(offer(carol, MPT(101), XRP(100))); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(expectOffers(env, carol, 0)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 301)); + } + + // IOU/MPT + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice, carol}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + env(trust(alice, USD(2'000))); + env(pay(gw, alice, USD(1'000))); + env.close(); + + env(trust(carol, USD(2'000))); + env(pay(gw, carol, USD(1'000))); + env.close(); + + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + env(offer(alice, USD(100), MPT(101))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{USD(100), MPT(101)}}})); + + env(offer(carol, MPT(101), USD(100))); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == USD(1'100)); + BEAST_EXPECT(env.balance(carol, USD) == USD(900)); + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(expectOffers(env, carol, 0)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 301)); + } + + // MPT/MPT + { + Env env{*this, features}; + + MPTTester mpt1(env, gw, {.holders = {alice, carol}}); + mpt1.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + + MPTTester mpt2(env, gw, {.holders = {alice, carol}, .fund = false}); + mpt2.create( + {.ownerCount = 2, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT2 = mpt2["MPT2"]; + + mpt1.authorize({.account = alice}); + mpt1.authorize({.account = carol}); + mpt1.pay(gw, alice, 200); + mpt1.pay(gw, carol, 200); + + mpt2.authorize({.account = alice}); + mpt2.authorize({.account = carol}); + mpt2.pay(gw, alice, 200); + mpt2.pay(gw, carol, 200); + + env(offer(alice, MPT2(100), MPT1(101))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{MPT2(100), MPT1(101)}}})); + + env(offer(carol, MPT1(101), MPT2(100))); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(expectOffers(env, carol, 0)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(carol, 301)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 300)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 100)); + } + } + + void + testCrossAssetPayment(FeatureBitset features) + { + testcase("Cross Asset Payment"); + using namespace test::jtx; + Account const gw = Account("gw"); + Account const alice = Account("alice"); + Account const carol = Account("carol"); + Account const bob = Account("bob"); + auto const USD = gw["USD"]; + + // Blocking flags + for (auto flags : + {tfMPTCanLock | + tfMPTCanTransfer, // locked, issuer and holder fails + tfMPTRequireAuth | + tfMPTCanTransfer, // not authorized, holder fails + tfMPTCanTrade, // can't transfer, holder fails + tfMPTCanLock}) // lock mptoken, holder fails + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice}}); + + auto const lockMPToken = + (flags & (tfMPTCanLock | tfMPTCanTransfer)) == tfMPTCanLock; + auto const lockMPTIssue = + (flags & (tfMPTCanLock | tfMPTCanTransfer)) == + (tfMPTCanLock | tfMPTCanTransfer); + flags = lockMPToken ? (flags | tfMPTCanTransfer) : flags; + + mpt.create({.ownerCount = 1, .holderCount = 0, .flags = flags}); + auto const MPT = mpt["MPT"]; + + if ((flags & tfMPTRequireAuth) == 0) + { + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + } + if (lockMPToken) + mpt.set({.holder = alice, .flags = tfMPTLock}); + else if (lockMPTIssue) + mpt.set({.flags = tfMPTLock}); + + auto const err = + flags & tfMPTRequireAuth ? tecUNFUNDED_OFFER : tecNO_PERMISSION; + + env(offer(alice, XRP(100), MPT(101)), ter(err)); + env.close(); + } + + // Loop + { + } + + // MPTokenV2 is disabled + { + Env env{*this, features - featureMPTokensV2}; + + MPTTester mpt(env, gw, {.holders = {alice}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + + env(pay(gw, alice, MPT(101)), + test::jtx::path(~MPT), + sendmax(XRP(100)), + txflags(tfPartialPayment), + ter(temDISABLED)); + } + + // MPT/XRP + { + Env env{*this, features}; + MPTTester mpt(env, gw, {.holders = {alice, carol, bob}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + mpt.authorize({.account = bob}); + + env(offer(alice, XRP(100), MPT(101))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{XRP(100), MPT(101)}}})); + + env(pay(carol, bob, MPT(101)), + test::jtx::path(~MPT), + sendmax(XRP(100)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 101)); + } + + // MPT/IOU + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice, carol, bob}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + env(trust(alice, USD(2'000))); + env(pay(gw, alice, USD(1'000))); + env(trust(bob, USD(2'000))); + env(pay(gw, bob, USD(1'000))); + env(trust(carol, USD(2'000))); + env(pay(gw, carol, USD(1'000))); + env.close(); + + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + mpt.authorize({.account = bob}); + + env(offer(alice, USD(100), MPT(101))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{USD(100), MPT(101)}}})); + + env(pay(carol, bob, MPT(101)), + test::jtx::path(~MPT), + sendmax(USD(100)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(env.balance(carol, USD) == USD(900)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 101)); + } + + // IOU/MPT + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice, carol, bob}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + env(trust(alice, USD(2'000)), txflags(tfClearNoRipple)); + env(pay(gw, alice, USD(1'000))); + env(trust(bob, USD(2'000)), txflags(tfClearNoRipple)); + env.close(); + + mpt.authorize({.account = alice}); + env(pay(gw, alice, MPT(200))); + + mpt.authorize({.account = carol}); + env(pay(gw, carol, MPT(200))); + + env(offer(alice, MPT(101), USD(100))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{MPT(101), USD(100)}}})); + + env(pay(carol, bob, USD(100)), + test::jtx::path(~USD), + sendmax(MPT(101)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(env.balance(alice, USD) == USD(900)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 301)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 99)); + BEAST_EXPECT(env.balance(bob, USD) == USD(100)); + } + + // MPT/MPT + { + Env env{*this, features}; + + MPTTester mpt1(env, gw, {.holders = {alice, carol, bob}}); + mpt1.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + + MPTTester mpt2( + env, gw, {.holders = {alice, carol, bob}, .fund = false}); + mpt2.create( + {.ownerCount = 2, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT2 = mpt2["MPT2"]; + + mpt1.authorize({.account = alice}); + mpt1.pay(gw, alice, 200); + mpt2.authorize({.account = alice}); + + mpt2.authorize({.account = carol}); + mpt2.pay(gw, carol, 200); + + mpt1.authorize({.account = bob}); + + env(offer(alice, MPT2(100), MPT1(101))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{MPT2(100), MPT1(101)}}})); + + env(pay(carol, bob, MPT1(101)), + test::jtx::path(~MPT1), + sendmax(MPT2(100)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(200)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 100)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 101)); + } + + // XRP/MPT AMM + { + Env env{*this, features}; + + fund(env, gw, {alice, carol, bob}, XRP(11'000), {USD(20'000)}); + + MPTTester mpt(env, gw, {.fund = false}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.authorize({.account = bob}); + mpt.pay(gw, alice, 10'100); + + AMM amm(env, alice, XRP(10'000), MPT(10'100)); + + env(pay(carol, bob, MPT(100)), + test::jtx::path(~MPT), + sendmax(XRP(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(XRP(10'100), MPT(10'000), amm.tokens())); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 100)); + } + + // IOU/MPT AMM + { + Env env{*this, features}; + + fund(env, gw, {alice, carol, bob}, XRP(11'000), {USD(20'000)}); + + MPTTester mpt(env, gw, {.fund = false}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.authorize({.account = bob}); + mpt.pay(gw, alice, 10'100); + + AMM amm(env, alice, USD(10'000), MPT(10'100)); + + env(pay(carol, bob, MPT(100)), + test::jtx::path(~MPT), + sendmax(USD(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(USD(10'100), MPT(10'000), amm.tokens())); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 100)); + } + + // MPT/MPT AMM cross-asset payment + { + Env env{*this, features}; + env.fund(XRP(20'000), gw, alice, carol, bob); + env.close(); + + MPTTester mpt1(env, gw, {.fund = false}); + mpt1.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + mpt1.authorize({.account = alice}); + mpt1.authorize({.account = bob}); + mpt1.pay(gw, alice, 10'100); + + MPTTester mpt2(env, gw, {.fund = false}); + mpt2.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT2 = mpt2["MPT1"]; + mpt2.authorize({.account = alice}); + mpt2.authorize({.account = bob}); + mpt2.authorize({.account = carol}); + mpt2.pay(gw, alice, 10'100); + mpt2.pay(gw, carol, 100); + + AMM amm(env, alice, MPT2(10'000), MPT1(10'100)); + + env(pay(carol, bob, MPT1(100)), + test::jtx::path(~MPT1), + sendmax(MPT2(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(MPT2(10'100), MPT1(10'000), amm.tokens())); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 100)); + } + + // Multi-steps with AMM + // IOU/MPT1 MPT1/MPT2 MPT2/IOU IOU/IOU AMM:IOU/MPT MPT/IOU + { + Env env{*this, features}; + auto const USD = gw["USD"]; + auto const EUR = gw["EUR"]; + auto const CRN = gw["CRN"]; + auto const YAN = gw["YAN"]; + + fund( + env, + gw, + {alice, carol, bob}, + XRP(1'000), + {USD(1'000), EUR(1'000), CRN(2'000), YAN(1'000)}); + + auto createMPT = [&]() -> std::pair { + MPTTester mpt(env, gw, {.fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 2'000); + return {mpt, mpt["MPT"]}; + }; + + auto const [mpt1, MPT1] = createMPT(); + auto const [mpt2, MPT2] = createMPT(); + auto const [mpt3, MPT3] = createMPT(); + + env(offer(alice, EUR(100), MPT1(101))); + env(offer(alice, MPT1(101), MPT2(102))); + env(offer(alice, MPT2(102), USD(103))); + env(offer(alice, USD(103), CRN(104))); + env.close(); + AMM amm(env, alice, CRN(1'000), MPT3(1'104)); + env(offer(alice, MPT3(104), YAN(100))); + + env(pay(carol, bob, YAN(100)), + test::jtx::path(~MPT1, ~MPT2, ~USD, ~CRN, ~MPT3, ~YAN), + sendmax(EUR(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT(env.balance(bob, YAN) == YAN(1'100)); + BEAST_EXPECT( + amm.expectBalances(CRN(1'104), MPT3(1'000), amm.tokens())); + BEAST_EXPECT(expectOffers(env, alice, 0)); + } + } + + void + testPath(FeatureBitset features) + { + testcase("Path"); + using namespace test::jtx; + Account const gw{"gw"}; + Account const gw1{"gw1"}; + Account const alice{"alice"}; + Account const carol{"carol"}; + Account const bob{"bob"}; + Account const dan{"dan"}; + auto const USD = gw["USD"]; + auto const EUR = gw1["EUR"]; + + // MPT can be a mpt end point step or a book-step + + // Direct MPT payment + { + Env env = pathTestEnv(*this); + + MPTTester mpt(env, gw, {.holders = {dan, carol}}); + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = dan}); + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + auto const [pathSet, srcAmt, dstAmt] = + find_paths(env, carol, dan, MPT(-1)); + BEAST_EXPECT(srcAmt == MPT(200)); + BEAST_EXPECT(dstAmt == MPT(200)); + // Direct payment, no path + BEAST_EXPECT(pathSet.empty()); + } + + // Cross-asset payment via XRP/MPT offer (one step) + { + Env env = pathTestEnv(*this); + + env.fund(XRP(1'000), carol); + + MPTTester mpt(env, gw, {.holders = {alice, dan}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.authorize({.account = dan}); + mpt.pay(gw, alice, 200); + + env(offer(alice, XRP(100), MPT(100))); + env.close(); + + auto const [pathSet, srcAmt, dstAmt] = + find_paths(env, carol, dan, MPT(-1)); + BEAST_EXPECT(srcAmt == XRP(100)); + BEAST_EXPECT(dstAmt == MPT(100)); + // This path is consistent with XRP/IOU. + BEAST_EXPECT(same(pathSet, stpath(IPE(mpt.issuanceID())))); + } + + // Cross-asset payment via IOU/MPT offer (one step) + { + Env env = pathTestEnv(*this); + + env.fund(XRP(1'000), carol); + env.fund(XRP(1'000), gw); + + MPTTester mpt(env, gw1, {.holders = {alice, dan}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.authorize({.account = dan}); + mpt.pay(gw1, alice, 200); + + env(trust(alice, USD(400))); + env(trust(carol, USD(400))); + env(pay(gw, carol, USD(200))); + + env(offer(alice, USD(100), MPT(100))); + env.close(); + + auto const [pathSet, srcAmt, dstAmt] = + find_paths(env, carol, dan, MPT(-1)); + BEAST_EXPECT(srcAmt == USD(100)); + BEAST_EXPECT(dstAmt == MPT(100)); + // This path is consistent with IOU1/gw1 / IOU/gw + BEAST_EXPECT(same(pathSet, stpath(gw, IPE(mpt.issuanceID())))); + } + + // Cross-asset payment via MPT1/MPT offer (one step) + { + Env env = pathTestEnv(*this); + + MPTTester mpt(env, gw, {.holders = {alice, dan}}); + MPTTester mpt1(env, gw1, {.holders = {carol}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt1.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + + mpt.authorize({.account = alice}); + mpt.authorize({.account = dan}); + mpt.pay(gw, alice, 200); + + mpt1.authorize({.account = carol}); + mpt1.authorize({.account = alice}); + mpt1.pay(gw1, carol, 200); + + env(offer(alice, MPT1(100), MPT(100))); + env.close(); + + auto const [pathSet, srcAmt, dstAmt] = + find_paths(env, carol, dan, MPT(-1)); + BEAST_EXPECT(srcAmt == MPT1(100)); + BEAST_EXPECT(dstAmt == MPT(100)); + // This path is consistent with IOU1/gw / IOU/gw path - + // [gw1, IOU/gw], except for gw1. This is due to no MPT rippling + BEAST_EXPECT(same(pathSet, stpath(IPE(mpt.issuanceID())))); + } + + // Cross-asset payment via offers (two steps) + { + Env env = pathTestEnv(*this); + + env.fund(XRP(1'000), carol); + env.fund(XRP(1'000), dan); + + MPTTester mpt(env, gw, {.holders = {alice, bob}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.authorize({.account = bob}); + mpt.pay(gw, alice, 200); + mpt.pay(gw, bob, 200); + + env(trust(bob, USD(200))); + env(pay(gw, bob, USD(100))); + env(trust(dan, USD(200))); + env(trust(alice, USD(200))); + + env(offer(alice, XRP(100), MPT(100))); + env(offer(bob, MPT(100), USD(100))); + env.close(); + + auto const [pathSet, srcAmt, dstAmt] = + find_paths(env, carol, dan, USD(-1)); + BEAST_EXPECT(srcAmt == XRP(100)); + BEAST_EXPECT(dstAmt == USD(100)); + // This path is consistent with XRP/ IOU1/gw - IOU1/gw1 / IOU/gw + BEAST_EXPECT( + same(pathSet, stpath(IPE(mpt.issuanceID()), IPE(USD)))); + } + + // Cross-asset payment via offers (two steps) + // Start/End with mpt/mp1 and book steps in the middle + { + Env env = pathTestEnv(*this); + Account const gw2{"gw2"}; + env.fund(XRP(1'000), gw2); + auto const USD2 = gw2["USD"]; + + MPTTester mpt(env, gw, {.holders = {alice, carol}}); + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = alice}); + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + MPTTester mpt1(env, gw1, {.holders = {bob, dan}}); + mpt1.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + mpt1.authorize({.account = bob}); + mpt1.pay(gw1, bob, 200); + mpt1.authorize({.account = dan}); + + env(trust(alice, USD2(400))); + env(pay(gw2, alice, USD2(200))); + env(trust(bob, USD2(400))); + + env(offer(alice, MPT(100), USD2(100))); + env(offer(bob, USD2(100), MPT1(100))); + env.close(); + + auto const [pathSet, srcAmt, dstAmt] = + find_paths(env, carol, dan, MPT1(-1)); + BEAST_EXPECT(srcAmt == MPT(100)); + BEAST_EXPECT(dstAmt == MPT1(100)); + // This path is consistent with IOU/gw / IOU/gw2 - + // IOU/gw2 / IOU1/gw1 path - + // [gw, IOU2/gw2, IOU1/gw1], except for gw. + // This is due to no MPT rippling + BEAST_EXPECT( + same(pathSet, stpath(IPE(USD2), IPE(mpt1.issuanceID())))); + } + } + + void + testCheck(FeatureBitset features) + { + testcase("Check Create/Cash"); + + using namespace test::jtx; + Account const gw{"gw"}; + Account const alice{"alice"}; + + // MPTokensV2 is disabled + { + Env env{*this, features - featureMPTokensV2}; + + MPTTester mpt(env, gw, {.holders = {alice}}); + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = alice}); + + uint256 const checkId{keylet::check(gw, env.seq(gw)).key}; + + env(check::create(gw, alice, MPT(100)), ter(temDISABLED)); + env.close(); + + env(check::cash(alice, checkId, MPT(100)), ter(temDISABLED)); + env.close(); + } + + // Insufficient funds + { + Env env{*this, features}; + Account const carol{"carol"}; + + MPTTester mpt(env, gw, {.holders = {alice, carol}}); + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 50); + + uint256 const checkId{keylet::check(alice, env.seq(alice)).key}; + + // can create + env(check::create(alice, carol, MPT(100))); + env.close(); + + // can't cash since alice only has 50 of MPT + env(check::cash(carol, checkId, MPT(100)), ter(tecPATH_PARTIAL)); + env.close(); + + // can cash if DeliverMin is set + // carol is not authorized, MPToken is authorized by CheckCash + env(check::cash(carol, checkId, check::DeliverMin(MPT(50)))); + env.close(); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 50)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(50)); + } + + // Exceed max amount + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice}}); + mpt.create( + {.maxAmt = 100, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + uint256 const checkId{keylet::check(gw, env.seq(gw)).key}; + + // can create + env(check::create(gw, alice, MPT(200))); + env.close(); + + // can't cash since the outstanding amount exceeds max amount + env(check::cash(alice, checkId, MPT(200)), ter(tecPATH_PARTIAL)); + env.close(); + + // can cash if DeliverMin is set + env(check::cash(alice, checkId, check::DeliverMin(MPT(100)))); + env.close(); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 100)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(100)); + } + + // Normal create/cash + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice}}); + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = alice}); + + uint256 const checkId{keylet::check(gw, env.seq(gw)).key}; + + env(check::create(gw, alice, MPT(100))); + env.close(); + + env(check::cash(alice, checkId, MPT(100))); + env.close(); + + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 100)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(100)); + } + } + + void + testAMMClawback(FeatureBitset features) + { + using namespace jtx; + testcase("AMMClawback"); + + { + Account const gw{"gw"}; + Account const alice{"alice"}; + auto const USD = gw["USD"]; + Env env(*this, features); + fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}); + MPTTester mpt(env, gw, {.holders = {alice}, .fund = false}); + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 1'000); + AMM amm(env, gw, MPT(100), XRP(100)); + amm.deposit(DepositArg{.account = alice, .tokens = 100}); + amm::ammClawback( + gw, alice, MPTIssue(mpt.issuanceID()), xrpIssue(), MPT(10)); + } + } + public: void run() override @@ -2240,10 +3144,12 @@ class MPToken_test : public beast::unit_test::suite // MPTokenIssuanceDestroy testDestroyValidation(all); + testDestroyValidation(all - featureMPTokensV2); testDestroyEnabled(all); // MPTokenAuthorize testAuthorizeValidation(all); + testAuthorizeValidation(all - featureMPTokensV2); testAuthorizeEnabled(all); // MPTokenIssuanceSet @@ -2252,11 +3158,16 @@ class MPToken_test : public beast::unit_test::suite // MPT clawback testClawbackValidation(all); + testClawbackValidation(all - featureMPTokensV2); testClawback(all); + testClawback(all - featureMPTokensV2); // Test Direct Payment testPayment(all); - testDepositPreauth(); + testPayment(all - featureMPTokensV2); + + testDepositPreauth(all); + testDepositPreauth(all - featureMPTokensV2); // Test MPT Amount is invalid in Tx, which don't support MPT testMPTInvalidInTx(all); @@ -2269,6 +3180,21 @@ class MPToken_test : public beast::unit_test::suite // Test helpers testHelperFunctions(); + + // Test offer crossing + testOfferCrossing(all); + + // Test cross asset payment + testCrossAssetPayment(all); + + // Test path finding + testPath(all); + + // Test checks + testCheck(all); + + // Add AMMClawback + testAMMClawback(all); } }; diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 2b4245a1ae4..b0f9d33da5f 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -1330,11 +1330,9 @@ class OfferBaseUtil_test : public beast::unit_test::suite // old and the new behavior. { auto acctOffers = offersOnAccount(env, account_to_test); - bool const noStaleOffers{ - features[featureFlowCross] || - features[fixTakerDryOfferRemoval]}; - BEAST_EXPECT(acctOffers.size() == (noStaleOffers ? 0 : 1)); + // No stale offers since FlowCross is always enabled + BEAST_EXPECT(acctOffers.size() == 0); for (auto const& offerPtr : acctOffers) { auto const& offer = *offerPtr; @@ -1443,8 +1441,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite std::uint32_t const bobOfferSeq = env.seq(bob); env(offer(bob, XRP(2000), USD(1))); - if (localFeatures[featureFlowCross] && - localFeatures[fixReducedOffersV2]) + if (localFeatures[fixReducedOffersV2]) { // With the rounding introduced by fixReducedOffersV2, bob's // offer does not cross alice's offer and goes straight into @@ -1468,8 +1465,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite // crossing algorithms becomes apparent. The old offer crossing // would consume small_amount and transfer no XRP. The new offer // crossing transfers a single drop, rather than no drops. - auto const crossingDelta = - localFeatures[featureFlowCross] ? drops(1) : drops(0); + auto const crossingDelta = drops(1); jrr = ledgerEntryState(env, alice, gw, "USD"); BEAST_EXPECT( @@ -2013,12 +2009,9 @@ class OfferBaseUtil_test : public beast::unit_test::suite env.require(balance(carol, USD(0))); env.require(balance(carol, EUR(none))); - // If neither featureFlowCross nor fixTakerDryOfferRemoval are defined - // then carol's offer will be left on the books, but with zero value. - int const emptyOfferCount{ - features[featureFlowCross] || features[fixTakerDryOfferRemoval] - ? 0 - : 1}; + // carol's offer is left on the books regardless of + // fixTakerDryOfferRemoval since FlowCross is always enabled + int const emptyOfferCount{0}; env.require(offers(carol, 0 + emptyOfferCount)); env.require(owners(carol, 1 + emptyOfferCount)); @@ -4197,12 +4190,6 @@ class OfferBaseUtil_test : public beast::unit_test::suite }; // clang-format off - TestData const takerTests[]{ - // btcStart ------------------- actor[0] -------------------- ------------------- actor[1] -------------------- - {0, 0, 1, BTC(5), {{"deb", 0, drops(3899999999960), BTC(5), USD(3000)}, {"dan", 0, drops(4099999999970), BTC(0), USD(750)}}}, // no BTC xfer fee - {0, 0, 0, BTC(5), {{"flo", 0, drops(3999999999950), BTC(5), USD(2000)} }} // no xfer fee - }; - TestData const flowTests[]{ // btcStart ------------------- actor[0] -------------------- ------------------- actor[1] -------------------- {0, 0, 1, BTC(5), {{"gay", 1, drops(3949999999960), BTC(5), USD(2500)}, {"gar", 1, drops(4049999999970), BTC(0), USD(1375)}}}, // no BTC xfer fee @@ -4210,10 +4197,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite }; // clang-format on - // Pick the right tests. - auto const& tests = features[featureFlowCross] ? flowTests : takerTests; - - for (auto const& t : tests) + for (auto const& t : flowTests) { Account const& self = t.actors[t.self].acct; Account const& leg0 = t.actors[t.leg0].acct; @@ -4339,8 +4323,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite // 1. alice creates an offer to acquire USD/gw, an asset for which // she does not have a trust line. At some point in the future, // gw adds lsfRequireAuth. Then, later, alice's offer is crossed. - // a. With Taker alice's unauthorized offer is consumed. - // b. With FlowCross alice's offer is deleted, not consumed, + // With FlowCross alice's offer is deleted, not consumed, // since alice is not authorized to hold USD/gw. // // 2. alice tries to create an offer for USD/gw, now that gw has @@ -4389,33 +4372,18 @@ class OfferBaseUtil_test : public beast::unit_test::suite // gw now requires authorization and bob has gwUSD(50). Let's see if // bob can cross alice's offer. // - // o With Taker bob's offer should cross alice's. // o With FlowCross bob's offer shouldn't cross and alice's // unauthorized offer should be deleted. env(offer(bob, XRP(4000), gwUSD(40))); env.close(); std::uint32_t const bobOfferSeq = env.seq(bob) - 1; - bool const flowCross = features[featureFlowCross]; - env.require(offers(alice, 0)); - if (flowCross) - { - // alice's unauthorized offer is deleted & bob's offer not crossed. - env.require(balance(alice, gwUSD(none))); - env.require(offers(bob, 1)); - env.require(balance(bob, gwUSD(50))); - } - else - { - // alice's offer crosses bob's - env.require(balance(alice, gwUSD(40))); - env.require(offers(bob, 0)); - env.require(balance(bob, gwUSD(10))); - // The rest of the test verifies FlowCross behavior. - return; - } + // alice's unauthorized offer is deleted & bob's offer not crossed. + env.require(balance(alice, gwUSD(none))); + env.require(offers(bob, 1)); + env.require(balance(bob, gwUSD(50))); // See if alice can create an offer without authorization. alice // should not be able to create the offer and bob's offer should be @@ -5144,9 +5112,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite // tfFillOrKill, TakerPays must be filled { TER const err = - features[fixFillOrKill] || !features[featureFlowCross] - ? TER(tesSUCCESS) - : tecKILLED; + features[fixFillOrKill] ? TER(tesSUCCESS) : tecKILLED; env(offer(maker, XRP(100), USD(100))); env.close(); @@ -5368,7 +5334,6 @@ class OfferBaseUtil_test : public beast::unit_test::suite { using namespace jtx; static FeatureBitset const all{supported_amendments()}; - static FeatureBitset const flowCross{featureFlowCross}; static FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval}; static FeatureBitset const rmSmallIncreasedQOffers{ fixRmSmallIncreasedQOffers}; @@ -5376,10 +5341,9 @@ class OfferBaseUtil_test : public beast::unit_test::suite featureImmediateOfferKilled}; FeatureBitset const fillOrKill{fixFillOrKill}; - static std::array const feats{ + static std::array const feats{ all - takerDryOffer - immediateOfferKilled, - all - flowCross - takerDryOffer - immediateOfferKilled, - all - flowCross - immediateOfferKilled, + all - immediateOfferKilled, all - rmSmallIncreasedQOffers - immediateOfferKilled - fillOrKill, all - fillOrKill, all}; @@ -5399,21 +5363,12 @@ class OfferBaseUtil_test : public beast::unit_test::suite } }; -class OfferWOFlowCross_test : public OfferBaseUtil_test -{ - void - run() override - { - OfferBaseUtil_test::run(1); - } -}; - class OfferWTakerDryOffer_test : public OfferBaseUtil_test { void run() override { - OfferBaseUtil_test::run(2); + OfferBaseUtil_test::run(1); } }; @@ -5422,7 +5377,7 @@ class OfferWOSmallQOffers_test : public OfferBaseUtil_test void run() override { - OfferBaseUtil_test::run(3); + OfferBaseUtil_test::run(2); } }; @@ -5431,7 +5386,7 @@ class OfferWOFillOrKill_test : public OfferBaseUtil_test void run() override { - OfferBaseUtil_test::run(4); + OfferBaseUtil_test::run(3); } }; @@ -5440,7 +5395,7 @@ class OfferAllFeatures_test : public OfferBaseUtil_test void run() override { - OfferBaseUtil_test::run(5, true); + OfferBaseUtil_test::run(4, true); } }; @@ -5451,24 +5406,22 @@ class Offer_manual_test : public OfferBaseUtil_test { using namespace jtx; FeatureBitset const all{supported_amendments()}; - FeatureBitset const flowCross{featureFlowCross}; FeatureBitset const f1513{fix1513}; FeatureBitset const immediateOfferKilled{featureImmediateOfferKilled}; FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval}; FeatureBitset const fillOrKill{fixFillOrKill}; - testAll(all - flowCross - f1513 - immediateOfferKilled); - testAll(all - flowCross - immediateOfferKilled); + testAll(all - f1513 - immediateOfferKilled); + testAll(all - immediateOfferKilled); testAll(all - immediateOfferKilled - fillOrKill); testAll(all - fillOrKill); testAll(all); - testAll(all - flowCross - takerDryOffer); + testAll(all - takerDryOffer); } }; BEAST_DEFINE_TESTSUITE_PRIO(OfferBaseUtil, tx, ripple, 2); -BEAST_DEFINE_TESTSUITE_PRIO(OfferWOFlowCross, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWTakerDryOffer, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWOSmallQOffers, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWOFillOrKill, tx, ripple, 2); diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp index f00a7361292..1a3bad1aa04 100644 --- a/src/test/app/PayStrand_test.cpp +++ b/src/test/app/PayStrand_test.cpp @@ -1006,7 +1006,11 @@ struct PayStrand_test : public beast::unit_test::suite // alice -> USD/XRP -> bob STPath path; - path.emplace_back(std::nullopt, xrpCurrency(), std::nullopt); + path.emplace_back( + std::nullopt, + xrpCurrency(), + std::nullopt, + STPathElement::PathAssetTag{}); auto [ter, strand] = toStrand( *env.current(), @@ -1255,13 +1259,10 @@ struct PayStrand_test : public beast::unit_test::suite { using namespace jtx; auto const sa = supported_amendments(); - testToStrand(sa - featureFlowCross); testToStrand(sa); - testRIPD1373(sa - featureFlowCross); testRIPD1373(sa); - testLoop(sa - featureFlowCross); testLoop(sa); testNoAccount(sa); diff --git a/src/test/app/SetAuth_test.cpp b/src/test/app/SetAuth_test.cpp index 3dd8ab590a4..9c6f3ed18de 100644 --- a/src/test/app/SetAuth_test.cpp +++ b/src/test/app/SetAuth_test.cpp @@ -73,7 +73,6 @@ struct SetAuth_test : public beast::unit_test::suite { using namespace jtx; auto const sa = supported_amendments(); - testAuth(sa - featureFlowCross); testAuth(sa); } }; diff --git a/src/test/app/TheoreticalQuality_test.cpp b/src/test/app/TheoreticalQuality_test.cpp index 917d23377bf..eaa6eb1a67f 100644 --- a/src/test/app/TheoreticalQuality_test.cpp +++ b/src/test/app/TheoreticalQuality_test.cpp @@ -71,7 +71,8 @@ struct RippleCalcTestParams *parseBase58( pe[jss::account].asString()), std::nullopt, - std::nullopt); + std::nullopt, + STPathElement::PathAssetTag{}); } else if ( pe.isMember(jss::currency) && pe.isMember(jss::issuer)) @@ -85,7 +86,11 @@ struct RippleCalcTestParams else assert(isXRP(*parseBase58( pe[jss::issuer].asString()))); - p.emplace_back(std::nullopt, currency, issuer); + p.emplace_back( + std::nullopt, + currency, + issuer, + STPathElement::PathAssetTag{}); } else { diff --git a/src/test/app/TrustAndBalance_test.cpp b/src/test/app/TrustAndBalance_test.cpp index b438d797276..29ec1dae2b1 100644 --- a/src/test/app/TrustAndBalance_test.cpp +++ b/src/test/app/TrustAndBalance_test.cpp @@ -480,7 +480,6 @@ class TrustAndBalance_test : public beast::unit_test::suite using namespace test::jtx; auto const sa = supported_amendments(); - testWithFeatures(sa - featureFlowCross); testWithFeatures(sa); } }; diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index 52039f74aea..0c0ccc2dcdc 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -77,7 +77,7 @@ struct DepositArg std::optional asset2In = std::nullopt; std::optional maxEP = std::nullopt; std::optional flags = std::nullopt; - std::optional> assets = std::nullopt; + std::optional> assets = std::nullopt; std::optional seq = std::nullopt; std::optional tfee = std::nullopt; std::optional err = std::nullopt; @@ -91,7 +91,7 @@ struct WithdrawArg std::optional asset2Out = std::nullopt; std::optional maxEP = std::nullopt; std::optional flags = std::nullopt; - std::optional> assets = std::nullopt; + std::optional> assets = std::nullopt; std::optional seq = std::nullopt; std::optional err = std::nullopt; }; @@ -102,7 +102,7 @@ struct VoteArg std::uint32_t tfee = 0; std::optional flags = std::nullopt; std::optional seq = std::nullopt; - std::optional> assets = std::nullopt; + std::optional> assets = std::nullopt; std::optional err = std::nullopt; }; @@ -113,7 +113,7 @@ struct BidArg std::optional> bidMax = std::nullopt; std::vector authAccounts = {}; std::optional flags = std::nullopt; - std::optional> assets = std::nullopt; + std::optional> assets = std::nullopt; }; /** Convenience class to test AMM functionality. @@ -171,8 +171,8 @@ class AMM ammRpcInfo( std::optional const& account = std::nullopt, std::optional const& ledgerIndex = std::nullopt, - std::optional issue1 = std::nullopt, - std::optional issue2 = std::nullopt, + std::optional issue1 = std::nullopt, + std::optional issue2 = std::nullopt, std::optional const& ammAccount = std::nullopt, bool ignoreParams = false, unsigned apiVersion = RPC::apiInvalidVersion) const; @@ -190,8 +190,8 @@ class AMM */ std::tuple balances( - Issue const& issue1, - Issue const& issue2, + Asset const& issue1, + Asset const& issue2, std::optional const& account = std::nullopt) const; [[nodiscard]] bool @@ -251,7 +251,7 @@ class AMM std::optional const& asset2In, std::optional const& maxEP, std::optional const& flags, - std::optional> const& assets, + std::optional> const& assets, std::optional const& seq, std::optional const& tfee = std::nullopt, std::optional const& ter = std::nullopt); @@ -297,7 +297,7 @@ class AMM std::optional const& asset2Out, std::optional const& maxEP, std::optional const& flags, - std::optional> const& assets, + std::optional> const& assets, std::optional const& seq, std::optional const& ter = std::nullopt); @@ -310,7 +310,7 @@ class AMM std::uint32_t feeVal, std::optional const& flags = std::nullopt, std::optional const& seq = std::nullopt, - std::optional> const& assets = std::nullopt, + std::optional> const& assets = std::nullopt, std::optional const& ter = std::nullopt); void @@ -381,7 +381,7 @@ class AMM void setTokens( Json::Value& jv, - std::optional> const& assets = std::nullopt); + std::optional> const& assets = std::nullopt); private: AccountID @@ -395,7 +395,7 @@ class AMM deposit( std::optional const& account, Json::Value& jv, - std::optional> const& assets = std::nullopt, + std::optional> const& assets = std::nullopt, std::optional const& seq = std::nullopt, std::optional const& ter = std::nullopt); @@ -404,7 +404,7 @@ class AMM std::optional const& account, Json::Value& jv, std::optional const& seq, - std::optional> const& assets = std::nullopt, + std::optional> const& assets = std::nullopt, std::optional const& ter = std::nullopt); void @@ -443,8 +443,8 @@ Json::Value ammClawback( Account const& issuer, Account const& holder, - Issue const& asset, - Issue const& asset2, + Asset const& asset, + Asset const& asset2, std::optional const& amount); } // namespace amm diff --git a/src/test/jtx/PathSet.h b/src/test/jtx/PathSet.h index 0f4c4ddd3dd..1d5a300a8e0 100644 --- a/src/test/jtx/PathSet.h +++ b/src/test/jtx/PathSet.h @@ -33,15 +33,15 @@ inline std::size_t countOffers( jtx::Env& env, jtx::Account const& account, - Issue const& takerPays, - Issue const& takerGets) + Asset const& takerPays, + Asset const& takerGets) { size_t count = 0; forEachItem( *env.current(), account, [&](std::shared_ptr const& sle) { if (sle->getType() == ltOFFER && - sle->getFieldAmount(sfTakerPays).issue() == takerPays && - sle->getFieldAmount(sfTakerGets).issue() == takerGets) + sle->getFieldAmount(sfTakerPays).asset() == takerPays && + sle->getFieldAmount(sfTakerGets).asset() == takerGets) ++count; }); return count; @@ -83,8 +83,8 @@ inline bool isOffer( jtx::Env& env, jtx::Account const& account, - Issue const& takerPays, - Issue const& takerGets) + Asset const& takerPays, + Asset const& takerGets) { return countOffers(env, account, takerPays, takerGets) > 0; } @@ -143,7 +143,7 @@ Path::push_back(Issue const& iss) inline Path& Path::push_back(jtx::Account const& account) { - path.emplace_back(account.id(), beast::zero, beast::zero); + path.emplace_back(account.id(), Currency{beast::zero}, beast::zero); return *this; } diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index d81551aa840..67f67a41e2e 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -140,6 +140,9 @@ equal(STAmount const& sa1, STAmount const& sa2); STPathElement IPE(Issue const& iss); +STPathElement +IPE(MPTIssue const& iss); + template STPath stpath(Args const&... args) @@ -166,6 +169,63 @@ same(STPathSet const& st1, Args const&... args) return true; } +Json::Value +rpf(jtx::Account const& src, + jtx::Account const& dst, + STAmount const& dstAmount, + std::optional const& sendMax = std::nullopt, + std::optional const& srcCurrency = std::nullopt); + +jtx::Env +pathTestEnv(beast::unit_test::suite& suite); + +class gate +{ +private: + std::condition_variable cv_; + std::mutex mutex_; + bool signaled_ = false; + +public: + // Thread safe, blocks until signaled or period expires. + // Returns `true` if signaled. + template + bool + wait_for(std::chrono::duration const& rel_time) + { + std::unique_lock lk(mutex_); + auto b = cv_.wait_for(lk, rel_time, [this] { return signaled_; }); + signaled_ = false; + return b; + } + + void + signal() + { + std::lock_guard lk(mutex_); + signaled_ = true; + cv_.notify_all(); + } +}; + +Json::Value +find_paths_request( + jtx::Env& env, + jtx::Account const& src, + jtx::Account const& dst, + STAmount const& saDstAmount, + std::optional const& saSendMax = std::nullopt, + std::optional const& saSrcCurrency = std::nullopt); + +std::tuple +find_paths( + jtx::Env& env, + jtx::Account const& src, + jtx::Account const& dst, + STAmount const& saDstAmount, + std::optional const& saSendMax = std::nullopt, + std::optional const& saSrcCurrency = std::nullopt); + /******************************************************************************/ XRPAmount diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index 9468b791f3d..5eb852e6fd2 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -154,11 +154,9 @@ operator<<(std::ostream& os, PrettyAmount const& amount); // Specifies an order book struct BookSpec { - AccountID account; - ripple::Currency currency; + ripple::Asset asset; - BookSpec(AccountID const& account_, ripple::Currency const& currency_) - : account(account_), currency(currency_) + BookSpec(ripple::Asset const& asset_) : asset(asset_) { } }; @@ -176,6 +174,10 @@ struct XRP_t { return xrpIssue(); } + operator Asset() const + { + return xrpIssue(); + } /** Returns an amount of XRP as PrettyAmount, which is trivially convertable to STAmount @@ -220,7 +222,7 @@ struct XRP_t friend BookSpec operator~(XRP_t const&) { - return BookSpec(xrpAccount(), xrpCurrency()); + return BookSpec(Issue{xrpCurrency(), xrpAccount()}); } }; @@ -350,7 +352,7 @@ class IOU friend BookSpec operator~(IOU const& iou) { - return BookSpec(iou.account.id(), iou.currency); + return BookSpec(Issue{iou.currency, iou.account.id()}); } }; @@ -392,6 +394,10 @@ class MPT { return MPTIssue{issuanceID}; } + operator ripple::Asset() const + { + return mpt(); + } template requires(sizeof(T) >= sizeof(int) && std::is_arithmetic_v) @@ -409,9 +415,7 @@ class MPT friend BookSpec operator~(MPT const& mpt) { - assert(false); - Throw("MPT is not supported"); - return BookSpec{beast::zero, noCurrency()}; + return BookSpec{Asset{mpt}}; } }; diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 089d3508d70..d24bdf62458 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -63,7 +63,7 @@ AMM::AMM( , creatorAccount_(account) , asset1_(asset1) , asset2_(asset2) - , ammID_(keylet::amm(asset1_.issue(), asset2_.issue()).key) + , ammID_(keylet::amm(asset1_.asset(), asset2_.asset()).key) , initialLPTokens_(initialTokens(asset1, asset2)) , log_(log) , doClose_(close) @@ -73,10 +73,8 @@ AMM::AMM( , msig_(ms) , fee_(fee) , ammAccount_(create(tfee, flags, seq, ter)) - , lptIssue_(ripple::ammLPTIssue( - asset1_.issue().currency, - asset2_.issue().currency, - ammAccount_)) + , lptIssue_( + ripple::ammLPTIssue(asset1_.asset(), asset2_.asset(), ammAccount_)) { } @@ -148,7 +146,7 @@ AMM::create( if (!ter || env_.ter() == tesSUCCESS) { if (auto const amm = env_.current()->read( - keylet::amm(asset1_.issue(), asset2_.issue()))) + keylet::amm(asset1_.asset(), asset2_.asset()))) { return amm->getAccountID(sfAccount); } @@ -160,8 +158,8 @@ Json::Value AMM::ammRpcInfo( std::optional const& account, std::optional const& ledgerIndex, - std::optional issue1, - std::optional issue2, + std::optional issue1, + std::optional issue2, std::optional const& ammAccount, bool ignoreParams, unsigned apiVersion) const @@ -185,9 +183,9 @@ AMM::ammRpcInfo( else if (!ammAccount) { jv[jss::asset] = - STIssue(sfAsset, asset1_.issue()).getJson(JsonOptions::none); + STIssue(sfAsset, asset1_.asset()).getJson(JsonOptions::none); jv[jss::asset2] = - STIssue(sfAsset2, asset2_.issue()).getJson(JsonOptions::none); + STIssue(sfAsset2, asset2_.asset()).getJson(JsonOptions::none); } if (ammAccount) jv[jss::amm_account] = to_string(*ammAccount); @@ -204,12 +202,12 @@ AMM::ammRpcInfo( std::tuple AMM::balances( - Issue const& issue1, - Issue const& issue2, + Asset const& issue1, + Asset const& issue2, std::optional const& account) const { if (auto const amm = - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()))) + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) { auto const ammAccountID = amm->getAccountID(sfAccount); auto const [asset1Balance, asset2Balance] = ammPoolHolds( @@ -218,6 +216,7 @@ AMM::balances( issue1, issue2, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, env_.journal); auto const lptAMMBalance = account ? ammLPHolds(*env_.current(), *amm, *account, env_.journal) @@ -235,7 +234,7 @@ AMM::expectBalances( std::optional const& account) const { auto const [asset1Balance, asset2Balance, lptAMMBalance] = - balances(asset1.issue(), asset2.issue(), account); + balances(asset1.asset(), asset2.asset(), account); return asset1 == asset1Balance && asset2 == asset2Balance && lptAMMBalance == STAmount{lpt, lptIssue_}; } @@ -252,7 +251,7 @@ AMM::getLPTokensBalance(std::optional const& account) const env_.journal) .iou(); if (auto const amm = - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()))) + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) return amm->getFieldAmount(sfLPTokenBalance).iou(); return IOUAmount{0}; } @@ -261,7 +260,7 @@ bool AMM::expectLPTokens(AccountID const& account, IOUAmount const& expTokens) const { if (auto const amm = - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()))) + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) { auto const lptAMMBalance = ammLPHolds(*env_.current(), *amm, account, env_.journal); @@ -311,7 +310,7 @@ bool AMM::expectTradingFee(std::uint16_t fee) const { auto const amm = - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue())); + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset())); return amm && (*amm)[sfTradingFee] == fee; } @@ -319,7 +318,7 @@ bool AMM::ammExists() const { return env_.current()->read(keylet::account(ammAccount_)) != nullptr && - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue())) != + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset())) != nullptr; } @@ -360,7 +359,7 @@ AMM::expectAmmInfo( if (!amountFromJsonNoThrow(lptBalance, jv[jss::lp_token])) return false; // ammRpcInfo returns unordered assets - if (asset1Info.issue() != asset1.issue()) + if (asset1Info.asset() != asset1.asset()) std::swap(asset1Info, asset2Info); return asset1 == asset1Info && asset2 == asset2Info && lptBalance == STAmount{balance, lptIssue_}; @@ -369,7 +368,7 @@ AMM::expectAmmInfo( void AMM::setTokens( Json::Value& jv, - std::optional> const& assets) + std::optional> const& assets) { if (assets) { @@ -381,9 +380,9 @@ AMM::setTokens( else { jv[jss::Asset] = - STIssue(sfAsset, asset1_.issue()).getJson(JsonOptions::none); + STIssue(sfAsset, asset1_.asset()).getJson(JsonOptions::none); jv[jss::Asset2] = - STIssue(sfAsset, asset2_.issue()).getJson(JsonOptions::none); + STIssue(sfAsset, asset2_.asset()).getJson(JsonOptions::none); } } @@ -391,7 +390,7 @@ IOUAmount AMM::deposit( std::optional const& account, Json::Value& jv, - std::optional> const& assets, + std::optional> const& assets, std::optional const& seq, std::optional const& ter) { @@ -436,7 +435,8 @@ AMM::deposit( std::optional const& flags, std::optional const& ter) { - assert(!(asset2In && maxEP)); + if (asset2In && maxEP) + Throw("Invalid options: asset2In and maxEP"); return deposit( account, std::nullopt, @@ -458,7 +458,7 @@ AMM::deposit( std::optional const& asset2In, std::optional const& maxEP, std::optional const& flags, - std::optional> const& assets, + std::optional> const& assets, std::optional const& seq, std::optional const& tfee, std::optional const& ter) @@ -518,7 +518,7 @@ AMM::withdraw( std::optional const& account, Json::Value& jv, std::optional const& seq, - std::optional> const& assets, + std::optional> const& assets, std::optional const& ter) { auto const& acct = account ? *account : creatorAccount_; @@ -560,7 +560,8 @@ AMM::withdraw( std::optional const& maxEP, std::optional const& ter) { - assert(!(asset2Out && maxEP)); + if (asset2Out && maxEP) + Throw("Invalid options: asset2Out and maxEP"); return withdraw( account, std::nullopt, @@ -581,7 +582,7 @@ AMM::withdraw( std::optional const& asset2Out, std::optional const& maxEP, std::optional const& flags, - std::optional> const& assets, + std::optional> const& assets, std::optional const& seq, std::optional const& ter) { @@ -638,7 +639,7 @@ AMM::vote( std::uint32_t feeVal, std::optional const& flags, std::optional const& seq, - std::optional> const& assets, + std::optional> const& assets, std::optional const& ter) { Json::Value jv; @@ -663,11 +664,11 @@ Json::Value AMM::bid(BidArg const& arg) { if (auto const amm = - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()))) + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) { - assert( - !env_.current()->rules().enabled(fixInnerObjTemplate) || - amm->isFieldPresent(sfAuctionSlot)); + if (env_.current()->rules().enabled(fixInnerObjTemplate) && + !amm->isFieldPresent(sfAuctionSlot)) + Throw("AMM::Bid"); if (amm->isFieldPresent(sfAuctionSlot)) { auto const& auctionSlot = @@ -758,11 +759,11 @@ bool AMM::expectAuctionSlot(auto&& cb) const { if (auto const amm = - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()))) + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) { - assert( - !env_.current()->rules().enabled(fixInnerObjTemplate) || - amm->isFieldPresent(sfAuctionSlot)); + if (env_.current()->rules().enabled(fixInnerObjTemplate) && + !amm->isFieldPresent(sfAuctionSlot)) + Throw("AMM::expectAuctionSlot"); if (amm->isFieldPresent(sfAuctionSlot)) { auto const& auctionSlot = @@ -828,8 +829,8 @@ Json::Value ammClawback( Account const& issuer, Account const& holder, - Issue const& asset, - Issue const& asset2, + Asset const& asset, + Asset const& asset2, std::optional const& amount) { Json::Value jv; diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index b39cac7dcc1..a04d957e304 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -64,7 +64,11 @@ ownerCount(Env const& env, Account const& account) void stpath_append_one(STPath& st, Account const& account) { - st.push_back(STPathElement({account.id(), std::nullopt, std::nullopt})); + st.push_back(STPathElement( + {account.id(), + std::nullopt, + std::nullopt, + STPathElement::PathAssetTag{}})); } void @@ -86,9 +90,163 @@ IPE(Issue const& iss) return STPathElement( STPathElement::typeCurrency | STPathElement::typeIssuer, xrpAccount(), - iss.currency, + PathAsset{iss.currency}, iss.account); } +STPathElement +IPE(MPTIssue const& iss) +{ + return STPathElement( + STPathElement::typeMPT | STPathElement::typeIssuer, + xrpAccount(), + PathAsset{iss.getMptID()}, + iss.getIssuer()); +} + +Json::Value +rpf(jtx::Account const& src, + jtx::Account const& dst, + STAmount const& dstAmount, + std::optional const& sendMax, + std::optional const& srcCurrency) +{ + Json::Value jv = Json::objectValue; + jv[jss::command] = "ripple_path_find"; + jv[jss::source_account] = toBase58(src); + jv[jss::destination_account] = toBase58(dst); + jv[jss::destination_amount] = dstAmount.getJson(JsonOptions::none); + if (sendMax) + jv[jss::send_max] = sendMax->getJson(JsonOptions::none); + if (srcCurrency) + { + auto& sc = jv[jss::source_currencies] = Json::arrayValue; + Json::Value j = Json::objectValue; + j[jss::currency] = to_string(srcCurrency.value()); + sc.append(j); + } + + return jv; +} + +jtx::Env +pathTestEnv(beast::unit_test::suite& suite) +{ + // These tests were originally written with search parameters that are + // different from the current defaults. This function creates an env + // with the search parameters that the tests were written for. + using namespace jtx; + return Env(suite, envconfig([](std::unique_ptr cfg) { + cfg->PATH_SEARCH_OLD = 7; + cfg->PATH_SEARCH = 7; + cfg->PATH_SEARCH_MAX = 10; + return cfg; + })); +} + +Json::Value +find_paths_request( + jtx::Env& env, + jtx::Account const& src, + jtx::Account const& dst, + STAmount const& saDstAmount, + std::optional const& saSendMax, + std::optional const& saSrcCurrency) +{ + using namespace jtx; + + auto& app = env.app(); + Resource::Charge loadType = Resource::feeReferenceRPC; + Resource::Consumer c; + + RPC::JsonContext context{ + {env.journal, + app, + loadType, + app.getOPs(), + app.getLedgerMaster(), + c, + Role::USER, + {}, + {}, + RPC::apiVersionIfUnspecified}, + {}, + {}}; + + Json::Value params = Json::objectValue; + params[jss::command] = "ripple_path_find"; + params[jss::source_account] = toBase58(src); + params[jss::destination_account] = toBase58(dst); + params[jss::destination_amount] = saDstAmount.getJson(JsonOptions::none); + if (saSendMax) + params[jss::send_max] = saSendMax->getJson(JsonOptions::none); + if (saSrcCurrency) + { + auto& sc = params[jss::source_currencies] = Json::arrayValue; + Json::Value j = Json::objectValue; + j[jss::currency] = to_string(saSrcCurrency.value()); + sc.append(j); + } + + Json::Value result; + gate g; + app.getJobQueue().postCoro(jtCLIENT, "RPC-Client", [&](auto const& coro) { + context.params = std::move(params); + context.coro = coro; + RPC::doCommand(context, result); + g.signal(); + }); + + using namespace std::chrono_literals; + using namespace beast::unit_test; + g.wait_for(5s); + return result; +} + +std::tuple +find_paths( + jtx::Env& env, + jtx::Account const& src, + jtx::Account const& dst, + STAmount const& saDstAmount, + std::optional const& saSendMax, + std::optional const& saSrcCurrency) +{ + Json::Value result = find_paths_request( + env, src, dst, saDstAmount, saSendMax, saSrcCurrency); + if (result.isMember(jss::error)) + return std::make_tuple(STPathSet{}, STAmount{}, STAmount{}); + + STAmount da; + if (result.isMember(jss::destination_amount)) + da = amountFromJson(sfGeneric, result[jss::destination_amount]); + + STAmount sa; + STPathSet paths; + if (result.isMember(jss::alternatives)) + { + auto const& alts = result[jss::alternatives]; + if (alts.size() > 0) + { + auto const& path = alts[0u]; + + if (path.isMember(jss::source_amount)) + sa = amountFromJson(sfGeneric, path[jss::source_amount]); + + if (path.isMember(jss::destination_amount)) + da = amountFromJson(sfGeneric, path[jss::destination_amount]); + + if (path.isMember(jss::paths_computed)) + { + Json::Value p; + p["Paths"] = path[jss::paths_computed]; + STParsedJSONObject po("generic", p); + paths = po.object->getFieldPathSet(sfPaths); + } + } + } + + return std::make_tuple(std::move(paths), std::move(sa), std::move(da)); +} /******************************************************************************/ diff --git a/src/test/jtx/impl/amount.cpp b/src/test/jtx/impl/amount.cpp index 5be53dc0a95..ac0a6553ec7 100644 --- a/src/test/jtx/impl/amount.cpp +++ b/src/test/jtx/impl/amount.cpp @@ -123,6 +123,13 @@ operator<<(std::ostream& os, IOU const& iou) return os; } +std::ostream& +operator<<(std::ostream& os, MPT const& mpt) +{ + os << to_string(mpt.issuanceID); + return os; +} + any_t const any{}; } // namespace jtx diff --git a/src/test/jtx/impl/paths.cpp b/src/test/jtx/impl/paths.cpp index 393e36e9d61..4608a6cde98 100644 --- a/src/test/jtx/impl/paths.cpp +++ b/src/test/jtx/impl/paths.cpp @@ -33,8 +33,8 @@ paths::operator()(Env& env, JTx& jt) const auto const to = env.lookup(jv[jss::Destination].asString()); auto const amount = amountFromJson(sfAmount, jv[jss::Amount]); Pathfinder pf( - std::make_shared( - env.current(), env.app().journal("RippleLineCache")), + std::make_shared( + env.current(), env.app().journal("AssetCache")), from, to, in_.currency, @@ -88,8 +88,13 @@ void path::append_one(BookSpec const& book) { auto& jv = create(); - jv["currency"] = to_string(book.currency); - jv["issuer"] = toBase58(book.account); + if (book.asset.holds()) + jv["mpt_issuance_id"] = to_string(book.asset); + else + { + jv["currency"] = to_string(book.asset.get().currency); + jv["issuer"] = toBase58(book.asset.getIssuer()); + } } void diff --git a/src/xrpld/app/ledger/AcceptedLedgerTx.cpp b/src/xrpld/app/ledger/AcceptedLedgerTx.cpp index 620ab031762..841353908b8 100644 --- a/src/xrpld/app/ledger/AcceptedLedgerTx.cpp +++ b/src/xrpld/app/ledger/AcceptedLedgerTx.cpp @@ -62,13 +62,14 @@ AcceptedLedgerTx::AcceptedLedgerTx( auto const amount = mTxn->getFieldAmount(sfTakerGets); // If the offer create is not self funded then add the owner balance - if (account != amount.issue().account) + if (account != amount.getIssuer()) { auto const ownerFunds = accountFunds( *ledger, account, amount, fhIGNORE_FREEZE, + ahIGNORE_AUTH, beast::Journal{beast::Journal::getNullSink()}); mJson[jss::transaction][jss::owner_funds] = ownerFunds.getText(); } diff --git a/src/xrpld/app/ledger/OrderBookDB.cpp b/src/xrpld/app/ledger/OrderBookDB.cpp index 926728cbf4f..fa610878e91 100644 --- a/src/xrpld/app/ledger/OrderBookDB.cpp +++ b/src/xrpld/app/ledger/OrderBookDB.cpp @@ -115,10 +115,28 @@ OrderBookDB::update(std::shared_ptr const& ledger) { Book book; - book.in.currency = sle->getFieldH160(sfTakerPaysCurrency); - book.in.account = sle->getFieldH160(sfTakerPaysIssuer); - book.out.currency = sle->getFieldH160(sfTakerGetsCurrency); - book.out.account = sle->getFieldH160(sfTakerGetsIssuer); + if (sle->isFieldPresent(sfTakerPaysCurrency)) + { + Issue iss; + iss.currency = sle->getFieldH160(sfTakerPaysCurrency); + iss.account = sle->getFieldH160(sfTakerPaysIssuer); + book.in = iss; + } + else + { + book.in = sle->getFieldH192(sfTakerPaysMPT); + } + if (sle->isFieldPresent(sfTakerGetsCurrency)) + { + Issue iss; + iss.currency = sle->getFieldH160(sfTakerGetsCurrency); + iss.account = sle->getFieldH160(sfTakerGetsIssuer); + book.out = iss; + } + else + { + book.out = sle->getFieldH192(sfTakerGetsMPT); + } allBooks[book.in].insert(book.out); @@ -129,9 +147,9 @@ OrderBookDB::update(std::shared_ptr const& ledger) } else if (sle->getType() == ltAMM) { - auto const issue1 = (*sle)[sfAsset]; - auto const issue2 = (*sle)[sfAsset2]; - auto addBook = [&](Issue const& in, Issue const& out) { + auto const asset1 = (*sle)[sfAsset]; + auto const asset2 = (*sle)[sfAsset2]; + auto addBook = [&](Asset const& in, Asset const& out) { allBooks[in].insert(out); if (isXRP(out)) @@ -139,8 +157,8 @@ OrderBookDB::update(std::shared_ptr const& ledger) ++cnt; }; - addBook(issue1, issue2); - addBook(issue2, issue1); + addBook(asset1, asset2); + addBook(asset2, asset1); } } } @@ -179,19 +197,19 @@ OrderBookDB::addOrderBook(Book const& book) // return list of all orderbooks that want this issuerID and currencyID std::vector -OrderBookDB::getBooksByTakerPays(Issue const& issue) +OrderBookDB::getBooksByTakerPays(Asset const& asset) { std::vector ret; { std::lock_guard sl(mLock); - if (auto it = allBooks_.find(issue); it != allBooks_.end()) + if (auto it = allBooks_.find(asset); it != allBooks_.end()) { ret.reserve(it->second.size()); for (auto const& gets : it->second) - ret.push_back(Book(issue, gets)); + ret.push_back(Book(asset, gets)); } } @@ -199,19 +217,19 @@ OrderBookDB::getBooksByTakerPays(Issue const& issue) } int -OrderBookDB::getBookSize(Issue const& issue) +OrderBookDB::getBookSize(Asset const& asset) { std::lock_guard sl(mLock); - if (auto it = allBooks_.find(issue); it != allBooks_.end()) + if (auto it = allBooks_.find(asset); it != allBooks_.end()) return static_cast(it->second.size()); return 0; } bool -OrderBookDB::isBookToXRP(Issue const& issue) +OrderBookDB::isBookToXRP(Asset const& asset) { std::lock_guard sl(mLock); - return xrpBooks_.count(issue) > 0; + return xrpBooks_.count(asset) > 0; } BookListeners::pointer @@ -276,8 +294,8 @@ OrderBookDB::processTxn( data->isFieldPresent(sfTakerGets)) { auto listeners = getBookListeners( - {data->getFieldAmount(sfTakerGets).issue(), - data->getFieldAmount(sfTakerPays).issue()}); + {data->getFieldAmount(sfTakerGets).asset(), + data->getFieldAmount(sfTakerPays).asset()}); if (listeners) listeners->publish(jvObj, havePublished); } diff --git a/src/xrpld/app/ledger/OrderBookDB.h b/src/xrpld/app/ledger/OrderBookDB.h index ce0d9f0fafe..cbff92613bc 100644 --- a/src/xrpld/app/ledger/OrderBookDB.h +++ b/src/xrpld/app/ledger/OrderBookDB.h @@ -45,15 +45,15 @@ class OrderBookDB /** @return a list of all orderbooks that want this issuerID and currencyID. */ std::vector - getBooksByTakerPays(Issue const&); + getBooksByTakerPays(Asset const&); /** @return a count of all orderbooks that want this issuerID and currencyID. */ int - getBookSize(Issue const&); + getBookSize(Asset const&); bool - isBookToXRP(Issue const&); + isBookToXRP(Asset const&); BookListeners::pointer getBookListeners(Book const&); @@ -71,10 +71,10 @@ class OrderBookDB Application& app_; // Maps order books by "issue in" to "issue out": - hardened_hash_map> allBooks_; + hardened_hash_map> allBooks_; // does an order book to XRP exist - hash_set xrpBooks_; + hash_set xrpBooks_; std::recursive_mutex mLock; diff --git a/src/xrpld/app/misc/AMMHelpers.h b/src/xrpld/app/misc/AMMHelpers.h index 7ad0093a2e4..fe61f848799 100644 --- a/src/xrpld/app/misc/AMMHelpers.h +++ b/src/xrpld/app/misc/AMMHelpers.h @@ -226,7 +226,7 @@ getAMMOfferStartWithTakerGets( // Round downward to minimize the offer and to maximize the quality. // This has the most impact when takerGets is XRP. auto const takerGets = toAmount( - getIssue(pool.out), nTakerGetsProposed, Number::downward); + getAsset(pool.out), nTakerGetsProposed, Number::downward); return TAmounts{ swapAssetOut(pool, takerGets, tfee), takerGets}; }; @@ -297,7 +297,7 @@ getAMMOfferStartWithTakerPays( // Round downward to minimize the offer and to maximize the quality. // This has the most impact when takerPays is XRP. auto const takerPays = toAmount( - getIssue(pool.in), nTakerPaysProposed, Number::downward); + getAsset(pool.in), nTakerPaysProposed, Number::downward); return TAmounts{ takerPays, swapAssetIn(pool, takerPays, tfee)}; }; @@ -374,7 +374,7 @@ changeSpotPriceQuality( return std::nullopt; } auto const takerPays = - toAmount(getIssue(pool.in), nTakerPays, Number::upward); + toAmount(getAsset(pool.in), nTakerPays, Number::upward); // should not fail if (auto const amounts = TAmounts{ @@ -385,9 +385,9 @@ changeSpotPriceQuality( { JLOG(j.error()) << "changeSpotPriceQuality failed: " << to_string(pool.in) - << " " << to_string(pool.out) << " " << " " << quality - << " " << tfee << " " << to_string(amounts.in) << " " - << to_string(amounts.out); + << " " << to_string(pool.out) << " " + << " " << quality << " " << tfee << " " + << to_string(amounts.in) << " " << to_string(amounts.out); Throw("changeSpotPriceQuality failed"); } else @@ -409,7 +409,7 @@ changeSpotPriceQuality( // Generate the offer starting with XRP side. Return seated offer amounts // if the offer can be generated, otherwise nullopt. auto const amounts = [&]() { - if (isXRP(getIssue(pool.out))) + if (isXRP(getAsset(pool.out))) return getAMMOfferStartWithTakerGets(pool, quality, tfee); return getAMMOfferStartWithTakerPays(pool, quality, tfee); }(); @@ -501,7 +501,7 @@ swapAssetIn( auto const denom = pool.in + assetIn * (1 - fee); if (denom.signum() <= 0) - return toAmount(getIssue(pool.out), 0); + return toAmount(getAsset(pool.out), 0); Number::setround(Number::upward); auto const ratio = numerator / denom; @@ -510,14 +510,14 @@ swapAssetIn( auto const swapOut = pool.out - ratio; if (swapOut.signum() < 0) - return toAmount(getIssue(pool.out), 0); + return toAmount(getAsset(pool.out), 0); - return toAmount(getIssue(pool.out), swapOut, Number::downward); + return toAmount(getAsset(pool.out), swapOut, Number::downward); } else { return toAmount( - getIssue(pool.out), + getAsset(pool.out), pool.out - (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)), Number::downward); @@ -569,7 +569,7 @@ swapAssetOut( auto const denom = pool.out - assetOut; if (denom.signum() <= 0) { - return toMaxAmount(getIssue(pool.in)); + return toMaxAmount(getAsset(pool.in)); } Number::setround(Number::upward); @@ -583,14 +583,14 @@ swapAssetOut( Number::setround(Number::upward); auto const swapIn = numerator2 / feeMult; if (swapIn.signum() < 0) - return toAmount(getIssue(pool.in), 0); + return toAmount(getAsset(pool.in), 0); - return toAmount(getIssue(pool.in), swapIn, Number::upward); + return toAmount(getAsset(pool.in), swapIn, Number::upward); } else { return toAmount( - getIssue(pool.in), + getAsset(pool.in), ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) / feeMult(tfee), Number::upward); diff --git a/src/xrpld/app/misc/AMMUtils.h b/src/xrpld/app/misc/AMMUtils.h index 52fe819a28e..dea81ad5d9e 100644 --- a/src/xrpld/app/misc/AMMUtils.h +++ b/src/xrpld/app/misc/AMMUtils.h @@ -39,9 +39,10 @@ std::pair ammPoolHolds( ReadView const& view, AccountID const& ammAccountID, - Issue const& issue1, - Issue const& issue2, + Asset const& issue1, + Asset const& issue2, FreezeHandling freezeHandling, + AuthHandling authHandling, beast::Journal const j); /** Get AMM pool and LP token balances. If both optIssue are @@ -52,9 +53,10 @@ Expected, TER> ammHolds( ReadView const& view, SLE const& ammSle, - std::optional const& optIssue1, - std::optional const& optIssue2, + std::optional const& optIssue1, + std::optional const& optIssue2, FreezeHandling freezeHandling, + AuthHandling authHandling, beast::Journal const j); /** Get the balance of LP tokens. @@ -62,8 +64,8 @@ ammHolds( STAmount ammLPHolds( ReadView const& view, - Currency const& cur1, - Currency const& cur2, + Asset const& issue1, + Asset const& issue2, AccountID const& ammAccount, AccountID const& lpAccount, beast::Journal const j); @@ -91,7 +93,7 @@ STAmount ammAccountHolds( ReadView const& view, AccountID const& ammAccountID, - Issue const& issue); + Asset const& issue); /** Delete trustlines to AMM. If all trustlines are deleted then * AMM object and account are deleted. Otherwise tecIMPCOMPLETE is returned. @@ -99,8 +101,8 @@ ammAccountHolds( TER deleteAMMAccount( Sandbox& view, - Issue const& asset, - Issue const& asset2, + Asset const& issue, + Asset const& issue2, beast::Journal j); /** Initialize Auction and Voting slots and set the trading/discounted fee. diff --git a/src/xrpld/app/misc/MPTUtils.h b/src/xrpld/app/misc/MPTUtils.h new file mode 100644 index 00000000000..b9fa33da38b --- /dev/null +++ b/src/xrpld/app/misc/MPTUtils.h @@ -0,0 +1,52 @@ +//------------------------------------------------------------------------------ +/* + 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_APP_MISC_MPTUTILS_H_INLCUDED +#define RIPPLE_APP_MISC_MPTUTILS_H_INLCUDED + +#include +#include +#include + +namespace ripple { + +class Asset; +class ReadView; + +/* Return true if a transaction is allowed for the specified MPT/account. The + * function checks MPTokenIssuance and MPToken objects flags to determine if the + * transaction is allowed. + */ +TER +isMPTTxAllowed( + ReadView const& v, + TxType tx, + Asset const& asset, + AccountID const& accountID); + +TER +isMPTDEXAllowed( + ReadView const& view, + Asset const& issuanceID, + AccountID const& srcAccount, + AccountID const& destAccount); + +} // namespace ripple + +#endif // RIPPLE_APP_MISC_MPTUTILS_H_INLCUDED diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index e7432b4de0c..0dfe2cffe71 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -3020,13 +3020,14 @@ NetworkOPsImp::transJson( auto const amount = transaction->getFieldAmount(sfTakerGets); // If the offer create is not self funded then add the owner balance - if (account != amount.issue().account) + if (account != amount.getIssuer()) { auto const ownerFunds = accountFunds( *ledger, account, amount, fhIGNORE_FREEZE, + ahIGNORE_AUTH, app_.journal("View")); jvObj[jss::transaction][jss::owner_funds] = ownerFunds.getText(); } @@ -4151,8 +4152,8 @@ NetworkOPsImp::getBookPage( ReadView const& view = *lpLedger; - bool const bGlobalFreeze = isGlobalFrozen(view, book.out.account) || - isGlobalFrozen(view, book.in.account); + bool const bGlobalFreeze = isGlobalFrozen(view, book.out.getIssuer()) || + isGlobalFrozen(view, book.in.getIssuer()); bool bDone = false; bool bDirectAdvance = true; @@ -4162,7 +4163,7 @@ NetworkOPsImp::getBookPage( unsigned int uBookEntry; STAmount saDirRate; - auto const rate = transferRate(view, book.out.account); + auto const rate = transferRate(view, book.out.getIssuer()); auto viewJ = app_.journal("View"); while (!bDone && iLimit-- > 0) @@ -4210,7 +4211,7 @@ NetworkOPsImp::getBookPage( STAmount saOwnerFunds; bool firstOwnerOffer(true); - if (book.out.account == uOfferOwnerID) + if (book.out.getIssuer() == uOfferOwnerID) { // If an offer is selling issuer's own IOUs, it is fully // funded. @@ -4239,9 +4240,9 @@ NetworkOPsImp::getBookPage( saOwnerFunds = accountHolds( view, uOfferOwnerID, - book.out.currency, - book.out.account, + book.out, fhZERO_IF_FROZEN, + ahZERO_IF_UNAUTHORIZED, viewJ); if (saOwnerFunds < beast::zero) @@ -4261,9 +4262,9 @@ NetworkOPsImp::getBookPage( if (rate != parityRate // Have a tranfer fee. - && uTakerID != book.out.account + && uTakerID != book.out.getIssuer() // Not taking offers of own IOUs. - && book.out.account != uOfferOwnerID) + && book.out.getIssuer() != uOfferOwnerID) // Offer owner not issuing ownfunds { // Need to charge a transfer fee to offer owner. @@ -4286,7 +4287,7 @@ NetworkOPsImp::getBookPage( std::min( saTakerPays, multiply( - saTakerGetsFunded, saDirRate, saTakerPays.issue())) + saTakerGetsFunded, saDirRate, saTakerPays.asset())) .setJson(jvOffer[jss::taker_pays_funded]); } @@ -4437,7 +4438,7 @@ NetworkOPsImp::getBookPage( // going on here? std::min( saTakerPays, - multiply(saTakerGetsFunded, saDirRate, saTakerPays.issue())) + multiply(saTakerGetsFunded, saDirRate, saTakerPays.asset())) .setJson(jvOffer[jss::taker_pays_funded]); } diff --git a/src/xrpld/app/misc/detail/AMMHelpers.cpp b/src/xrpld/app/misc/detail/AMMHelpers.cpp index 4e23a607502..2da3790a6d0 100644 --- a/src/xrpld/app/misc/detail/AMMHelpers.cpp +++ b/src/xrpld/app/misc/detail/AMMHelpers.cpp @@ -49,7 +49,7 @@ lpTokensIn( Number const r = asset1Deposit / asset1Balance; auto const c = root2(f2 * f2 + r / f1) - f2; auto const t = lptAMMBalance * (r - c) / (1 + c); - return toSTAmount(lptAMMBalance.issue(), t); + return toSTAmount(lptAMMBalance.get(), t); } /* Equation 4 solves equation 3 for b: @@ -79,7 +79,7 @@ ammAssetIn( auto const b = 2 * d / t2 - 1 / f1; auto const c = d * d - f2 * f2; return toSTAmount( - asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c)); + asset1Balance.asset(), asset1Balance * solveQuadraticEq(a, b, c)); } /* Equation 7: diff --git a/src/xrpld/app/misc/detail/AMMUtils.cpp b/src/xrpld/app/misc/detail/AMMUtils.cpp index c91f0c56642..311364ba582 100644 --- a/src/xrpld/app/misc/detail/AMMUtils.cpp +++ b/src/xrpld/app/misc/detail/AMMUtils.cpp @@ -29,15 +29,16 @@ std::pair ammPoolHolds( ReadView const& view, AccountID const& ammAccountID, - Issue const& issue1, - Issue const& issue2, + Asset const& issue1, + Asset const& issue2, FreezeHandling freezeHandling, + AuthHandling authHandling, beast::Journal const j) { - auto const assetInBalance = - accountHolds(view, ammAccountID, issue1, freezeHandling, j); - auto const assetOutBalance = - accountHolds(view, ammAccountID, issue2, freezeHandling, j); + auto const assetInBalance = accountHolds( + view, ammAccountID, issue1, freezeHandling, authHandling, j); + auto const assetOutBalance = accountHolds( + view, ammAccountID, issue2, freezeHandling, authHandling, j); return std::make_pair(assetInBalance, assetOutBalance); } @@ -45,12 +46,13 @@ Expected, TER> ammHolds( ReadView const& view, SLE const& ammSle, - std::optional const& optIssue1, - std::optional const& optIssue2, + std::optional const& optIssue1, + std::optional const& optIssue2, FreezeHandling freezeHandling, + AuthHandling authHandling, beast::Journal const j) { - auto const issues = [&]() -> std::optional> { + auto const issues = [&]() -> std::optional> { auto const issue1 = ammSle[sfAsset]; auto const issue2 = ammSle[sfAsset2]; if (optIssue1 && optIssue2) @@ -71,8 +73,8 @@ ammHolds( } auto const singleIssue = [&issue1, &issue2, &j]( - Issue checkIssue, - const char* label) -> std::optional> { + Asset checkIssue, + const char* label) -> std::optional> { if (checkIssue == issue1) return std::make_optional(std::make_pair(issue1, issue2)); else if (checkIssue == issue2) @@ -97,21 +99,22 @@ ammHolds( }(); if (!issues) return Unexpected(tecAMM_INVALID_TOKENS); - auto const [asset1, asset2] = ammPoolHolds( + auto const [amount1, amount2] = ammPoolHolds( view, ammSle.getAccountID(sfAccount), issues->first, issues->second, freezeHandling, + authHandling, j); - return std::make_tuple(asset1, asset2, ammSle[sfLPTokenBalance]); + return std::make_tuple(amount1, amount2, ammSle[sfLPTokenBalance]); } STAmount ammLPHolds( ReadView const& view, - Currency const& cur1, - Currency const& cur2, + Asset const& issue1, + Asset const& issue2, AccountID const& ammAccount, AccountID const& lpAccount, beast::Journal const j) @@ -119,7 +122,7 @@ ammLPHolds( return accountHolds( view, lpAccount, - ammLPTCurrency(cur1, cur2), + ammLPTCurrency(issue1, issue2), ammAccount, FreezeHandling::fhZERO_IF_FROZEN, j); @@ -134,8 +137,8 @@ ammLPHolds( { return ammLPHolds( view, - ammSle[sfAsset].currency, - ammSle[sfAsset2].currency, + ammSle[sfAsset], + ammSle[sfAsset2], ammSle[sfAccount], lpAccount, j); @@ -177,22 +180,37 @@ STAmount ammAccountHolds( ReadView const& view, AccountID const& ammAccountID, - Issue const& issue) + Asset const& issue) { + if (issue.holds()) + return accountHolds( + view, + ammAccountID, + issue.get(), + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + beast::Journal(beast::Journal::getNullSink())); + // Should be accountHolds for Asset for both? if (isXRP(issue)) { if (auto const sle = view.read(keylet::account(ammAccountID))) return (*sle)[sfBalance]; } - else if (auto const sle = view.read( - keylet::line(ammAccountID, issue.account, issue.currency)); + else if (auto const sle = view.read(keylet::line( + ammAccountID, + issue.get().account, + issue.get().currency)); sle && - !isFrozen(view, ammAccountID, issue.currency, issue.account)) + !isFrozen( + view, + ammAccountID, + issue.get().currency, + issue.get().account)) { auto amount = (*sle)[sfBalance]; - if (ammAccountID > issue.account) + if (ammAccountID > issue.get().account) amount.negate(); - amount.setIssuer(issue.account); + amount.setIssuer(issue.get().account); return amount; } @@ -248,16 +266,16 @@ deleteAMMTrustLines( TER deleteAMMAccount( Sandbox& sb, - Issue const& asset, - Issue const& asset2, + Asset const& issue, + Asset const& issue2, beast::Journal j) { - auto ammSle = sb.peek(keylet::amm(asset, asset2)); + auto ammSle = sb.peek(keylet::amm(issue, issue2)); if (!ammSle) { // LCOV_EXCL_START JLOG(j.error()) << "deleteAMMAccount: AMM object does not exist " - << asset << " " << asset2; + << issue << " " << issue2; return tecINTERNAL; // LCOV_EXCL_STOP } @@ -278,6 +296,37 @@ deleteAMMAccount( ter != tesSUCCESS) return ter; + auto checkDeleteMPToken = [&](Asset const& issue_) -> TER { + if (issue_.holds()) + { + auto const mptIssuanceID = + keylet::mptIssuance(issue_.get().getMptID()); + auto const mptokenKey = + keylet::mptoken(mptIssuanceID.key, ammAccountID); + + auto const sleMpt = sb.peek(mptokenKey); + if (!sleMpt) + return tecINTERNAL; + + if (!sb.dirRemove( + keylet::ownerDir(ammAccountID), + (*sleMpt)[sfOwnerNode], + sleMpt->key(), + false)) + return tecINTERNAL; + + sb.erase(sleMpt); + } + + return tesSUCCESS; + }; + + if (auto const err = checkDeleteMPToken(issue)) + return err; + + if (auto const err = checkDeleteMPToken(issue2)) + return err; + auto const ownerDirKeylet = keylet::ownerDir(ammAccountID); if (!sb.dirRemove( ownerDirKeylet, (*ammSle)[sfOwnerNode], ammSle->key(), false)) diff --git a/src/xrpld/app/misc/detail/MPTUtils.cpp b/src/xrpld/app/misc/detail/MPTUtils.cpp new file mode 100644 index 00000000000..e57ae6da800 --- /dev/null +++ b/src/xrpld/app/misc/detail/MPTUtils.cpp @@ -0,0 +1,105 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include + +namespace ripple { + +static TER +isMPTAllowed( + ReadView const& view, + TxType txType, + Asset const& asset, + AccountID const& accountID, + std::optional const& destAccount) +{ + if (!asset.holds()) + return tesSUCCESS; + + auto const& issuanceID = asset.get().getMptID(); + auto const isDEX = txType == ttPAYMENT && destAccount; + auto const ammTx = txType == ttAMM_CREATE || txType == ttAMM_DEPOSIT || + txType == ttAMM_WITHDRAW; + auto const allMPTTx = ammTx || txType == ttOFFER_CREATE || + txType == ttCHECK_CREATE || txType == ttCHECK_CASH || + txType == ttPAYMENT; + assert(allMPTTx || isDEX); + + auto const issuanceKey = keylet::mptIssuance(issuanceID); + auto const issuanceSle = view.read(issuanceKey); + if (!issuanceSle) + return tecOBJECT_NOT_FOUND; + + auto const& issuer = asset.getIssuer(); + auto const flags = issuanceSle->getFlags(); + + if (flags & lsfMPTLocked) + return tecNO_PERMISSION; + // Offer crossing and Payment + if ((flags & lsfMPTCanTrade) == 0 && isDEX) + return tecNO_PERMISSION; + if ((flags & lsfMPTCanClawback) && txType == ttAMM_CREATE) + return tecNO_PERMISSION; + + if (accountID != issuer) + { + if ((flags & lsfMPTCanTransfer) == 0) + return tecNO_PERMISSION; + + auto const mptSle = + view.read(keylet::mptoken(issuanceKey.key, accountID)); + // Allow to succeed since some tx create MPToken if it doesn't exist. + // Tx's have their own check for missing MPToken. + if (!mptSle) + return tesSUCCESS; + + if ((mptSle->getFlags() & lsfMPTLocked) && destAccount != issuer) + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +TER +isMPTTxAllowed( + ReadView const& view, + TxType txType, + Asset const& asset, + AccountID const& accountID) +{ + // use isDEXAllowed for payment/offer crossing + assert(txType != ttPAYMENT); + return isMPTAllowed(view, txType, asset, accountID, std::nullopt); +} + +TER +isMPTDEXAllowed( + ReadView const& view, + Asset const& asset, + AccountID const& accountID, + AccountID const& dest) +{ + // use ttPAYMENT for both offer crossing and payment + return isMPTAllowed(view, ttPAYMENT, asset, accountID, dest); +} + +} // namespace ripple diff --git a/src/xrpld/app/paths/AMMLiquidity.h b/src/xrpld/app/paths/AMMLiquidity.h index fe60d39262f..9c4a4ca4675 100644 --- a/src/xrpld/app/paths/AMMLiquidity.h +++ b/src/xrpld/app/paths/AMMLiquidity.h @@ -26,12 +26,13 @@ #include #include #include +#include #include #include namespace ripple { -template +template class AMMOffer; /** AMMLiquidity class provides AMM offers to BookStep class. @@ -56,8 +57,8 @@ class AMMLiquidity AMMContext& ammContext_; AccountID const ammAccountID_; std::uint32_t const tradingFee_; - Issue const issueIn_; - Issue const issueOut_; + Asset const assetIn_; + Asset const assetOut_; // Initial AMM pool balances TAmounts const initialBalances_; beast::Journal const j_; @@ -67,8 +68,8 @@ class AMMLiquidity ReadView const& view, AccountID const& ammAccountID, std::uint32_t tradingFee, - Issue const& in, - Issue const& out, + Asset const& in, + Asset const& out, AMMContext& ammContext, beast::Journal j); ~AMMLiquidity() = default; @@ -109,16 +110,16 @@ class AMMLiquidity return ammContext_; } - Issue const& - issueIn() const + Asset const& + assetIn() const { - return issueIn_; + return assetIn_; } - Issue const& - issueOut() const + Asset const& + assetOut() const { - return issueOut_; + return assetOut_; } private: diff --git a/src/xrpld/app/paths/AMMOffer.h b/src/xrpld/app/paths/AMMOffer.h index e90a5b8611f..f6de0ccd9f0 100644 --- a/src/xrpld/app/paths/AMMOffer.h +++ b/src/xrpld/app/paths/AMMOffer.h @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -35,7 +36,7 @@ class QualityFunction; * methods for use in generic BookStep methods. AMMOffer amounts * are changed indirectly in BookStep limiting steps. */ -template +template class AMMOffer { private: @@ -71,8 +72,11 @@ class AMMOffer return quality_; } - Issue const& - issueIn() const; + Asset const& + assetIn() const; + + Asset const& + assetOut() const; AccountID const& owner() const; diff --git a/src/xrpld/app/paths/AccountCurrencies.cpp b/src/xrpld/app/paths/AccountCurrencies.cpp index 8646b46939a..434c0421c2c 100644 --- a/src/xrpld/app/paths/AccountCurrencies.cpp +++ b/src/xrpld/app/paths/AccountCurrencies.cpp @@ -24,7 +24,7 @@ namespace ripple { hash_set accountSourceCurrencies( AccountID const& account, - std::shared_ptr const& lrCache, + std::shared_ptr const& lrCache, bool includeXRP) { hash_set currencies; @@ -60,7 +60,7 @@ accountSourceCurrencies( hash_set accountDestCurrencies( AccountID const& account, - std::shared_ptr const& lrCache, + std::shared_ptr const& lrCache, bool includeXRP) { hash_set currencies; diff --git a/src/xrpld/app/paths/AccountCurrencies.h b/src/xrpld/app/paths/AccountCurrencies.h index 26282e742c3..debc9bf6a94 100644 --- a/src/xrpld/app/paths/AccountCurrencies.h +++ b/src/xrpld/app/paths/AccountCurrencies.h @@ -20,7 +20,7 @@ #ifndef RIPPLE_APP_PATHS_ACCOUNTCURRENCIES_H_INCLUDED #define RIPPLE_APP_PATHS_ACCOUNTCURRENCIES_H_INCLUDED -#include +#include #include namespace ripple { @@ -28,13 +28,13 @@ namespace ripple { hash_set accountDestCurrencies( AccountID const& account, - std::shared_ptr const& cache, + std::shared_ptr const& cache, bool includeXRP); hash_set accountSourceCurrencies( AccountID const& account, - std::shared_ptr const& lrLedger, + std::shared_ptr const& lrLedger, bool includeXRP); } // namespace ripple diff --git a/src/xrpld/app/paths/RippleLineCache.cpp b/src/xrpld/app/paths/AssetCache.cpp similarity index 81% rename from src/xrpld/app/paths/RippleLineCache.cpp rename to src/xrpld/app/paths/AssetCache.cpp index 053546ce0be..35e5a73443f 100644 --- a/src/xrpld/app/paths/RippleLineCache.cpp +++ b/src/xrpld/app/paths/AssetCache.cpp @@ -17,13 +17,13 @@ */ //============================================================================== -#include +#include #include #include namespace ripple { -RippleLineCache::RippleLineCache( +AssetCache::AssetCache( std::shared_ptr const& ledger, beast::Journal j) : ledger_(ledger), journal_(j) @@ -31,7 +31,7 @@ RippleLineCache::RippleLineCache( JLOG(journal_.debug()) << "created for ledger " << ledger_->info().seq; } -RippleLineCache::~RippleLineCache() +AssetCache::~AssetCache() { JLOG(journal_.debug()) << "destroyed for ledger " << ledger_->info().seq << " with " << lines_.size() << " accounts and " @@ -39,9 +39,7 @@ RippleLineCache::~RippleLineCache() } std::shared_ptr> -RippleLineCache::getRippleLines( - AccountID const& accountID, - LineDirection direction) +AssetCache::getRippleLines(AccountID const& accountID, LineDirection direction) { auto const hash = hasher_(accountID); AccountKey key(accountID, direction, hash); @@ -79,9 +77,7 @@ RippleLineCache::getRippleLines( // to be replaced by the full set. The full set will be built // below, and will be returned, if needed, on subsequent calls // for either value of outgoing. - ASSERT( - size <= totalLineCount_, - "ripple::RippleLineCache::getRippleLines : maximum lines"); + assert(size <= totalLineCount_); totalLineCount_ -= size; lines_.erase(otheriter); } @@ -101,9 +97,7 @@ RippleLineCache::getRippleLines( if (inserted) { - ASSERT( - it->second == nullptr, - "ripple::RippleLineCache::getRippleLines : null lines"); + assert(it->second == nullptr); auto lines = PathFindTrustLine::getItems(accountID, *ledger_, direction); if (lines.size()) @@ -114,9 +108,7 @@ RippleLineCache::getRippleLines( } } - ASSERT( - !it->second || (it->second->size() > 0), - "ripple::RippleLineCache::getRippleLines : null or nonempty lines"); + assert(!it->second || (it->second->size() > 0)); auto const size = it->second ? it->second->size() : 0; JLOG(journal_.trace()) << "getRippleLines for ledger " << ledger_->info().seq << " found " << size @@ -131,4 +123,32 @@ RippleLineCache::getRippleLines( return it->second; } +std::shared_ptr> const& +AssetCache::getMPTs(const ripple::AccountID& account) +{ + std::lock_guard sl(mLock); + + if (auto it = mpts_.find(account); it != mpts_.end()) + return it->second; + + std::vector mpts; + // Get issued/authorized tokens + forEachItem(*ledger_, account, [&](std::shared_ptr const& sle) { + if (sle->getType() == ltMPTOKEN_ISSUANCE) + mpts.push_back(makeMptID(sle->getFieldU32(sfSequence), account)); + else if (sle->getType() == ltMPTOKEN) + mpts.push_back(sle->getFieldH192(sfMPTokenIssuanceID)); + }); + + totalMPTCount_ += mpts.size(); + + if (mpts.empty()) + mpts_.emplace(account, nullptr); + else + mpts_.emplace( + account, std::make_shared>(std::move(mpts))); + + return mpts_[account]; +} + } // namespace ripple diff --git a/src/xrpld/app/paths/RippleLineCache.h b/src/xrpld/app/paths/AssetCache.h similarity index 93% rename from src/xrpld/app/paths/RippleLineCache.h rename to src/xrpld/app/paths/AssetCache.h index cde1d589f92..056bdc52afd 100644 --- a/src/xrpld/app/paths/RippleLineCache.h +++ b/src/xrpld/app/paths/AssetCache.h @@ -33,13 +33,13 @@ namespace ripple { // Used by Pathfinder -class RippleLineCache final : public CountedObject +class AssetCache final : public CountedObject { public: - explicit RippleLineCache( + explicit AssetCache( std::shared_ptr const& l, beast::Journal j); - ~RippleLineCache(); + ~AssetCache(); std::shared_ptr const& getLedger() const @@ -62,6 +62,9 @@ class RippleLineCache final : public CountedObject std::shared_ptr> getRippleLines(AccountID const& accountID, LineDirection direction); + std::shared_ptr> const& + getMPTs(AccountID const& account); + private: std::mutex mLock; @@ -125,6 +128,8 @@ class RippleLineCache final : public CountedObject AccountKey::Hash> lines_; std::size_t totalLineCount_ = 0; + hash_map>> mpts_; + std::size_t totalMPTCount_ = 0; }; } // namespace ripple diff --git a/src/xrpld/app/paths/Flow.cpp b/src/xrpld/app/paths/Flow.cpp index 23d9da64150..b406a0d3339 100644 --- a/src/xrpld/app/paths/Flow.cpp +++ b/src/xrpld/app/paths/Flow.cpp @@ -38,8 +38,8 @@ template static auto finishFlow( PaymentSandbox& sb, - Issue const& srcIssue, - Issue const& dstIssue, + Asset const& srcAsset, + Asset const& dstAsset, FlowResult&& f) { path::RippleCalc::Output result; @@ -49,8 +49,8 @@ finishFlow( result.removableOffers = std::move(f.removableOffers); result.setResult(f.ter); - result.actualAmountIn = toSTAmount(f.in, srcIssue); - result.actualAmountOut = toSTAmount(f.out, dstIssue); + result.actualAmountIn = toSTAmount(f.in, srcAsset); + result.actualAmountOut = toSTAmount(f.out, dstAsset); return result; }; @@ -71,19 +71,21 @@ flow( beast::Journal j, path::detail::FlowDebugInfo* flowDebugInfo) { - Issue const srcIssue = [&] { + Asset const srcAsset = [&]() -> Asset { if (sendMax) - return sendMax->issue(); - if (!isXRP(deliver.issue().currency)) - return Issue(deliver.issue().currency, src); - return xrpIssue(); + return sendMax->asset(); + if (isXRP(deliver)) + return xrpIssue(); + if (deliver.holds()) + return Issue(deliver.get().currency, src); + return deliver.asset(); }(); - Issue const dstIssue = deliver.issue(); + Asset const dstAsset = deliver.asset(); - std::optional sendMaxIssue; + std::optional sendMaxAsset; if (sendMax) - sendMaxIssue = sendMax->issue(); + sendMaxAsset = sendMax->asset(); AMMContext ammContext(src, false); @@ -94,9 +96,9 @@ flow( sb, src, dst, - dstIssue, + dstAsset, limitQuality, - sendMaxIssue, + sendMaxAsset, paths, defaultPaths, ownerPaysTransferFee, @@ -116,7 +118,7 @@ flow( if (j.trace()) { j.trace() << "\nsrc: " << src << "\ndst: " << dst - << "\nsrcIssue: " << srcIssue << "\ndstIssue: " << dstIssue; + << "\nsrcAsset: " << srcAsset << "\ndstAsset: " << dstAsset; j.trace() << "\nNumStrands: " << strands.size(); for (auto const& curStrand : strands) { @@ -128,87 +130,43 @@ flow( } } - const bool srcIsXRP = isXRP(srcIssue.currency); - const bool dstIsXRP = isXRP(dstIssue.currency); - - auto const asDeliver = toAmountSpec(deliver); - - // The src account may send either xrp or iou. The dst account may receive - // either xrp or iou. Since XRP and IOU amounts are represented by different - // types, use templates to tell `flow` about the amount types. - if (srcIsXRP && dstIsXRP) - { - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( - sb, - strands, - asDeliver.xrp, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); - } - - if (srcIsXRP && !dstIsXRP) - { - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( + using Var = + std::variant; + auto getTypedAmt = [&](Asset const& iss) -> Var { + static auto xrp = XRPAmount{}; + static auto mpt = MPTAmount{}; + static auto iou = IOUAmount{}; + if (isXRP(iss)) + return &xrp; + if (iss.holds()) + return &mpt; + return &iou; + }; + + // The src account may send either xrp,iou,mpt. The dst account may receive + // either xrp,iou,mpt. Since XRP, IOU, and MPT amounts are represented by + // different types, use templates to tell `flow` about the amount types. + return std::visit( + [&, &strands_ = strands]( + TIn const*&&, TOut const*&&) { + return finishFlow( sb, - strands, - asDeliver.iou, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); - } - - if (!srcIsXRP && dstIsXRP) - { - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( - sb, - strands, - asDeliver.xrp, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); - } - - ASSERT(!srcIsXRP && !dstIsXRP, "ripple::flow : neither is XRP"); - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( - sb, - strands, - asDeliver.iou, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); + srcAsset, + dstAsset, + flow( + sb, + strands_, + get(deliver), + partialPayment, + offerCrossing, + limitQuality, + sendMax, + j, + ammContext, + flowDebugInfo)); + }, + getTypedAmt(srcAsset), + getTypedAmt(dstAsset)); } } // namespace ripple diff --git a/src/xrpld/app/paths/PathRequest.cpp b/src/xrpld/app/paths/PathRequest.cpp index 28ade441c2d..63dbe67c556 100644 --- a/src/xrpld/app/paths/PathRequest.cpp +++ b/src/xrpld/app/paths/PathRequest.cpp @@ -169,7 +169,7 @@ PathRequest::updateComplete() } bool -PathRequest::isValid(std::shared_ptr const& crCache) +PathRequest::isValid(std::shared_ptr const& crCache) { if (!raSrcAccount || !raDstAccount) return false; @@ -222,6 +222,13 @@ PathRequest::isValid(std::shared_ptr const& crCache) for (auto const& currency : usDestCurrID) jvDestCur.append(to_string(currency)); + + if (auto mpts = crCache->getMPTs(*raDstAccount)) + { + for (auto const& mpt : *mpts) + jvDestCur.append(to_string(mpt)); + } + jvStatus[jss::destination_tag] = (sleDest->getFlags() & lsfRequireDestTag); } @@ -242,7 +249,7 @@ PathRequest::isValid(std::shared_ptr const& crCache) */ std::pair PathRequest::doCreate( - std::shared_ptr const& cache, + std::shared_ptr const& cache, Json::Value const& value) { bool valid = false; @@ -313,11 +320,9 @@ PathRequest::parseJson(Json::Value const& jvParams) return PFR_PJ_INVALID; } - convert_all_ = saDstAmount == STAmount(saDstAmount.issue(), 1u, 0, true); + convert_all_ = saDstAmount == STAmount(saDstAmount.asset(), 1u, 0, true); - if ((saDstAmount.getCurrency().isZero() && - saDstAmount.getIssuer().isNonZero()) || - (saDstAmount.getCurrency() == badCurrency()) || + if (!validAsset(saDstAmount.asset()) || (!convert_all_ && saDstAmount <= beast::zero)) { jvStatus = rpcError(rpcDST_AMT_MALFORMED); @@ -335,11 +340,9 @@ PathRequest::parseJson(Json::Value const& jvParams) saSendMax.emplace(); if (!amountFromJsonNoThrow(*saSendMax, jvParams[jss::send_max]) || - (saSendMax->getCurrency().isZero() && - saSendMax->getIssuer().isNonZero()) || - (saSendMax->getCurrency() == badCurrency()) || + !validAsset(saSendMax->asset()) || (*saSendMax <= beast::zero && - *saSendMax != STAmount(saSendMax->issue(), 1u, 0, true))) + *saSendMax != STAmount(saSendMax->asset(), 1u, 0, true))) { jvStatus = rpcError(rpcSENDMAX_MALFORMED); return PFR_PJ_INVALID; @@ -356,47 +359,72 @@ PathRequest::parseJson(Json::Value const& jvParams) return PFR_PJ_INVALID; } - sciSourceCurrencies.clear(); + sciSourceAssets.clear(); for (auto const& c : jvSrcCurrencies) { - // Mandatory currency - Currency srcCurrencyID; - if (!c.isObject() || !c.isMember(jss::currency) || - !c[jss::currency].isString() || - !to_currency(srcCurrencyID, c[jss::currency].asString())) + // Mandatory currency or MPT + if (!validJSONAsset(c) || !c.isObject()) { jvStatus = rpcError(rpcSRC_CUR_MALFORMED); return PFR_PJ_INVALID; } + PathAsset srcPathAsset; + if (c.isMember(jss::currency)) + { + Currency currency; + if (!c[jss::currency].isString() || + !to_currency(currency, c[jss::currency].asString())) + { + jvStatus = rpcError(rpcSRC_CUR_MALFORMED); + return PFR_PJ_INVALID; + } + srcPathAsset = currency; + } + else + { + uint192 u; + if (!c[jss::mpt_issuance_id].isString() || + !u.parseHex(c[jss::mpt_issuance_id].asString())) + { + jvStatus = rpcError(rpcSRC_CUR_MALFORMED); + return PFR_PJ_INVALID; + } + srcPathAsset = u; + } + // Optional issuer AccountID srcIssuerID; if (c.isMember(jss::issuer) && - (!c[jss::issuer].isString() || + (c.isMember(jss::mpt_issuance_id) || + !c[jss::issuer].isString() || !to_issuer(srcIssuerID, c[jss::issuer].asString()))) { jvStatus = rpcError(rpcSRC_ISR_MALFORMED); return PFR_PJ_INVALID; } - if (srcCurrencyID.isZero()) + if (srcPathAsset.holds()) { - if (srcIssuerID.isNonZero()) + if (srcPathAsset.get().isZero()) { - jvStatus = rpcError(rpcSRC_CUR_MALFORMED); - return PFR_PJ_INVALID; + if (srcIssuerID.isNonZero()) + { + jvStatus = rpcError(rpcSRC_CUR_MALFORMED); + return PFR_PJ_INVALID; + } + } + else if (srcIssuerID.isZero()) + { + srcIssuerID = *raSrcAccount; } - } - else if (srcIssuerID.isZero()) - { - srcIssuerID = *raSrcAccount; } if (saSendMax) { - // If the currencies don't match, ignore the source currency. - if (srcCurrencyID == saSendMax->getCurrency()) + // If the assets don't match, ignore the source asset. + if (equalAssets(srcPathAsset, saSendMax->asset())) { // If neither is the source and they are not equal, then the // source issuer is illegal. @@ -410,26 +438,37 @@ PathRequest::parseJson(Json::Value const& jvParams) // If both are the source, use the source. // Otherwise, use the one that's not the source. - if (srcIssuerID != *raSrcAccount) + if (srcPathAsset.holds()) { - sciSourceCurrencies.insert( - {srcCurrencyID, srcIssuerID}); - } - else if (saSendMax->getIssuer() != *raSrcAccount) - { - sciSourceCurrencies.insert( - {srcCurrencyID, saSendMax->getIssuer()}); + if (srcIssuerID != *raSrcAccount) + { + sciSourceAssets.insert(Issue{ + srcPathAsset.get(), srcIssuerID}); + } + else if (saSendMax->getIssuer() != *raSrcAccount) + { + sciSourceAssets.insert(Issue{ + srcPathAsset.get(), + saSendMax->getIssuer()}); + } + else + { + sciSourceAssets.insert(Issue{ + srcPathAsset.get(), *raSrcAccount}); + } } else - { - sciSourceCurrencies.insert( - {srcCurrencyID, *raSrcAccount}); - } + sciSourceAssets.insert(srcPathAsset.get()); } } + else if (srcPathAsset.holds()) + { + sciSourceAssets.insert( + Issue{srcPathAsset.get(), srcIssuerID}); + } else { - sciSourceCurrencies.insert({srcCurrencyID, srcIssuerID}); + sciSourceAssets.insert(MPTIssue{srcPathAsset.get()}); } } } @@ -465,21 +504,21 @@ PathRequest::doAborting() const std::unique_ptr const& PathRequest::getPathFinder( - std::shared_ptr const& cache, - hash_map>& currency_map, - Currency const& currency, + std::shared_ptr const& cache, + hash_map>& pathasset_map, + PathAsset const& asset, STAmount const& dst_amount, int const level, std::function const& continueCallback) { - auto i = currency_map.find(currency); - if (i != currency_map.end()) + auto i = pathasset_map.find(asset); + if (i != pathasset_map.end()) return i->second; auto pathfinder = std::make_unique( cache, *raSrcAccount, *raDstAccount, - currency, + asset, std::nullopt, dst_amount, saSendMax, @@ -488,51 +527,60 @@ PathRequest::getPathFinder( pathfinder->computePathRanks(max_paths_, continueCallback); else pathfinder.reset(); // It's a bad request - clear it. - return currency_map[currency] = std::move(pathfinder); + return pathasset_map[asset] = std::move(pathfinder); } bool PathRequest::findPaths( - std::shared_ptr const& cache, + std::shared_ptr const& cache, int const level, Json::Value& jvArray, std::function const& continueCallback) { - auto sourceCurrencies = sciSourceCurrencies; - if (sourceCurrencies.empty() && saSendMax) + auto sourceAssets = sciSourceAssets; + if (sourceAssets.empty() && saSendMax) { - sourceCurrencies.insert(saSendMax->issue()); + sourceAssets.insert(saSendMax->asset()); } - if (sourceCurrencies.empty()) + if (sourceAssets.empty()) { auto currencies = accountSourceCurrencies(*raSrcAccount, cache, true); bool const sameAccount = *raSrcAccount == *raDstAccount; for (auto const& c : currencies) { - if (!sameAccount || c != saDstAmount.getCurrency()) + if (!sameAccount || + (saDstAmount.holds() && + c != saDstAmount.get().currency)) { - if (sourceCurrencies.size() >= RPC::Tuning::max_auto_src_cur) + if (sourceAssets.size() >= RPC::Tuning::max_auto_src_cur) return false; - sourceCurrencies.insert( - {c, c.isZero() ? xrpAccount() : *raSrcAccount}); + sourceAssets.insert( + Issue{c, c.isZero() ? xrpAccount() : *raSrcAccount}); } } + if (auto mpts = cache->getMPTs(*raSrcAccount)) + { + if (sourceAssets.size() >= RPC::Tuning::max_auto_src_cur) + return false; + for (auto const& mpt : *mpts) + sourceAssets.insert(mpt); + } } auto const dst_amount = convertAmount(saDstAmount, convert_all_); - hash_map> currency_map; - for (auto const& issue : sourceCurrencies) + hash_map> pathasset_map; + for (auto const& asset : sourceAssets) { if (continueCallback && !continueCallback()) break; JLOG(m_journal.debug()) << iIdentifier - << " Trying to find paths: " << STAmount(issue, 1).getFullText(); + << " Trying to find paths: " << STAmount(asset, 1).getFullText(); auto& pathfinder = getPathFinder( cache, - currency_map, - issue.currency, + pathasset_map, + PathAsset::toPathAsset(asset), dst_amount, level, continueCallback); @@ -546,23 +594,32 @@ PathRequest::findPaths( auto ps = pathfinder->getBestPaths( max_paths_, fullLiquidityPath, - mContext[issue], - issue.account, + mContext[asset], + asset.getIssuer(), continueCallback); - mContext[issue] = ps; + mContext[asset] = ps; auto const& sourceAccount = [&] { - if (!isXRP(issue.account)) - return issue.account; + if (!isXRP(asset.getIssuer())) + return asset.getIssuer(); - if (isXRP(issue.currency)) + if (isXRP(asset)) return xrpAccount(); return *raSrcAccount; }(); - STAmount saMaxAmount = saSendMax.value_or( - STAmount(Issue{issue.currency, sourceAccount}, 1u, 0, true)); + STAmount saMaxAmount = [&]() { + if (saSendMax) + return *saSendMax; + if (asset.holds()) + return STAmount( + Issue{asset.get().currency, sourceAccount}, + 1u, + 0, + true); + return STAmount(asset.get(), 1u, 0, true); + }(); JLOG(m_journal.debug()) << iIdentifier << " Paths found, calling rippleCalc"; @@ -619,7 +676,9 @@ PathRequest::findPaths( if (rc.result() == tesSUCCESS) { Json::Value jvEntry(Json::objectValue); - rc.actualAmountIn.setIssuer(sourceAccount); + // TODO MPT + if (rc.actualAmountIn.holds()) + rc.actualAmountIn.setIssuer(sourceAccount); jvEntry[jss::source_amount] = rc.actualAmountIn.getJson(JsonOptions::none); jvEntry[jss::paths_computed] = ps.getJson(JsonOptions::none); @@ -647,14 +706,14 @@ PathRequest::findPaths( The minimum cost is 50 and the maximum is 400. The cost increases after four source currencies, 50 - (4 * 4) = 34. */ - int const size = sourceCurrencies.size(); + int const size = sourceAssets.size(); consumer_.charge({std::clamp(size * size + 34, 50, 400), "path update"}); return true; } Json::Value PathRequest::doUpdate( - std::shared_ptr const& cache, + std::shared_ptr const& cache, bool fast, std::function const& continueCallback) { @@ -674,11 +733,16 @@ PathRequest::doUpdate( if (hasCompletion()) { // Old ripple_path_find API gives destination_currencies - auto& destCurrencies = + auto& destAssets = (newStatus[jss::destination_currencies] = Json::arrayValue); - auto usCurrencies = accountDestCurrencies(*raDstAccount, cache, true); - for (auto const& c : usCurrencies) - destCurrencies.append(to_string(c)); + auto usAssets = accountDestCurrencies(*raDstAccount, cache, true); + for (auto const& c : usAssets) + destAssets.append(to_string(c)); + if (auto mpts = cache->getMPTs(*raDstAccount)) + { + for (auto const& mpt : *mpts) + destAssets.append(to_string(mpt)); + } } newStatus[jss::source_account] = toBase58(*raSrcAccount); diff --git a/src/xrpld/app/paths/PathRequest.h b/src/xrpld/app/paths/PathRequest.h index 21f10d066ba..534b1392946 100644 --- a/src/xrpld/app/paths/PathRequest.h +++ b/src/xrpld/app/paths/PathRequest.h @@ -21,10 +21,11 @@ #define RIPPLE_APP_PATHS_PATHREQUEST_H_INCLUDED #include +#include #include -#include #include #include +#include #include #include #include @@ -37,7 +38,7 @@ namespace ripple { // A pathfinding request submitted by a client // The request issuer must maintain a strong pointer -class RippleLineCache; +class AssetCache; class PathRequests; // Return values from parseJson <0 = invalid, >0 = valid @@ -86,7 +87,7 @@ class PathRequest final : public InfoSubRequest, updateComplete(); std::pair - doCreate(std::shared_ptr const&, Json::Value const&); + doCreate(std::shared_ptr const&, Json::Value const&); Json::Value doClose() override; @@ -98,7 +99,7 @@ class PathRequest final : public InfoSubRequest, // update jvStatus Json::Value doUpdate( - std::shared_ptr const&, + std::shared_ptr const&, bool fast, std::function const& continueCallback = {}); InfoSub::pointer @@ -108,13 +109,13 @@ class PathRequest final : public InfoSubRequest, private: bool - isValid(std::shared_ptr const& crCache); + isValid(std::shared_ptr const& crCache); std::unique_ptr const& getPathFinder( - std::shared_ptr const&, - hash_map>&, - Currency const&, + std::shared_ptr const&, + hash_map>&, + PathAsset const&, STAmount const&, int const, std::function const&); @@ -124,7 +125,7 @@ class PathRequest final : public InfoSubRequest, */ bool findPaths( - std::shared_ptr const&, + std::shared_ptr const&, int const, Json::Value&, std::function const&); @@ -152,8 +153,8 @@ class PathRequest final : public InfoSubRequest, STAmount saDstAmount; std::optional saSendMax; - std::set sciSourceCurrencies; - std::map mContext; + std::set sciSourceAssets; + std::map mContext; bool convert_all_; diff --git a/src/xrpld/app/paths/PathRequests.cpp b/src/xrpld/app/paths/PathRequests.cpp index 86560445ec7..8888096ff04 100644 --- a/src/xrpld/app/paths/PathRequests.cpp +++ b/src/xrpld/app/paths/PathRequests.cpp @@ -30,21 +30,22 @@ namespace ripple { -/** Get the current RippleLineCache, updating it if necessary. +/** Get the current AssetCache, updating it if necessary. Get the correct ledger to use. */ -std::shared_ptr -PathRequests::getLineCache( +std::shared_ptr +PathRequests::getAssetCache( std::shared_ptr const& ledger, bool authoritative) { std::lock_guard sl(mLock); - auto lineCache = lineCache_.lock(); + auto assetCache = assetCache_.lock(); - std::uint32_t const lineSeq = lineCache ? lineCache->getLedger()->seq() : 0; + std::uint32_t const lineSeq = + assetCache ? assetCache->getLedger()->seq() : 0; std::uint32_t const lgrSeq = ledger->seq(); - JLOG(mJournal.debug()) << "getLineCache has cache for " << lineSeq + JLOG(mJournal.debug()) << "getAssetCache has cache for " << lineSeq << ", considering " << lgrSeq; if ((lineSeq == 0) || // no ledger @@ -54,14 +55,14 @@ PathRequests::getLineCache( (lgrSeq > (lineSeq + 8))) // we jumped way forward for some reason { JLOG(mJournal.debug()) - << "getLineCache creating new cache for " << lgrSeq; + << "getAssetCache creating new cache for " << lgrSeq; // Assign to the local before the member, because the member is a // weak_ptr, and will immediately discard it if there are no other // references. - lineCache_ = lineCache = std::make_shared( - ledger, app_.journal("RippleLineCache")); + assetCache_ = assetCache = + std::make_shared(ledger, app_.journal("AssetCache")); } - return lineCache; + return assetCache; } void @@ -71,13 +72,13 @@ PathRequests::updateAll(std::shared_ptr const& inLedger) app_.getJobQueue().makeLoadEvent(jtPATH_FIND, "PathRequest::updateAll"); std::vector requests; - std::shared_ptr cache; + std::shared_ptr cache; // Get the ledger and cache we should be using { std::lock_guard sl(mLock); requests = requests_; - cache = getLineCache(inLedger, true); + cache = getAssetCache(inLedger, true); } bool newRequests = app_.getLedgerMaster().isNewPathRequest(); @@ -202,7 +203,7 @@ PathRequests::updateAll(std::shared_ptr const& inLedger) // Hold on to the line cache until after the lock is released, so it can // be destroyed outside of the lock - std::shared_ptr lastCache; + std::shared_ptr lastCache; { // Get the latest requests, cache, and ledger for next pass std::lock_guard sl(mLock); @@ -211,7 +212,7 @@ PathRequests::updateAll(std::shared_ptr const& inLedger) break; requests = requests_; lastCache = cache; - cache = getLineCache(cache->getLedger(), false); + cache = getAssetCache(cache->getLedger(), false); } } while (!app_.getJobQueue().isStopping()); @@ -255,7 +256,7 @@ PathRequests::makePathRequest( app_, subscriber, ++mLastIdentifier, *this, mJournal); auto [valid, jvRes] = - req->doCreate(getLineCache(inLedger, false), requestJson); + req->doCreate(getAssetCache(inLedger, false), requestJson); if (valid) { @@ -280,7 +281,8 @@ PathRequests::makeLegacyPathRequest( req = std::make_shared( app_, completion, consumer, ++mLastIdentifier, *this, mJournal); - auto [valid, jvRes] = req->doCreate(getLineCache(inLedger, false), request); + auto [valid, jvRes] = + req->doCreate(getAssetCache(inLedger, false), request); if (!valid) { @@ -306,8 +308,8 @@ PathRequests::doLegacyPathRequest( std::shared_ptr const& inLedger, Json::Value const& request) { - auto cache = std::make_shared( - inLedger, app_.journal("RippleLineCache")); + auto cache = + std::make_shared(inLedger, app_.journal("AssetCache")); auto req = std::make_shared( app_, [] {}, consumer, ++mLastIdentifier, *this, mJournal); diff --git a/src/xrpld/app/paths/PathRequests.h b/src/xrpld/app/paths/PathRequests.h index 670790518a1..e5760d64ea2 100644 --- a/src/xrpld/app/paths/PathRequests.h +++ b/src/xrpld/app/paths/PathRequests.h @@ -21,8 +21,8 @@ #define RIPPLE_APP_PATHS_PATHREQUESTS_H_INCLUDED #include +#include #include -#include #include #include #include @@ -54,8 +54,8 @@ class PathRequests bool requestsPending() const; - std::shared_ptr - getLineCache( + std::shared_ptr + getAssetCache( std::shared_ptr const& ledger, bool authoritative); @@ -111,8 +111,8 @@ class PathRequests // Track all requests std::vector requests_; - // Use a RippleLineCache - std::weak_ptr lineCache_; + // Use a AssetCache + std::weak_ptr assetCache_; std::atomic mLastIdentifier; diff --git a/src/xrpld/app/paths/Pathfinder.cpp b/src/xrpld/app/paths/Pathfinder.cpp index 3f506809887..fd2749ad353 100644 --- a/src/xrpld/app/paths/Pathfinder.cpp +++ b/src/xrpld/app/paths/Pathfinder.cpp @@ -19,9 +19,9 @@ #include #include +#include #include #include -#include #include #include #include @@ -154,15 +154,49 @@ pathTypeToString(Pathfinder::PathType const& type) STAmount smallestUsefulAmount(STAmount const& amount, int maxPaths) { - return divide(amount, STAmount(maxPaths + 2), amount.issue()); + return divide(amount, STAmount(maxPaths + 2), amount.asset()); } + +STAmount +amountFromPathAsset( + PathAsset const& pathAsset, + std::optional const& srcIssuer, + AccountID const& srcAccount) +{ + return std::visit( + [&](T const& el) { + if constexpr (std::is_same_v) + { + auto const account = + srcIssuer.value_or(isXRP(el) ? xrpAccount() : srcAccount); + return STAmount(Issue{el, account}, 1u, 0, true); + } + else + return STAmount(el, 1u, 0, true); + }, + pathAsset.value()); +} + +Asset +assetFromPathAsset(PathAsset const& pathAsset, AccountID const& account) +{ + return std::visit( + [&](T const& el) { + if constexpr (std::is_same_v) + return Asset{Issue{el, account}}; + else + return Asset{el}; + }, + pathAsset.value()); +} + } // namespace Pathfinder::Pathfinder( - std::shared_ptr const& cache, + std::shared_ptr const& cache, AccountID const& uSrcAccount, AccountID const& uDstAccount, - Currency const& uSrcCurrency, + PathAsset const& uSrcPathAsset, std::optional const& uSrcIssuer, STAmount const& saDstAmount, std::optional const& srcAmount, @@ -173,24 +207,17 @@ Pathfinder::Pathfinder( isXRP(saDstAmount.getIssuer()) ? uDstAccount : saDstAmount.getIssuer()) , mDstAmount(saDstAmount) - , mSrcCurrency(uSrcCurrency) + , mSrcPathAsset(uSrcPathAsset) , mSrcIssuer(uSrcIssuer) - , mSrcAmount(srcAmount.value_or(STAmount( - Issue{ - uSrcCurrency, - uSrcIssuer.value_or( - isXRP(uSrcCurrency) ? xrpAccount() : uSrcAccount)}, - 1u, - 0, - true))) + , mSrcAmount(amountFromPathAsset(uSrcPathAsset, uSrcIssuer, uSrcAccount)) , convert_all_(convertAllCheck(mDstAmount)) , mLedger(cache->getLedger()) - , mRLCache(cache) + , mAssetCache(cache) , app_(app) , j_(app.journal("Pathfinder")) { ASSERT( - !uSrcIssuer || isXRP(uSrcCurrency) == isXRP(uSrcIssuer.value()), + !uSrcIssuer || uSrcPathAsset.isXRP() == isXRP(uSrcIssuer.value()), "ripple::Pathfinder::Pathfinder : valid inputs"); } @@ -212,7 +239,7 @@ Pathfinder::findPaths( } if (mSrcAccount == mDstAccount && mDstAccount == mEffectiveDst && - mSrcCurrency == mDstAmount.getCurrency()) + mSrcPathAsset == mDstAmount.asset()) { // No need to send to same account with same currency. JLOG(j_.debug()) << "Tried to send to same issuer"; @@ -220,26 +247,26 @@ Pathfinder::findPaths( return false; } - if (mSrcAccount == mEffectiveDst && - mSrcCurrency == mDstAmount.getCurrency()) + if (mSrcAccount == mEffectiveDst && mSrcPathAsset == mDstAmount.asset()) { // Default path might work, but any path would loop return true; } m_loadEvent = app_.getJobQueue().makeLoadEvent(jtPATH_FIND, "FindPath"); - auto currencyIsXRP = isXRP(mSrcCurrency); + auto currencyIsXRP = isXRP(mSrcPathAsset); bool useIssuerAccount = mSrcIssuer && !currencyIsXRP && !isXRP(*mSrcIssuer); auto& account = useIssuerAccount ? *mSrcIssuer : mSrcAccount; auto issuer = currencyIsXRP ? AccountID() : account; - mSource = STPathElement(account, mSrcCurrency, issuer); + mSource = STPathElement(account, mSrcPathAsset, issuer); auto issuerString = mSrcIssuer ? to_string(*mSrcIssuer) : std::string("none"); - JLOG(j_.trace()) << "findPaths>" << " mSrcAccount=" << mSrcAccount + JLOG(j_.trace()) << "findPaths>" + << " mSrcAccount=" << mSrcAccount << " mDstAccount=" << mDstAccount << " mDstAmount=" << mDstAmount.getFullText() - << " mSrcCurrency=" << mSrcCurrency + << " mSrcPathAsset=" << mSrcPathAsset << " mSrcIssuer=" << issuerString; if (!mLedger) @@ -248,8 +275,8 @@ Pathfinder::findPaths( return false; } - bool bSrcXrp = isXRP(mSrcCurrency); - bool bDstXrp = isXRP(mDstAmount.getCurrency()); + bool bSrcXrp = isXRP(mSrcPathAsset); + bool bDstXrp = isXRP(mDstAmount.asset()); if (!mLedger->exists(keylet::account(mSrcAccount))) { @@ -306,7 +333,7 @@ Pathfinder::findPaths( JLOG(j_.debug()) << "non-XRP to XRP payment"; paymentType = pt_nonXRP_to_XRP; } - else if (mSrcCurrency == mDstAmount.getCurrency()) + else if (equalAssets(mSrcPathAsset, mDstAmount.asset())) { // non-XRP -> non-XRP - Same currency JLOG(j_.debug()) << "non-XRP to non-XRP - same currency"; @@ -583,7 +610,7 @@ Pathfinder::getBestPaths( fullLiquidityPath.empty(), "ripple::Pathfinder::getBestPaths : first empty path result"); const bool issuerIsSender = - isXRP(mSrcCurrency) || (srcIssuer == mSrcAccount); + isXRP(mSrcPathAsset) || (srcIssuer == mSrcAccount); std::vector extraPathRanks; rankPaths(maxPaths, extraPaths, extraPathRanks, continueCallback); @@ -700,28 +727,28 @@ Pathfinder::getBestPaths( } bool -Pathfinder::issueMatchesOrigin(Issue const& issue) +Pathfinder::issueMatchesOrigin(Asset const& asset) { - bool matchingCurrency = (issue.currency == mSrcCurrency); - bool matchingAccount = isXRP(issue.currency) || - (mSrcIssuer && issue.account == mSrcIssuer) || - issue.account == mSrcAccount; + bool matchingAsset = equalAssets(asset, mSrcPathAsset); + bool matchingAccount = isXRP(asset) || + (mSrcIssuer && asset.getIssuer() == mSrcIssuer) || + asset.getIssuer() == mSrcAccount; - return matchingCurrency && matchingAccount; + return matchingAsset && matchingAccount; } int Pathfinder::getPathsOut( - Currency const& currency, + PathAsset const& pathAsset, AccountID const& account, - LineDirection direction, - bool isDstCurrency, + std::optional direction, + bool isDstAsset, AccountID const& dstAccount, std::function const& continueCallback) { - Issue const issue(currency, account); + Asset const asset = assetFromPathAsset(pathAsset, account); - auto [it, inserted] = mPathsOutCountMap.emplace(issue, 0); + auto [it, inserted] = mPathsOutCountMap.emplace(asset, 0); // If it was already present, return the stored number of paths if (!inserted) @@ -733,41 +760,80 @@ Pathfinder::getPathsOut( return 0; int aFlags = sleAccount->getFieldU32(sfFlags); - bool const bAuthRequired = (aFlags & lsfRequireAuth) != 0; - bool const bFrozen = ((aFlags & lsfGlobalFreeze) != 0); + bool const bAuthRequired = [&]() { + if (pathAsset.holds()) + return (aFlags & lsfRequireAuth) != 0; + return requireAuth(*mLedger, asset.get(), account) != + tesSUCCESS; + }(); + bool const bFrozen = [&]() { + if (pathAsset.holds()) + return (aFlags & lsfGlobalFreeze) != 0; + return isGlobalFrozen(*mLedger, asset.get()); + }(); int count = 0; if (!bFrozen) { - count = app_.getOrderBookDB().getBookSize(issue); + count = app_.getOrderBookDB().getBookSize(asset); - if (auto const lines = mRLCache->getRippleLines(account, direction)) + if (asset.holds()) { - for (auto const& rspEntry : *lines) + assert(direction); + if (auto const lines = + mAssetCache->getRippleLines(account, *direction)) { - if (currency != rspEntry.getLimit().getCurrency()) + for (auto const& rspEntry : *lines) { + if (pathAsset.get() != + rspEntry.getLimit().getCurrency()) + { + } + else if ( + rspEntry.getBalance() <= beast::zero && + (!rspEntry.getLimitPeer() || + -rspEntry.getBalance() >= rspEntry.getLimitPeer() || + (bAuthRequired && !rspEntry.getAuth()))) + { + } + else if ( + isDstAsset && dstAccount == rspEntry.getAccountIDPeer()) + { + count += + 10000; // count a path to the destination extra + } + else if (rspEntry.getNoRipplePeer()) + { + // This probably isn't a useful path out + } + else if (rspEntry.getFreezePeer()) + { + // Not a useful path out + } + else + { + ++count; + } } - else if ( - rspEntry.getBalance() <= beast::zero && - (!rspEntry.getLimitPeer() || - -rspEntry.getBalance() >= rspEntry.getLimitPeer() || - (bAuthRequired && !rspEntry.getAuth()))) + } + } + else if (auto const mpts = mAssetCache->getMPTs(account)) + { + for (auto const& mpt : *mpts) + { + if (pathAsset.get() != mpt) { } + // TODO MPT is this correct else if ( - isDstCurrency && dstAccount == rspEntry.getAccountIDPeer()) + bAuthRequired && + requireAuth(*mLedger, MPTIssue{mpt}, account) != tesSUCCESS) { - count += 10000; // count a path to the destination extra } - else if (rspEntry.getNoRipplePeer()) + else if (isDstAsset && dstAccount == getMPTIssuer(mpt)) { - // This probably isn't a useful path out - } - else if (rspEntry.getFreezePeer()) - { - // Not a useful path out + count += 10000; } else { @@ -925,7 +991,8 @@ Pathfinder::isNoRippleOut(STPath const& currentPath) ? mSrcAccount : (currentPath.end() - 2)->getAccountID(); auto const& toAccount = endElement.getAccountID(); - return isNoRipple(fromAccount, toAccount, endElement.getCurrency()); + return endElement.hasCurrency() && + isNoRipple(fromAccount, toAccount, endElement.getCurrency()); } void @@ -949,10 +1016,10 @@ Pathfinder::addLink( std::function const& continueCallback) { auto const& pathEnd = currentPath.empty() ? mSource : currentPath.back(); - auto const& uEndCurrency = pathEnd.getCurrency(); + auto const& uEndPathAsset = pathEnd.getPathAsset(); auto const& uEndIssuer = pathEnd.getIssuerID(); auto const& uEndAccount = pathEnd.getAccountID(); - bool const bOnXRP = uEndCurrency.isZero(); + bool const bOnXRP = isXRP(uEndPathAsset); // Does pathfinding really need to get this to // a gateway (the issuer of the destination amount) @@ -984,27 +1051,38 @@ Pathfinder::addLink( { bool const bRequireAuth( sleEnd->getFieldU32(sfFlags) & lsfRequireAuth); - bool const bIsEndCurrency( - uEndCurrency == mDstAmount.getCurrency()); + bool const bIsEndAsset( + equalAssets(uEndPathAsset, mDstAmount.asset())); bool const bIsNoRippleOut(isNoRippleOut(currentPath)); bool const bDestOnly(addFlags & afAC_LAST); - if (auto const lines = mRLCache->getRippleLines( - uEndAccount, - bIsNoRippleOut ? LineDirection::incoming - : LineDirection::outgoing)) - { - auto& rippleLines = *lines; + AccountCandidates candidates; - AccountCandidates candidates; - candidates.reserve(rippleLines.size()); + auto forAssets = [&]( + AssetType const& assets) { + candidates.reserve(assets.size()); - for (auto const& rs : rippleLines) + static bool constexpr isLine = std:: + is_same_v>; + static bool constexpr isMPT = + std::is_same_v>; + + for (auto const& asset : assets) { if (continueCallback && !continueCallback()) return; - auto const& acct = rs.getAccountIDPeer(); - LineDirection const direction = rs.getDirectionPeer(); + auto const& acct = [&]() constexpr { + if constexpr (isLine) + return asset.getAccountIDPeer(); + if constexpr (isMPT) + return getMPTIssuer(asset); + }(); + auto const direction = + [&]() constexpr -> std::optional { + if constexpr (isLine) + return asset.getDirectionPeer(); + return std::nullopt; + }(); if (hasEffectiveDestination && (acct == mDstAccount)) { @@ -1019,26 +1097,41 @@ Pathfinder::addLink( continue; } - if ((uEndCurrency == rs.getLimit().getCurrency()) && - !currentPath.hasSeen(acct, uEndCurrency, acct)) + auto const correctAsset = [&]() { + if constexpr (isLine) + return uEndPathAsset.get() == + asset.getLimit().getCurrency(); + if constexpr (isMPT) + return uEndPathAsset.get() == asset; + }(); + auto checkLine = [&]() { + if constexpr (isLine) + { + return ( + (asset.getBalance() <= beast::zero && + (!asset.getLimitPeer() || + -asset.getBalance() >= + asset.getLimitPeer() || + (bRequireAuth && !asset.getAuth()))) || + (bIsNoRippleOut && asset.getNoRipple())); + } + if constexpr (isMPT) + return false; + }; + + if (correctAsset && + !currentPath.hasSeen(acct, uEndPathAsset, acct)) { // path is for correct currency and has not been // seen - if (rs.getBalance() <= beast::zero && - (!rs.getLimitPeer() || - -rs.getBalance() >= rs.getLimitPeer() || - (bRequireAuth && !rs.getAuth()))) - { - // path has no credit - } - else if (bIsNoRippleOut && rs.getNoRipple()) + if (checkLine()) { // Can't leave on this path } else if (bToDestination) { // destination is always worth trying - if (uEndCurrency == mDstAmount.getCurrency()) + if (uEndPathAsset == mDstAmount.asset()) { // this is a complete path if (!currentPath.empty()) @@ -1066,10 +1159,10 @@ Pathfinder::addLink( { // save this candidate int out = getPathsOut( - uEndCurrency, + uEndPathAsset, acct, direction, - bIsEndCurrency, + bIsEndAsset, mEffectiveDst, continueCallback); if (out) @@ -1077,40 +1170,54 @@ Pathfinder::addLink( } } } + }; - if (!candidates.empty()) + if (uEndPathAsset.holds()) + { + if (auto const lines = mAssetCache->getRippleLines( + uEndAccount, + bIsNoRippleOut ? LineDirection::incoming + : LineDirection::outgoing)) { - std::sort( - candidates.begin(), - candidates.end(), - std::bind( - compareAccountCandidate, - mLedger->seq(), - std::placeholders::_1, - std::placeholders::_2)); - - int count = candidates.size(); - // allow more paths from source - if ((count > 10) && (uEndAccount != mSrcAccount)) - count = 10; - else if (count > 50) - count = 50; - - auto it = candidates.begin(); - while (count-- != 0) - { - if (continueCallback && !continueCallback()) - return; - // Add accounts to incompletePaths - STPathElement pathElement( - STPathElement::typeAccount, - it->account, - uEndCurrency, - it->account); - incompletePaths.assembleAdd( - currentPath, pathElement); - ++it; - } + forAssets(*lines); + } + } + else if (auto const mpts = mAssetCache->getMPTs(uEndAccount)) + { + forAssets(*mpts); + } + + if (!candidates.empty()) + { + std::sort( + candidates.begin(), + candidates.end(), + std::bind( + compareAccountCandidate, + mLedger->seq(), + std::placeholders::_1, + std::placeholders::_2)); + + int count = candidates.size(); + // allow more paths from source + if ((count > 10) && (uEndAccount != mSrcAccount)) + count = 10; + else if (count > 50) + count = 50; + + auto it = candidates.begin(); + while (count-- != 0) + { + if (continueCallback && !continueCallback()) + return; + // Add accounts to incompletePaths + STPathElement pathElement( + STPathElement::typeAccount, + it->account, + uEndPathAsset, + it->account); + incompletePaths.assembleAdd(currentPath, pathElement); + ++it; } } } @@ -1127,7 +1234,8 @@ Pathfinder::addLink( { // to XRP only if (!bOnXRP && - app_.getOrderBookDB().isBookToXRP({uEndCurrency, uEndIssuer})) + app_.getOrderBookDB().isBookToXRP( + assetFromPathAsset(uEndPathAsset, uEndIssuer))) { STPathElement pathElement( STPathElement::typeCurrency, @@ -1141,7 +1249,7 @@ Pathfinder::addLink( { bool bDestOnly = (addFlags & afOB_LAST) != 0; auto books = app_.getOrderBookDB().getBooksByTakerPays( - {uEndCurrency, uEndIssuer}); + assetFromPathAsset(uEndPathAsset, uEndIssuer)); JLOG(j_.trace()) << books.size() << " books found from this currency/issuer"; @@ -1150,14 +1258,13 @@ Pathfinder::addLink( if (continueCallback && !continueCallback()) return; if (!currentPath.hasSeen( - xrpAccount(), book.out.currency, book.out.account) && + xrpAccount(), book.out, book.out.getIssuer()) && !issueMatchesOrigin(book.out) && - (!bDestOnly || - (book.out.currency == mDstAmount.getCurrency()))) + (!bDestOnly || equalTokens(book.out, mDstAmount.asset()))) { STPath newPath(currentPath); - if (book.out.currency.isZero()) + if (isXRP(book.out)) { // to XRP // add the order book itself @@ -1167,7 +1274,7 @@ Pathfinder::addLink( xrpCurrency(), xrpAccount()); - if (mDstAmount.getCurrency().isZero()) + if (isXRP(mDstAmount.asset())) { // destination is XRP, add account and path is // complete @@ -1180,10 +1287,13 @@ Pathfinder::addLink( incompletePaths.push_back(newPath); } else if (!currentPath.hasSeen( - book.out.account, - book.out.currency, - book.out.account)) + book.out.getIssuer(), + book.out, + book.out.getIssuer())) { + auto const assetType = book.out.holds() + ? STPathElement::typeCurrency + : STPathElement::typeMPT; // Don't want the book if we've already seen the issuer // book -> account -> book if ((newPath.size() >= 2) && @@ -1192,32 +1302,30 @@ Pathfinder::addLink( { // replace the redundant account with the order book newPath[newPath.size() - 1] = STPathElement( - STPathElement::typeCurrency | - STPathElement::typeIssuer, + assetType | STPathElement::typeIssuer, xrpAccount(), - book.out.currency, - book.out.account); + book.out, + book.out.getIssuer()); } else { // add the order book newPath.emplace_back( - STPathElement::typeCurrency | - STPathElement::typeIssuer, + assetType | STPathElement::typeIssuer, xrpAccount(), - book.out.currency, - book.out.account); + book.out, + book.out.getIssuer()); } if (hasEffectiveDestination && - book.out.account == mDstAccount && - book.out.currency == mDstAmount.getCurrency()) + book.out.getIssuer() == mDstAccount && + equalTokens(book.out, mDstAmount.asset())) { // We skipped a required issuer } else if ( - book.out.account == mEffectiveDst && - book.out.currency == mDstAmount.getCurrency()) + book.out.getIssuer() == mEffectiveDst && + equalTokens(book.out, mDstAmount.asset())) { // with the destination account, this path is // complete JLOG(j_.trace()) @@ -1227,14 +1335,15 @@ Pathfinder::addLink( } else { + // TODO MPT why asset and issuer are also included? // add issuer's account, path still incomplete incompletePaths.assembleAdd( newPath, STPathElement( STPathElement::typeAccount, - book.out.account, - book.out.currency, - book.out.account)); + book.out.getIssuer(), + book.out, + book.out.getIssuer())); } } } diff --git a/src/xrpld/app/paths/Pathfinder.h b/src/xrpld/app/paths/Pathfinder.h index 01556a3c63f..882c95b32ed 100644 --- a/src/xrpld/app/paths/Pathfinder.h +++ b/src/xrpld/app/paths/Pathfinder.h @@ -21,9 +21,10 @@ #define RIPPLE_APP_PATHS_PATHFINDER_H_INCLUDED #include -#include +#include #include #include +#include #include #include @@ -40,10 +41,10 @@ class Pathfinder : public CountedObject public: /** Construct a pathfinder without an issuer.*/ Pathfinder( - std::shared_ptr const& cache, + std::shared_ptr const& cache, AccountID const& srcAccount, AccountID const& dstAccount, - Currency const& uSrcCurrency, + PathAsset const& uSrcPathAsset, std::optional const& uSrcIssuer, STAmount const& dstAmount, std::optional const& srcAmount, @@ -138,14 +139,14 @@ class Pathfinder : public CountedObject std::function const& continueCallback); bool - issueMatchesOrigin(Issue const&); + issueMatchesOrigin(Asset const&); int getPathsOut( - Currency const& currency, + PathAsset const& pathAsset, AccountID const& account, - LineDirection direction, - bool isDestCurrency, + std::optional direction, + bool isDestPathAsset, AccountID const& dest, std::function const& continueCallback); @@ -197,7 +198,7 @@ class Pathfinder : public CountedObject AccountID mDstAccount; AccountID mEffectiveDst; // The account the paths need to end at STAmount mDstAmount; - Currency mSrcCurrency; + PathAsset mSrcPathAsset; std::optional mSrcIssuer; STAmount mSrcAmount; /** The amount remaining from mSrcAccount after the default liquidity has @@ -207,14 +208,14 @@ class Pathfinder : public CountedObject std::shared_ptr mLedger; std::unique_ptr m_loadEvent; - std::shared_ptr mRLCache; + std::shared_ptr mAssetCache; STPathElement mSource; STPathSet mCompletePaths; std::vector mPathRanks; std::map mPaths; - hash_map mPathsOutCountMap; + hash_map mPathsOutCountMap; Application& app_; beast::Journal const j_; diff --git a/src/xrpld/app/paths/RippleCalc.cpp b/src/xrpld/app/paths/RippleCalc.cpp index c7b2e1f01e0..ddaa4827e04 100644 --- a/src/xrpld/app/paths/RippleCalc.cpp +++ b/src/xrpld/app/paths/RippleCalc.cpp @@ -84,7 +84,7 @@ RippleCalc::rippleCalculate( auto const sendMax = [&]() -> std::optional { if (saMaxAmountReq >= beast::zero || - saMaxAmountReq.getCurrency() != saDstAmountReq.getCurrency() || + saMaxAmountReq.asset() != saDstAmountReq.asset() || saMaxAmountReq.getIssuer() != uSrcAccountID) { return saMaxAmountReq; diff --git a/src/xrpld/app/paths/detail/AMMLiquidity.cpp b/src/xrpld/app/paths/detail/AMMLiquidity.cpp index 88600e2cc66..dd7b94de6cb 100644 --- a/src/xrpld/app/paths/detail/AMMLiquidity.cpp +++ b/src/xrpld/app/paths/detail/AMMLiquidity.cpp @@ -27,15 +27,15 @@ AMMLiquidity::AMMLiquidity( ReadView const& view, AccountID const& ammAccountID, std::uint32_t tradingFee, - Issue const& in, - Issue const& out, + Asset const& in, + Asset const& out, AMMContext& ammContext, beast::Journal j) : ammContext_(ammContext) , ammAccountID_(ammAccountID) , tradingFee_(tradingFee) - , issueIn_(in) - , issueOut_(out) + , assetIn_(in) + , assetOut_(out) , initialBalances_{fetchBalances(view)} , j_(j) { @@ -45,13 +45,13 @@ template TAmounts AMMLiquidity::fetchBalances(ReadView const& view) const { - auto const assetIn = ammAccountHolds(view, ammAccountID_, issueIn_); - auto const assetOut = ammAccountHolds(view, ammAccountID_, issueOut_); + auto const amountIn = ammAccountHolds(view, ammAccountID_, assetIn_); + auto const amountOut = ammAccountHolds(view, ammAccountID_, assetOut_); // This should not happen. - if (assetIn < beast::zero || assetOut < beast::zero) + if (amountIn < beast::zero || amountOut < beast::zero) Throw("AMMLiquidity: invalid balances"); - return TAmounts{get(assetIn), get(assetOut)}; + return TAmounts{get(amountIn), get(amountOut)}; } template @@ -62,7 +62,7 @@ AMMLiquidity::generateFibSeqOffer( TAmounts cur{}; cur.in = toAmount( - getIssue(balances.in), + getAsset(balances.in), InitialFibSeqPct * initialBalances_.in, Number::rounding_mode::upward); cur.out = swapAssetIn(initialBalances_, cur.in, tradingFee_); @@ -82,7 +82,7 @@ AMMLiquidity::generateFibSeqOffer( "ripple::AMMLiquidity::generateFibSeqOffer : maximum iterations"); cur.out = toAmount( - getIssue(balances.out), + getAsset(balances.out), cur.out * fib[ammContext_.curIters() - 1], Number::rounding_mode::downward); // swapAssetOut() returns negative in this case @@ -106,11 +106,13 @@ maxAmount() return IOUAmount(STAmount::cMaxValue / 2, STAmount::cMaxOffset); else if constexpr (std::is_same_v) return STAmount(STAmount::cMaxValue / 2, STAmount::cMaxOffset); + else if constexpr (std::is_same_v) + return MPTAmount(maxMPTokenAmount); } template T -maxOut(T const& out, Issue const& iss) +maxOut(T const& out, Asset const& iss) { Number const res = out * Number{99, -2}; return toAmount(iss, res, Number::rounding_mode::downward); @@ -134,7 +136,7 @@ AMMLiquidity::maxOffer( } else { - auto const out = maxOut(balances.out, issueOut()); + auto const out = maxOut(balances.out, assetOut()); if (out <= TOut{0} || out >= balances.out) return std::nullopt; return AMMOffer( @@ -243,8 +245,8 @@ AMMLiquidity::getOffer( { JLOG(j_.trace()) << "AMMLiquidity::getOffer, created " - << to_string(offer->amount().in) << "/" << issueIn_ << " " - << to_string(offer->amount().out) << "/" << issueOut_; + << to_string(offer->amount().in) << "/" << assetIn_ << " " + << to_string(offer->amount().out) << "/" << assetOut_; return offer; } @@ -259,9 +261,13 @@ AMMLiquidity::getOffer( return std::nullopt; } -template class AMMLiquidity; template class AMMLiquidity; template class AMMLiquidity; template class AMMLiquidity; +template class AMMLiquidity; +template class AMMLiquidity; +template class AMMLiquidity; +template class AMMLiquidity; +template class AMMLiquidity; } // namespace ripple diff --git a/src/xrpld/app/paths/detail/AMMOffer.cpp b/src/xrpld/app/paths/detail/AMMOffer.cpp index 16ea8628f3b..6de969a51e0 100644 --- a/src/xrpld/app/paths/detail/AMMOffer.cpp +++ b/src/xrpld/app/paths/detail/AMMOffer.cpp @@ -23,7 +23,7 @@ namespace ripple { -template +template AMMOffer::AMMOffer( AMMLiquidity const& ammLiquidity, TAmounts const& amounts, @@ -37,28 +37,35 @@ AMMOffer::AMMOffer( { } -template -Issue const& -AMMOffer::issueIn() const +template +Asset const& +AMMOffer::assetIn() const { - return ammLiquidity_.issueIn(); + return ammLiquidity_.assetIn(); } -template +template +Asset const& +AMMOffer::assetOut() const +{ + return ammLiquidity_.assetOut(); +} + +template AccountID const& AMMOffer::owner() const { return ammLiquidity_.ammAccount(); } -template +template TAmounts const& AMMOffer::amount() const { return amounts_; } -template +template void AMMOffer::consume( ApplyView& view, @@ -76,7 +83,7 @@ AMMOffer::consume( ammLiquidity_.context().setAMMUsed(); } -template +template TAmounts AMMOffer::limitOut( TAmounts const& offrAmt, @@ -106,7 +113,7 @@ AMMOffer::limitOut( return {swapAssetOut(balances_, limit, ammLiquidity_.tradingFee()), limit}; } -template +template TAmounts AMMOffer::limitIn( TAmounts const& offrAmt, @@ -125,7 +132,7 @@ AMMOffer::limitIn( return {limit, swapAssetIn(balances_, limit, ammLiquidity_.tradingFee())}; } -template +template QualityFunction AMMOffer::getQualityFunc() const { @@ -135,7 +142,7 @@ AMMOffer::getQualityFunc() const balances_, ammLiquidity_.tradingFee(), QualityFunction::AMMTag{}}; } -template +template bool AMMOffer::checkInvariant( TAmounts const& consumed, @@ -173,9 +180,13 @@ AMMOffer::checkInvariant( return false; } -template class AMMOffer; template class AMMOffer; template class AMMOffer; template class AMMOffer; +template class AMMOffer; +template class AMMOffer; +template class AMMOffer; +template class AMMOffer; +template class AMMOffer; } // namespace ripple diff --git a/src/xrpld/app/paths/detail/AmountSpec.h b/src/xrpld/app/paths/detail/AmountSpec.h index 59ca5f5d29b..47f47a5729e 100644 --- a/src/xrpld/app/paths/detail/AmountSpec.h +++ b/src/xrpld/app/paths/detail/AmountSpec.h @@ -32,22 +32,56 @@ struct AmountSpec { explicit AmountSpec() = default; - bool native; - union - { - XRPAmount xrp; - IOUAmount iou = {}; - }; + std::variant amount; std::optional issuer; std::optional currency; + std::optional mptid; + + bool + native() const + { + return std::holds_alternative(amount); + } + bool + isIOU() const + { + return std::holds_alternative(amount); + } + template + void + check() const + { + if (!std::holds_alternative(amount)) + Throw("AmountSpec doesn't hold requested amount"); + } + XRPAmount const& + xrp() const + { + check(); + return std::get(amount); + } + IOUAmount const& + iou() const + { + check(); + return std::get(amount); + } + MPTAmount const& + mpt() const + { + check(); + return std::get(amount); + } friend std::ostream& operator<<(std::ostream& stream, AmountSpec const& amt) { - if (amt.native) - stream << to_string(amt.xrp); + if (std::holds_alternative(amt.amount)) + stream << to_string(*amt.mptid); + else if (amt.native()) + stream << to_string(amt.xrp()); else - stream << to_string(amt.iou); + stream << to_string(amt.iou()); if (amt.currency) stream << "/(" << *amt.currency << ")"; if (amt.issuer) @@ -58,63 +92,86 @@ struct AmountSpec struct EitherAmount { -#ifndef NDEBUG - bool native = false; -#endif - - union - { - IOUAmount iou = {}; - XRPAmount xrp; - }; + std::variant amount; EitherAmount() = default; - explicit EitherAmount(IOUAmount const& a) : iou(a) + explicit EitherAmount(IOUAmount const& a) : amount(a) { } -#if defined(__GNUC__) && !defined(__clang__) -#pragma GCC diagnostic push - // ignore warning about half of iou amount being uninitialized -#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" -#endif - explicit EitherAmount(XRPAmount const& a) : xrp(a) + explicit EitherAmount(XRPAmount const& a) : amount(a) + { + } + + explicit EitherAmount(MPTAmount const& a) : amount(a) { -#ifndef NDEBUG - native = true; -#endif } -#if defined(__GNUC__) && !defined(__clang__) -#pragma GCC diagnostic pop -#endif explicit EitherAmount(AmountSpec const& a) { -#ifndef NDEBUG - native = a.native; -#endif - if (a.native) - xrp = a.xrp; - else - iou = a.iou; + amount = a.amount; + } + + bool + native() const + { + return std::holds_alternative(amount); + } + bool + isIOU() const + { + return std::holds_alternative(amount); + } + bool + isMPT() const + { + return std::holds_alternative(amount); + } + template + void + check() const + { + if (!std::holds_alternative(amount)) + Throw( + "EitherAmount doesn't hold requested amount"); + } + XRPAmount const& + xrp() const + { + check(); + return std::get(amount); + } + IOUAmount const& + iou() const + { + check(); + return std::get(amount); + } + MPTAmount const& + mpt() const + { + check(); + return std::get(amount); } #ifndef NDEBUG friend std::ostream& operator<<(std::ostream& stream, EitherAmount const& amt) { - if (amt.native) - stream << to_string(amt.xrp); + if (amt.native()) + stream << to_string(amt.xrp()); + else if (amt.isIOU()) + stream << to_string(amt.iou()); else - stream << to_string(amt.iou); + stream << to_string(amt.mpt()); return stream; } #endif }; template -T& +T const& get(EitherAmount& amt) { static_assert(sizeof(T) == -1, "Must used specialized function"); @@ -122,19 +179,27 @@ get(EitherAmount& amt) } template <> -inline IOUAmount& +inline IOUAmount const& get(EitherAmount& amt) { - ASSERT(!amt.native, "ripple::get(EitherAmount&) : is not XRP"); - return amt.iou; + ASSERT(amt.isIOU(), "ripple::get(EitherAmount&) : is IOU"); + return amt.iou(); } template <> -inline XRPAmount& +inline XRPAmount const& get(EitherAmount& amt) { - ASSERT(amt.native, "ripple::get(EitherAmount&) : is XRP"); - return amt.xrp; + ASSERT(amt.native(), "ripple::get(EitherAmount&) : is XRP"); + return amt.xrp(); +} + +template <> +inline MPTAmount const& +get(EitherAmount& amt) +{ + ASSERT(amt.isMPT(), "ripple::get(EitherAmount&) : is MPT"); + return amt.mpt(); } template @@ -150,17 +215,26 @@ inline IOUAmount const& get(EitherAmount const& amt) { ASSERT( - !amt.native, + !amt.native(), "ripple::get(EitherAmount const&) : is not XRP"); - return amt.iou; + return amt.iou(); } template <> inline XRPAmount const& get(EitherAmount const& amt) { - ASSERT(amt.native, "ripple::get(EitherAmount const&) : is XRP"); - return amt.xrp; + ASSERT( + amt.native(), "ripple::get(EitherAmount const&) : is XRP"); + return amt.xrp(); +} + +template <> +inline MPTAmount const& +get(EitherAmount const& amt) +{ + ASSERT(amt.isMPT(), "ripple::get(EitherAmount const&) : is MPT"); + return amt.mpt(); } inline AmountSpec @@ -174,14 +248,18 @@ toAmountSpec(STAmount const& amt) isNeg ? -std::int64_t(amt.mantissa()) : amt.mantissa(); AmountSpec result; - result.native = isXRP(amt); - if (result.native) + if (isXRP(amt)) { - result.xrp = XRPAmount(sMant); + result.amount = XRPAmount(sMant); + } + else if (amt.holds()) + { + result.mptid = amt.get().getMptID(); + result.amount = amt.mpt(); } else { - result.iou = IOUAmount(sMant, amt.exponent()); + result.amount = IOUAmount(sMant, amt.exponent()); result.issuer = amt.issue().account; result.currency = amt.issue().currency; } @@ -194,27 +272,21 @@ toEitherAmount(STAmount const& amt) { if (isXRP(amt)) return EitherAmount{amt.xrp()}; - return EitherAmount{amt.iou()}; + else if (amt.holds()) + return EitherAmount{amt.iou()}; + return EitherAmount(amt.mpt()); } inline AmountSpec toAmountSpec(EitherAmount const& ea, std::optional const& c) { AmountSpec r; - r.native = (!c || isXRP(*c)); r.currency = c; ASSERT( - ea.native == r.native, + ea.native() == r.native(), "ripple::toAmountSpec(EitherAmount const&&, std::optional) : " "matching native"); - if (r.native) - { - r.xrp = ea.xrp; - } - else - { - r.iou = ea.iou; - } + r.amount = ea.amount; return r; } diff --git a/src/xrpld/app/paths/detail/BookStep.cpp b/src/xrpld/app/paths/detail/BookStep.cpp index df05cc41864..910217744df 100644 --- a/src/xrpld/app/paths/detail/BookStep.cpp +++ b/src/xrpld/app/paths/detail/BookStep.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -90,7 +91,7 @@ class BookStep : public StepImp> } public: - BookStep(StrandContext const& ctx, Issue const& in, Issue const& out) + BookStep(StrandContext const& ctx, Asset const& in, Asset const& out) : maxOffersToConsume_(getMaxOffersToConsume(ctx)) , book_(in, out) , strandSrc_(ctx.strandSrc) @@ -189,10 +190,11 @@ class BookStep : public StepImp> logStringImpl(char const* name) const { std::ostringstream ostr; - ostr << name << ": " << "\ninIss: " << book_.in.account - << "\noutIss: " << book_.out.account - << "\ninCur: " << book_.in.currency - << "\noutCur: " << book_.out.currency; + ostr << name << ": " + << "\ninIss: " << book_.in.getIssuer() + << "\noutIss: " << book_.out.getIssuer() + << "\ninCur: " << to_string(book_.in) + << "\noutCur: " << to_string(book_.out); return ostr.str(); } @@ -337,19 +339,21 @@ class BookPaymentStep : public BookStep> // (the old code does not charge a fee) // Calculate amount that goes to the taker and the amount charged the // offer owner - auto rate = [&](AccountID const& id) { - if (isXRP(id) || id == this->strandDst_) + auto rate = [&](Asset const& asset) { + if (isXRP(asset) || asset.getIssuer() == this->strandDst_) return parityRate; - return transferRate(v, id); + if (asset.holds()) + return transferRate(v, asset.getIssuer()); + return transferRate(v, asset.get().getMptID()); }; auto const trIn = - redeems(prevStepDir) ? rate(this->book_.in.account) : parityRate; + redeems(prevStepDir) ? rate(this->book_.in) : parityRate; // Always charge the transfer fee, even if the owner is the issuer, // unless the fee is waived auto const trOut = (this->ownerPaysTransferFee_ && waiveFee == WaiveTransferFee::No) - ? rate(this->book_.out.account) + ? rate(this->book_.out) : parityRate; Quality const q1{getRate(STAmount(trOut.value), STAmount(trIn.value))}; @@ -391,8 +395,8 @@ class BookOfferCrossingStep public: BookOfferCrossingStep( StrandContext const& ctx, - Issue const& in, - Issue const& out) + Asset const& in, + Asset const& out) : BookStep>(ctx, in, out) , defaultPath_(ctx.isDefaultPath) , qualityThreshold_(getQuality(ctx.limitQuality)) @@ -546,8 +550,9 @@ class BookOfferCrossingStep return transferRate(v, id); }; - auto const trIn = - redeems(prevStepDir) ? rate(this->book_.in.account) : parityRate; + auto const trIn = redeems(prevStepDir) + ? rate(this->book_.in.getIssuer()) + : parityRate; // AMM doesn't pay the transfer fee on the out amount auto const trOut = parityRate; @@ -723,17 +728,19 @@ BookStep::forEachOffer( // (the old code does not charge a fee) // Calculate amount that goes to the taker and the amount charged the offer // owner - auto rate = [this, &sb](AccountID const& id) -> std::uint32_t { - if (isXRP(id) || id == this->strandDst_) + auto rate = [this, &sb](Asset const& asset) -> std::uint32_t { + if (isXRP(asset) || asset.getIssuer() == this->strandDst_) return QUALITY_ONE; - return transferRate(sb, id).value; + if (asset.holds()) + return transferRate(sb, asset.getIssuer()).value; + return transferRate(sb, asset.get().getMptID()).value; }; std::uint32_t const trIn = - redeems(prevStepDir) ? rate(book_.in.account) : QUALITY_ONE; + redeems(prevStepDir) ? rate(book_.in) : QUALITY_ONE; // Always charge the transfer fee, even if the owner is the issuer std::uint32_t const trOut = - ownerPaysTransferFee_ ? rate(book_.out.account) : QUALITY_ONE; + ownerPaysTransferFee_ ? rate(book_.out) : QUALITY_ONE; typename FlowOfferStream::StepCounter counter( maxOffersToConsume_, j_); @@ -741,7 +748,6 @@ BookStep::forEachOffer( FlowOfferStream offers( sb, afView, book_, sb.parentCloseTime(), counter, j_); - bool const flowCross = afView.rules().enabled(featureFlowCross); bool offerAttempted = false; std::optional ofrQ; auto execOffer = [&](auto& offer) { @@ -758,34 +764,17 @@ BookStep::forEachOffer( // Make sure offer owner has authorization to own IOUs from issuer. // An account can always own XRP or their own IOUs. - if (flowCross && (!isXRP(offer.issueIn().currency)) && - (offer.owner() != offer.issueIn().account)) + if (requireAuth(afView, offer.assetIn(), offer.owner()) != tesSUCCESS) { - auto const& issuerID = offer.issueIn().account; - auto const issuer = afView.read(keylet::account(issuerID)); - if (issuer && ((*issuer)[sfFlags] & lsfRequireAuth)) - { - // Issuer requires authorization. See if offer owner has that. - auto const& ownerID = offer.owner(); - auto const authFlag = - issuerID > ownerID ? lsfHighAuth : lsfLowAuth; - - auto const line = afView.read( - keylet::line(ownerID, issuerID, offer.issueIn().currency)); - - if (!line || (((*line)[sfFlags] & authFlag) == 0)) - { - // Offer owner not authorized to hold IOU from issuer. - // Remove this offer even if no crossing occurs. - if (auto const key = offer.key()) - offers.permRmOffer(*key); - if (!offerAttempted) - // Change quality only if no previous offers were tried. - ofrQ = std::nullopt; - // Returning true causes offers.step() to delete the offer. - return true; - } - } + // Offer owner not authorized to hold IOU/MPT from issuer. + // Remove this offer even if no crossing occurs. + if (auto const key = offer.key()) + offers.permRmOffer(*key); + if (!offerAttempted) + // Change quality only if no previous offers were tried. + ofrQ = std::nullopt; + // Returning true causes offers.step() to delete the offer. + return true; } if (!static_cast(this)->checkQualityThreshold( @@ -893,7 +882,7 @@ BookStep::consumeOffer( { auto const dr = offer.send( sb, - book_.in.account, + book_.in.getIssuer(), offer.owner(), toSTAmount(ofrAmt.in, book_.in), j_); @@ -907,7 +896,7 @@ BookStep::consumeOffer( auto const cr = offer.send( sb, offer.owner(), - book_.out.account, + book_.out.getIssuer(), toSTAmount(ownerGives, book_.out), j_); if (cr != tesSUCCESS) @@ -1355,20 +1344,21 @@ BookStep::check(StrandContext const& ctx) const // Do not allow two books to output the same issue. This may cause offers on // one step to unfund offers in another step. if (!ctx.seenBookOuts.insert(book_.out).second || - ctx.seenDirectIssues[0].count(book_.out)) + ctx.seenDirectAssets[0].count(book_.out)) { JLOG(j_.debug()) << "BookStep: loop detected: " << *this; return temBAD_PATH_LOOP; } - if (ctx.seenDirectIssues[1].count(book_.out)) + if (ctx.seenDirectAssets[1].count(book_.out)) { JLOG(j_.debug()) << "BookStep: loop detected: " << *this; return temBAD_PATH_LOOP; } - auto issuerExists = [](ReadView const& view, Issue const& iss) -> bool { - return isXRP(iss.account) || view.read(keylet::account(iss.account)); + auto issuerExists = [](ReadView const& view, Asset const& iss) -> bool { + return isXRP(iss.getIssuer()) || + view.read(keylet::account(iss.getIssuer())); }; if (!issuerExists(ctx.view, book_.in) || !issuerExists(ctx.view, book_.out)) @@ -1382,14 +1372,33 @@ BookStep::check(StrandContext const& ctx) const if (auto const prev = ctx.prevStep->directStepSrcAcct()) { auto const& view = ctx.view; - auto const& cur = book_.in.account; - - auto sle = view.read(keylet::line(*prev, cur, book_.in.currency)); - if (!sle) - return terNO_LINE; - if ((*sle)[sfFlags] & - ((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple)) - return terNO_RIPPLE; + auto const& cur = book_.in.getIssuer(); + + if (book_.in.holds()) + { + auto sle = view.read( + keylet::line(*prev, cur, book_.in.get().currency)); + if (!sle) + return terNO_LINE; + if ((*sle)[sfFlags] & + ((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple)) + return terNO_RIPPLE; + } + else + { + auto const mptID = + keylet::mptIssuance(book_.in.get().getMptID()); + if (!view.exists(mptID)) + return tecOBJECT_NOT_FOUND; + + if (auto const ter = isMPTDEXAllowed( + view, + book_.in, + book_.in.getIssuer(), + book_.in.getIssuer()); + ter != tesSUCCESS) + return ter; + } } } @@ -1410,32 +1419,38 @@ equalHelper(Step const& step, ripple::Book const& book) return false; } +static std::variant +getTypedAmt(Asset const& asset) +{ + static auto xrp = XRPAmount{}; + static auto mpt = MPTAmount{}; + static auto iou = IOUAmount{}; + if (asset.holds()) + { + if (isXRP(asset.get().currency)) + return &xrp; + return &iou; + } + return &mpt; +} + bool bookStepEqual(Step const& step, ripple::Book const& book) { - bool const inXRP = isXRP(book.in.currency); - bool const outXRP = isXRP(book.out.currency); - if (inXRP && outXRP) + if (isXRP(book.in) && isXRP(book.out)) { UNREACHABLE("ripple::test::bookStepEqual : no XRP to XRP book step"); return false; // no such thing as xrp/xrp book step } - if (inXRP && !outXRP) - return equalHelper< - XRPAmount, - IOUAmount, - BookPaymentStep>(step, book); - if (!inXRP && outXRP) - return equalHelper< - IOUAmount, - XRPAmount, - BookPaymentStep>(step, book); - if (!inXRP && !outXRP) - return equalHelper< - IOUAmount, - IOUAmount, - BookPaymentStep>(step, book); - return false; + bool ret = false; + std::visit( + [&](TIn*&&, TOut*&&) { + ret = + equalHelper>(step, book); + }, + getTypedAmt(book.in), + getTypedAmt(book.out)); + return ret; } } // namespace test @@ -1443,7 +1458,7 @@ bookStepEqual(Step const& step, ripple::Book const& book) template static std::pair> -make_BookStepHelper(StrandContext const& ctx, Issue const& in, Issue const& out) +make_BookStepHelper(StrandContext const& ctx, Asset const& in, Asset const& out) { TER ter = tefINTERNAL; std::unique_ptr r; @@ -1485,4 +1500,38 @@ make_BookStepXI(StrandContext const& ctx, Issue const& out) return make_BookStepHelper(ctx, xrpIssue(), out); } +// MPT's +std::pair> +make_BookStepMM( + StrandContext const& ctx, + MPTIssue const& in, + MPTIssue const& out) +{ + return make_BookStepHelper(ctx, in, out); +} + +std::pair> +make_BookStepMI(StrandContext const& ctx, MPTIssue const& in, Issue const& out) +{ + return make_BookStepHelper(ctx, in, out); +} + +std::pair> +make_BookStepIM(StrandContext const& ctx, Issue const& in, MPTIssue const& out) +{ + return make_BookStepHelper(ctx, in, out); +} + +std::pair> +make_BookStepMX(StrandContext const& ctx, MPTIssue const& in) +{ + return make_BookStepHelper(ctx, in, xrpIssue()); +} + +std::pair> +make_BookStepXM(StrandContext const& ctx, MPTIssue const& out) +{ + return make_BookStepHelper(ctx, xrpIssue(), out); +} + } // namespace ripple diff --git a/src/xrpld/app/paths/detail/DirectStep.cpp b/src/xrpld/app/paths/detail/DirectStep.cpp index 194e77edc2c..70e209053f6 100644 --- a/src/xrpld/app/paths/detail/DirectStep.cpp +++ b/src/xrpld/app/paths/detail/DirectStep.cpp @@ -722,7 +722,7 @@ DirectStepI::validFwd( auto const savCache = *cache_; - ASSERT(!in.native, "ripple::DirectStepI::validFwd : input is not XRP"); + ASSERT(!in.native(), "ripple::DirectStepI::validFwd : input is not XRP"); auto const [maxSrcToDst, srcDebtDir] = static_cast(this)->maxFlow(sb, cache_->srcToDst); @@ -731,7 +731,7 @@ DirectStepI::validFwd( try { boost::container::flat_set dummy; - fwdImp(sb, afView, dummy, in.iou); // changes cache + fwdImp(sb, afView, dummy, in.iou()); // changes cache } catch (FlowException const&) { @@ -939,13 +939,14 @@ DirectStepI::check(StrandContext const& ctx) const // issue if (auto book = ctx.prevStep->bookStepBook()) { - if (book->out != srcIssue) + if (book->out.holds() && + book->out.get() != srcIssue) return temBAD_PATH_LOOP; } } - if (!ctx.seenDirectIssues[0].insert(srcIssue).second || - !ctx.seenDirectIssues[1].insert(dstIssue).second) + if (!ctx.seenDirectAssets[0].insert(srcIssue).second || + !ctx.seenDirectAssets[1].insert(dstIssue).second) { JLOG(j_.debug()) << "DirectStepI: loop detected: Index: " << ctx.strandSize diff --git a/src/xrpld/app/paths/detail/FlowDebugInfo.h b/src/xrpld/app/paths/detail/FlowDebugInfo.h index 38e1a8cff5e..4fb3c0ad897 100644 --- a/src/xrpld/app/paths/detail/FlowDebugInfo.h +++ b/src/xrpld/app/paths/detail/FlowDebugInfo.h @@ -238,7 +238,7 @@ struct FlowDebugInfo std::vector const& amts, char delim = ';') { auto get_val = [](EitherAmount const& a) -> std::string { - return ripple::to_string(a.xrp); + return ripple::to_string(a.xrp()); }; write_list(amts, get_val, delim); }; @@ -246,7 +246,7 @@ struct FlowDebugInfo std::vector const& amts, char delim = ';') { auto get_val = [](EitherAmount const& a) -> std::string { - return ripple::to_string(a.iou); + return ripple::to_string(a.iou()); }; write_list(amts, get_val, delim); }; diff --git a/src/xrpld/app/paths/detail/MPTEndpointStep.cpp b/src/xrpld/app/paths/detail/MPTEndpointStep.cpp new file mode 100644 index 00000000000..7b6b1fad1aa --- /dev/null +++ b/src/xrpld/app/paths/detail/MPTEndpointStep.cpp @@ -0,0 +1,957 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace ripple { + +template +class MPTEndpointStep + : public StepImp> +{ +protected: + AccountID const src_; + AccountID const dst_; + MPTIssue const mptIssue_; + + // Charge transfer fees when the prev step redeems + Step const* const prevStep_ = nullptr; + bool const isLast_; + // Used by maxFlow's last step. + bool const isDirectBetweenHolders_; + beast::Journal const j_; + + struct Cache + { + MPTAmount in; + MPTAmount srcToDst; + MPTAmount out; + DebtDirection srcDebtDir; + + Cache( + MPTAmount const& in_, + MPTAmount const& srcToDst_, + MPTAmount const& out_, + DebtDirection srcDebtDir_) + : in(in_), srcToDst(srcToDst_), out(out_), srcDebtDir(srcDebtDir_) + { + } + }; + + std::optional cache_; + + // Compute the maximum value that can flow from src->dst at + // the best available quality. + // return: first element is max amount that can flow, + // second is the debt direction of the source w.r.t. the dst + std::pair + maxPaymentFlow(ReadView const& sb) const; + + // Compute srcQOut and dstQIn when the source redeems. + std::pair + qualitiesSrcRedeems(ReadView const& sb) const; + + // Compute srcQOut and dstQIn when the source issues. + std::pair + qualitiesSrcIssues(ReadView const& sb, DebtDirection prevStepDebtDirection) + const; + + // Returns srcQOut, dstQIn + std::pair + qualities( + ReadView const& sb, + DebtDirection srcDebtDir, + StrandDirection strandDir) const; + +public: + MPTEndpointStep( + StrandContext const& ctx, + AccountID const& src, + AccountID const& dst, + MPTID const& mpt) + : src_(src) + , dst_(dst) + , mptIssue_(mpt) + , prevStep_(ctx.prevStep) + , isLast_(ctx.isLast) + , isDirectBetweenHolders_( + mptIssue_ == ctx.strandDeliver && + ((ctx.isFirst && src != mptIssue_.getIssuer() && + dst_ != mptIssue_.getIssuer()) || + (ctx.prevStep && !ctx.prevStep->bookStepBook() && ctx.isLast && + dst_ == ctx.strandDst))) + , j_(ctx.j) + { + } + + AccountID const& + src() const + { + return src_; + } + AccountID const& + dst() const + { + return dst_; + } + MPTID const& + mptID() const + { + return mptIssue_.getMptID(); + } + + std::optional + cachedIn() const override + { + if (!cache_) + return std::nullopt; + return EitherAmount(cache_->in); + } + + std::optional + cachedOut() const override + { + if (!cache_) + return std::nullopt; + return EitherAmount(cache_->out); + } + + std::optional + directStepSrcAcct() const override + { + return src_; + } + + std::optional> + directStepAccts() const override + { + return std::make_pair(src_, dst_); + } + + DebtDirection + debtDirection(ReadView const& sb, StrandDirection dir) const override; + + std::uint32_t + lineQualityIn(ReadView const& v) const override; + + std::pair, DebtDirection> + qualityUpperBound(ReadView const& v, DebtDirection dir) const override; + + std::pair + revImp( + PaymentSandbox& sb, + ApplyView& afView, + boost::container::flat_set& ofrsToRm, + MPTAmount const& out); + + std::pair + fwdImp( + PaymentSandbox& sb, + ApplyView& afView, + boost::container::flat_set& ofrsToRm, + MPTAmount const& in); + + std::pair + validFwd(PaymentSandbox& sb, ApplyView& afView, EitherAmount const& in) + override; + + // Check for error, existing liquidity, and violations of auth/frozen + // constraints. + TER + check(StrandContext const& ctx) const; + + void + setCacheLimiting( + MPTAmount const& fwdIn, + MPTAmount const& fwdSrcToDst, + MPTAmount const& fwdOut, + DebtDirection srcDebtDir); + + friend bool + operator==(MPTEndpointStep const& lhs, MPTEndpointStep const& rhs) + { + return lhs.src_ == rhs.src_ && lhs.dst_ == rhs.dst_ && + lhs.mptIssue_ == rhs.mptIssue_; + } + + friend bool + operator!=(MPTEndpointStep const& lhs, MPTEndpointStep const& rhs) + { + return !(lhs == rhs); + } + +protected: + std::string + logStringImpl(char const* name) const + { + std::ostringstream ostr; + ostr << name << ": " + << "\nSrc: " << src_ << "\nDst: " << dst_; + return ostr.str(); + } + +private: + bool + equal(Step const& rhs) const override + { + if (auto ds = dynamic_cast(&rhs)) + { + return *this == *ds; + } + return false; + } +}; + +//------------------------------------------------------------------------------ + +// Flow is used in two different circumstances for transferring funds: +// o Payments, and +// o Offer crossing. +// The rules for handling funds in these two cases are almost, but not +// quite, the same. + +// Payment DirectStep class (not offer crossing). +class DirectMPTPaymentStep : public MPTEndpointStep +{ +public: + using MPTEndpointStep::MPTEndpointStep; + using MPTEndpointStep::check; + + bool + verifyPrevStepDebtDirection(DebtDirection) const + { + // A payment doesn't care regardless of prevStepRedeems. + return true; + } + + bool + verifyDstQualityIn(std::uint32_t dstQIn) const + { + // Payments have no particular expectations for what dstQIn will be. + return true; + } + + std::uint32_t + quality(ReadView const& sb, QualityDirection qDir) const; + + // Compute the maximum value that can flow from src->dst at + // the best available quality. + // return: first element is max amount that can flow, + // second is the debt direction w.r.t. the source account + std::pair + maxFlow(ReadView const& sb, MPTAmount const& desired) const; + + // Verify the consistency of the step. These checks are specific to + // payments and assume that general checks were already performed. + TER + check(StrandContext const& ctx, std::shared_ptr const& sleSrc) + const; + + std::string + logString() const override + { + return logStringImpl("DirectMPTPaymentStep"); + } +}; + +// Offer crossing DirectStep class (not a payment). +class DirectMPTOfferCrossingStep + : public MPTEndpointStep +{ +public: + using MPTEndpointStep::MPTEndpointStep; + using MPTEndpointStep::check; + + bool + verifyPrevStepDebtDirection(DebtDirection prevStepDir) const + { + // During offer crossing we rely on the fact that prevStepRedeems + // will *always* issue. That's because: + // o If there's a prevStep_, it will always be a BookStep. + // o BookStep::debtDirection() always returns `issues` when offer + // crossing. + // An assert based on this return value will tell us if that + // behavior changes. + return issues(prevStepDir); + } + + bool + verifyDstQualityIn(std::uint32_t dstQIn) const + { + // Due to a couple of factors dstQIn is always QUALITY_ONE for + // offer crossing. If that changes we need to know. + return dstQIn == QUALITY_ONE; + } + + std::uint32_t + quality(ReadView const& sb, QualityDirection qDir) const; + + // Compute the maximum value that can flow from src->dst at + // the best available quality. + // return: first element is max amount that can flow, + // second is the debt direction w.r.t the source + std::pair + maxFlow(ReadView const& sb, MPTAmount const& desired) const; + + // Verify the consistency of the step. These checks are specific to + // offer crossing and assume that general checks were already performed. + TER + check(StrandContext const& ctx, std::shared_ptr const& sleSrc) + const; + + std::string + logString() const override + { + return logStringImpl("DirectMPTOfferCrossingStep"); + } +}; + +//------------------------------------------------------------------------------ + +std::uint32_t +DirectMPTPaymentStep::quality(ReadView const& sb, QualityDirection qDir) const +{ + // There is no trust line Quality fields + return QUALITY_ONE; +} + +std::uint32_t +DirectMPTOfferCrossingStep::quality(ReadView const&, QualityDirection qDir) + const +{ + // There is no trust line Quality fields + return QUALITY_ONE; +} + +std::pair +DirectMPTPaymentStep::maxFlow(ReadView const& sb, MPTAmount const&) const +{ + return maxPaymentFlow(sb); +} + +std::pair +DirectMPTOfferCrossingStep::maxFlow( + ReadView const& sb, + MPTAmount const& desired) const +{ + if (isLast_) + return {desired, DebtDirection::issues}; + + return maxPaymentFlow(sb); +} + +TER +DirectMPTPaymentStep::check( + StrandContext const& ctx, + std::shared_ptr const& sleSrc) const +{ + auto const& mptID = mptIssue_.getMptID(); + // Since this is a payment, MPToken must be present. Perform all + // MPToken related checks. + if (!ctx.view.exists(keylet::mptIssuance(mptID))) + return tecOBJECT_NOT_FOUND; + + auto const& issuer = mptIssue_.getIssuer(); + if (src_ != issuer) + { + auto const key = keylet::mptoken(mptID, src_); + if (!ctx.view.exists(key)) + return tecNO_AUTH; + + if (auto const ter = requireAuth(ctx.view, mptIssue_, src_); + ter != tesSUCCESS) + return ter; + } + + if (dst_ != issuer) + { + auto const key = keylet::mptoken(mptID, dst_); + if (!ctx.view.exists(key)) + return tecNO_AUTH; + + if (auto const ter = requireAuth(ctx.view, mptIssue_, dst_); + ter != tesSUCCESS) + return ter; + } + + // Direct MPT payment + if (mptIssue_ == ctx.strandDeliver && + (ctx.isFirst || (ctx.prevStep && !ctx.prevStep->bookStepBook()))) + { + // Between holders + if (isDirectBetweenHolders_) + { + auto const& holder = ctx.isFirst ? src_ : dst_; + // Payment between the holders + if (isFrozen(ctx.view, holder, mptIssue_)) + return tecLOCKED; + + if (auto const ter = + canTransfer(ctx.view, mptIssue_, holder, ctx.strandDst); + ter != tesSUCCESS) + return ter; + } + } + // Cross-token MPT payment via DEX + else + { + auto const account = ctx.isFirst ? src_ : dst_; + if (auto const ter = isMPTDEXAllowed( + ctx.view, mptIssue_, account, mptIssue_.getIssuer()); + ter != tesSUCCESS) + return ter; + } + + return tesSUCCESS; +} + +TER +DirectMPTOfferCrossingStep::check( + StrandContext const& ctx, + std::shared_ptr const&) const +{ + auto const& holder = ctx.isFirst ? src_ : dst_; + auto const& issuer = mptIssue_.getIssuer(); + if (holder != issuer) + { + if (auto const ter = + isMPTDEXAllowed(ctx.view, mptIssue_, holder, issuer); + ter != tesSUCCESS) + return ter; + } + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +template +std::pair +MPTEndpointStep::maxPaymentFlow(ReadView const& sb) const +{ + if (src_ != mptIssue_.getIssuer()) + return { + toAmount(accountHolds( + sb, src_, mptIssue_, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_)), + DebtDirection::redeems}; + + if (auto const sle = sb.read(keylet::mptIssuance(mptIssue_.getMptID()))) + { + std::uint64_t const maximumAmount = [&] { + auto const max = sle->getFieldU64(sfMaximumAmount); + return max > 0 ? max : maxMPTokenAmount; + }(); + std::int64_t const maxFlow = + maximumAmount - sle->getFieldU64(sfOutstandingAmount); + + // Direct issue + if (!prevStep_) + return {MPTAmount{maxFlow}, DebtDirection::issues}; + + // TODO check limiting steps work and max amounts + // Transfer between accounts + return {MPTAmount(maximumAmount), DebtDirection::issues}; + } + + return {MPTAmount{0}, DebtDirection::issues}; +} + +template +DebtDirection +MPTEndpointStep::debtDirection( + ReadView const& sb, + StrandDirection dir) const +{ + if (dir == StrandDirection::forward && cache_) + return cache_->srcDebtDir; + + if (src_ != mptIssue_.getIssuer()) + return DebtDirection::redeems; + return DebtDirection::issues; +} + +template +std::pair +MPTEndpointStep::revImp( + PaymentSandbox& sb, + ApplyView& /*afView*/, + boost::container::flat_set& /*ofrsToRm*/, + MPTAmount const& out) +{ + cache_.reset(); + + auto const [maxSrcToDst, srcDebtDir] = + static_cast(this)->maxFlow(sb, out); + + auto const [srcQOut, dstQIn] = + qualities(sb, srcDebtDir, StrandDirection::reverse); + ASSERT( + static_cast(this)->verifyDstQualityIn(dstQIn), + "MPTEndpointStep::revImp : verify dst quaity in"); + + MPTIssue const srcToDstIss(mptIssue_); + + JLOG(j_.trace()) << "MPTEndpointStep::rev" + << " srcRedeems: " << redeems(srcDebtDir) + << " outReq: " << to_string(out) + << " maxSrcToDst: " << to_string(maxSrcToDst) + << " srcQOut: " << srcQOut << " dstQIn: " << dstQIn; + + if (maxSrcToDst.signum() <= 0) + { + JLOG(j_.trace()) << "MPTEndpointStep::rev: dry"; + cache_.emplace( + MPTAmount(beast::zero), + MPTAmount(beast::zero), + MPTAmount(beast::zero), + srcDebtDir); + return {beast::zero, beast::zero}; + } + + MPTAmount const srcToDst = + mulRatio(out, QUALITY_ONE, dstQIn, /*roundUp*/ true); + + if (srcToDst <= maxSrcToDst) + { + MPTAmount const in = + mulRatio(srcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true); + cache_.emplace(in, srcToDst, srcToDst, srcDebtDir); + auto const ter = rippleCredit( + sb, + src_, + dst_, + toSTAmount(srcToDst, srcToDstIss), + /*checkIssuer*/ false, + j_); + (void)ter; + JLOG(j_.trace()) << "MPTEndpointStep::rev: Non-limiting" + << " srcRedeems: " << redeems(srcDebtDir) + << " in: " << to_string(in) + << " srcToDst: " << to_string(srcToDst) + << " out: " << to_string(out); + return {in, out}; + } + + // limiting node + MPTAmount const in = + mulRatio(maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true); + MPTAmount const actualOut = + mulRatio(maxSrcToDst, dstQIn, QUALITY_ONE, /*roundUp*/ false); + cache_.emplace(in, maxSrcToDst, actualOut, srcDebtDir); + + auto const ter = rippleCredit( + sb, + src_, + dst_, + toSTAmount(maxSrcToDst, srcToDstIss), + /*checkIssuer*/ false, + j_); + (void)ter; + JLOG(j_.trace()) << "MPTEndpointStep::rev: Limiting" + << " srcRedeems: " << redeems(srcDebtDir) + << " in: " << to_string(in) + << " srcToDst: " << to_string(maxSrcToDst) + << " out: " << to_string(out); + return {in, actualOut}; +} + +// The forward pass should never have more liquidity than the reverse +// pass. But sometimes rounding differences cause the forward pass to +// deliver more liquidity. Use the cached values from the reverse pass +// to prevent this. +template +void +MPTEndpointStep::setCacheLimiting( + MPTAmount const& fwdIn, + MPTAmount const& fwdSrcToDst, + MPTAmount const& fwdOut, + DebtDirection srcDebtDir) +{ + if (cache_->in < fwdIn) + { + MPTAmount const smallDiff(1); + auto const diff = fwdIn - cache_->in; + if (diff > smallDiff) + { + if (!cache_->in.value() || + (double(fwdIn.value()) / double(cache_->in.value())) > 1.01) + { + // Detect large diffs on forward pass so they may be + // investigated + JLOG(j_.warn()) + << "MPTEndpointStep::fwd: setCacheLimiting" + << " fwdIn: " << to_string(fwdIn) + << " cacheIn: " << to_string(cache_->in) + << " fwdSrcToDst: " << to_string(fwdSrcToDst) + << " cacheSrcToDst: " << to_string(cache_->srcToDst) + << " fwdOut: " << to_string(fwdOut) + << " cacheOut: " << to_string(cache_->out); + cache_.emplace(fwdIn, fwdSrcToDst, fwdOut, srcDebtDir); + return; + } + } + } + cache_->in = fwdIn; + if (fwdSrcToDst < cache_->srcToDst) + cache_->srcToDst = fwdSrcToDst; + if (fwdOut < cache_->out) + cache_->out = fwdOut; + cache_->srcDebtDir = srcDebtDir; +}; + +template +std::pair +MPTEndpointStep::fwdImp( + PaymentSandbox& sb, + ApplyView& /*afView*/, + boost::container::flat_set& /*ofrsToRm*/, + MPTAmount const& in) +{ + ASSERT(cache_, "MPTEndpointStep::fwdImp : valid cache"); + + auto const [maxSrcToDst, srcDebtDir] = + static_cast(this)->maxFlow(sb, cache_->srcToDst); + + auto const [srcQOut, dstQIn] = + qualities(sb, srcDebtDir, StrandDirection::forward); + + MPTIssue const srcToDstIss(mptIssue_); + + JLOG(j_.trace()) << "MPTEndpointStep::fwd" + << " srcRedeems: " << redeems(srcDebtDir) + << " inReq: " << to_string(in) + << " maxSrcToDst: " << to_string(maxSrcToDst) + << " srcQOut: " << srcQOut << " dstQIn: " << dstQIn; + + if (maxSrcToDst.signum() <= 0) + { + JLOG(j_.trace()) << "MPTEndpointStep::fwd: dry"; + cache_.emplace( + MPTAmount(beast::zero), + MPTAmount(beast::zero), + MPTAmount(beast::zero), + srcDebtDir); + return {beast::zero, beast::zero}; + } + + MPTAmount const srcToDst = + mulRatio(in, QUALITY_ONE, srcQOut, /*roundUp*/ false); + + if (srcToDst <= maxSrcToDst) + { + MPTAmount const out = + mulRatio(srcToDst, dstQIn, QUALITY_ONE, /*roundUp*/ false); + setCacheLimiting(in, srcToDst, out, srcDebtDir); + auto const ter = rippleCredit( + sb, + src_, + dst_, + toSTAmount(cache_->srcToDst, srcToDstIss), + /*checkIssuer*/ false, + j_); + (void)ter; + JLOG(j_.trace()) << "MPTEndpointStep::fwd: Non-limiting" + << " srcRedeems: " << redeems(srcDebtDir) + << " in: " << to_string(in) + << " srcToDst: " << to_string(srcToDst) + << " out: " << to_string(out); + } + else + { + // limiting node + MPTAmount const actualIn = + mulRatio(maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true); + MPTAmount const out = + mulRatio(maxSrcToDst, dstQIn, QUALITY_ONE, /*roundUp*/ false); + setCacheLimiting(actualIn, maxSrcToDst, out, srcDebtDir); + auto const ter = rippleCredit( + sb, + src_, + dst_, + toSTAmount(cache_->srcToDst, srcToDstIss), + /*checkIssuer*/ false, + j_); + (void)ter; + JLOG(j_.trace()) << "MPTEndpointStep::rev: Limiting" + << " srcRedeems: " << redeems(srcDebtDir) + << " in: " << to_string(actualIn) + << " srcToDst: " << to_string(srcToDst) + << " out: " << to_string(out); + } + return {cache_->in, cache_->out}; +} + +template +std::pair +MPTEndpointStep::validFwd( + PaymentSandbox& sb, + ApplyView& afView, + EitherAmount const& in) +{ + if (!cache_) + { + JLOG(j_.trace()) << "Expected valid cache in validFwd"; + return {false, EitherAmount(MPTAmount(beast::zero))}; + } + + auto const savCache = *cache_; + + ASSERT( + !in.native() && !in.isIOU(), + "MPTEndpoint::validFwd : not XRP or IOU"); + + auto const [maxSrcToDst, srcDebtDir] = + static_cast(this)->maxFlow(sb, cache_->srcToDst); + (void)srcDebtDir; + + try + { + boost::container::flat_set dummy; + fwdImp(sb, afView, dummy, in.mpt()); // changes cache + } + catch (FlowException const&) + { + return {false, EitherAmount(MPTAmount(beast::zero))}; + } + + if (maxSrcToDst < cache_->srcToDst) + { + JLOG(j_.warn()) << "MPTEndpointStep: Strand re-execute check failed." + << " Exceeded max src->dst limit" + << " max src->dst: " << to_string(maxSrcToDst) + << " actual src->dst: " << to_string(cache_->srcToDst); + return {false, EitherAmount(cache_->out)}; + } + + if (!(checkNear(savCache.in, cache_->in) && + checkNear(savCache.out, cache_->out))) + { + JLOG(j_.warn()) << "MPTEndpointStep: Strand re-execute check failed." + << " ExpectedIn: " << to_string(savCache.in) + << " CachedIn: " << to_string(cache_->in) + << " ExpectedOut: " << to_string(savCache.out) + << " CachedOut: " << to_string(cache_->out); + return {false, EitherAmount(cache_->out)}; + } + return {true, EitherAmount(cache_->out)}; +} + +// Returns srcQOut, dstQIn +template +std::pair +MPTEndpointStep::qualitiesSrcRedeems(ReadView const& sb) const +{ + if (!prevStep_) + return {QUALITY_ONE, QUALITY_ONE}; + + auto const prevStepQIn = prevStep_->lineQualityIn(sb); + auto srcQOut = + static_cast(this)->quality(sb, QualityDirection::out); + + if (prevStepQIn > srcQOut) + srcQOut = prevStepQIn; + return {srcQOut, QUALITY_ONE}; +} + +// Returns srcQOut, dstQIn +template +std::pair +MPTEndpointStep::qualitiesSrcIssues( + ReadView const& sb, + DebtDirection prevStepDebtDirection) const +{ + // Charge a transfer rate when issuing and previous step redeems + + ASSERT( + static_cast(this)->verifyPrevStepDebtDirection( + prevStepDebtDirection), + "MPTEndpointStep::qualitiesSrcIssues : verify prev step debt " + "direction"); + + std::uint32_t const srcQOut = redeems(prevStepDebtDirection) + ? transferRate(sb, mptIssue_.getMptID()).value + : QUALITY_ONE; + auto dstQIn = + static_cast(this)->quality(sb, QualityDirection::in); + + if (isLast_ && dstQIn > QUALITY_ONE) + dstQIn = QUALITY_ONE; + return {srcQOut, dstQIn}; +} + +// Returns srcQOut, dstQIn +template +std::pair +MPTEndpointStep::qualities( + ReadView const& sb, + DebtDirection srcDebtDir, + StrandDirection strandDir) const +{ + if (redeems(srcDebtDir)) + { + return qualitiesSrcRedeems(sb); + } + else + { + auto const prevStepDebtDirection = [&] { + if (prevStep_) + return prevStep_->debtDirection(sb, strandDir); + return DebtDirection::issues; + }(); + return qualitiesSrcIssues(sb, prevStepDebtDirection); + } +} + +template +std::uint32_t +MPTEndpointStep::lineQualityIn(ReadView const& v) const +{ + // dst quality in + return static_cast(this)->quality(v, QualityDirection::in); +} + +template +std::pair, DebtDirection> +MPTEndpointStep::qualityUpperBound( + ReadView const& v, + DebtDirection prevStepDir) const +{ + auto const dir = this->debtDirection(v, StrandDirection::forward); + + if (!v.rules().enabled(fixQualityUpperBound)) + { + std::uint32_t const srcQOut = [&]() -> std::uint32_t { + if (redeems(prevStepDir) && issues(dir)) + return transferRate(v, mptIssue_.getMptID()).value; + return QUALITY_ONE; + }(); + auto dstQIn = static_cast(this)->quality( + v, QualityDirection::in); + + if (isLast_ && dstQIn > QUALITY_ONE) + dstQIn = QUALITY_ONE; + MPTIssue const iss{mptIssue_}; + return { + Quality(getRate(STAmount(iss, srcQOut), STAmount(iss, dstQIn))), + dir}; + } + + auto const [srcQOut, dstQIn] = redeems(dir) + ? qualitiesSrcRedeems(v) + : qualitiesSrcIssues(v, prevStepDir); + + MPTIssue const iss{mptIssue_}; + // Be careful not to switch the parameters to `getRate`. The + // `getRate(offerOut, offerIn)` function is usually used for offers. It + // returns offerIn/offerOut. For a direct step, the rate is srcQOut/dstQIn + // (Input*dstQIn/srcQOut = Output; So rate = srcQOut/dstQIn). Although the + // first parameter is called `offerOut`, it should take the `dstQIn` + // variable. + return { + Quality(getRate(STAmount(iss, dstQIn), STAmount(iss, srcQOut))), dir}; +} + +template +TER +MPTEndpointStep::check(StrandContext const& ctx) const +{ + // The following checks apply for both payments and offer crossing. + if (!src_ || !dst_) + { + JLOG(j_.debug()) << "MPTEndpointStep: specified bad account."; + return temBAD_PATH; + } + + if (src_ == dst_) + { + JLOG(j_.debug()) << "MPTEndpointStep: same src and dst."; + return temBAD_PATH; + } + + auto const sleSrc = ctx.view.read(keylet::account(src_)); + if (!sleSrc) + { + JLOG(j_.warn()) + << "MPTEndpointStep: can't receive MPT from non-existent issuer: " + << src_; + return terNO_ACCOUNT; + } + + // pure issue/redeem can't be frozen - can this happen? can only be an + // endpoint + if (!(ctx.isLast && ctx.isFirst)) + { + if (isFrozen(ctx.view, src_, mptIssue_) || + isFrozen(ctx.view, dst_, mptIssue_)) + return tecLOCKED; + } + + // MPT can only be an endpoint + if (!(ctx.isLast || ctx.isFirst)) + { + JLOG(j_.warn()) << "MPTEndpointStep: MPT can only be an endpoint"; + return terNO_RIPPLE; + } + + return static_cast(this)->check(ctx, sleSrc); +} + +//------------------------------------------------------------------------------ + +std::pair> +make_MPTEndpointStep( + StrandContext const& ctx, + AccountID const& src, + AccountID const& dst, + MPTID const& mpt) +{ + TER ter = tefINTERNAL; + std::unique_ptr r; + if (ctx.offerCrossing) + { + auto offerCrossingStep = + std::make_unique(ctx, src, dst, mpt); + ter = offerCrossingStep->check(ctx); + r = std::move(offerCrossingStep); + } + else // payment + { + auto paymentStep = + std::make_unique(ctx, src, dst, mpt); + ter = paymentStep->check(ctx); + r = std::move(paymentStep); + } + if (ter != tesSUCCESS) + return {ter, nullptr}; + + return {tesSUCCESS, std::move(r)}; +} + +} // namespace ripple diff --git a/src/xrpld/app/paths/detail/PathfinderUtils.h b/src/xrpld/app/paths/detail/PathfinderUtils.h index b06dded75bd..5010555868e 100644 --- a/src/xrpld/app/paths/detail/PathfinderUtils.h +++ b/src/xrpld/app/paths/detail/PathfinderUtils.h @@ -30,7 +30,9 @@ largestAmount(STAmount const& amt) if (amt.native()) return INITIAL_XRP; - return STAmount(amt.issue(), STAmount::cMaxValue, STAmount::cMaxOffset); + if (amt.holds()) + return STAmount(amt.asset(), STAmount::cMaxValue, STAmount::cMaxOffset); + return STAmount(amt.asset(), maxMPTokenAmount, 0); } inline STAmount diff --git a/src/xrpld/app/paths/detail/PaySteps.cpp b/src/xrpld/app/paths/detail/PaySteps.cpp index 56eae0aab3d..623087258d6 100644 --- a/src/xrpld/app/paths/detail/PaySteps.cpp +++ b/src/xrpld/app/paths/detail/PaySteps.cpp @@ -63,6 +63,12 @@ checkNear(XRPAmount const& expected, XRPAmount const& actual) return expected == actual; }; +bool +checkNear(MPTAmount const& expected, MPTAmount const& actual) +{ + return expected == actual; +}; + static bool isXRPAccount(STPathElement const& pe) { @@ -76,13 +82,13 @@ toStep( StrandContext const& ctx, STPathElement const* e1, STPathElement const* e2, - Issue const& curIssue) + Asset const& curAsset) { auto& j = ctx.j; if (ctx.isFirst && e1->isAccount() && (e1->getNodeType() & STPathElement::typeCurrency) && - isXRP(e1->getCurrency())) + e1->getPathAsset().isXRP()) { return make_XRPEndpointStep(ctx, e1->getAccountID()); } @@ -92,8 +98,17 @@ toStep( if (e1->isAccount() && e2->isAccount()) { + if (curAsset.holds()) + return make_MPTEndpointStep( + ctx, + e1->getAccountID(), + e2->getAccountID(), + curAsset.get().getMptID()); return make_DirectStepI( - ctx, e1->getAccountID(), e2->getAccountID(), curIssue.currency); + ctx, + e1->getAccountID(), + e2->getAccountID(), + curAsset.get().currency); } if (e1->isOffer() && e2->isAccount()) @@ -106,17 +121,17 @@ toStep( } ASSERT( - (e2->getNodeType() & STPathElement::typeCurrency) || + (e2->getNodeType() & STPathElement::typeAsset) || (e2->getNodeType() & STPathElement::typeIssuer), "ripple::toStep : currency or issuer"); - auto const outCurrency = e2->getNodeType() & STPathElement::typeCurrency - ? e2->getCurrency() - : curIssue.currency; + auto const outAsset = e2->getNodeType() & STPathElement::typeAsset + ? e2->getPathAsset() + : curAsset; auto const outIssuer = e2->getNodeType() & STPathElement::typeIssuer ? e2->getIssuerID() - : curIssue.account; + : curAsset.getIssuer(); - if (isXRP(curIssue.currency) && isXRP(outCurrency)) + if (isXRP(curAsset) && outAsset.isXRP()) { JLOG(j.info()) << "Found xrp/xrp offer payment step"; return {temBAD_PATH, std::unique_ptr{}}; @@ -124,13 +139,34 @@ toStep( ASSERT(e2->isOffer(), "ripple::toStep : is offer"); - if (isXRP(outCurrency)) - return make_BookStepIX(ctx, curIssue); + if (outAsset.isXRP()) + { + if (curAsset.holds()) + return make_BookStepMX(ctx, curAsset.get()); + return make_BookStepIX(ctx, curAsset.get()); + } - if (isXRP(curIssue.currency)) - return make_BookStepXI(ctx, {outCurrency, outIssuer}); + if (isXRP(curAsset)) + { + if (outAsset.holds()) + return make_BookStepXM(ctx, outAsset.get()); + return make_BookStepXI(ctx, {outAsset.get(), outIssuer}); + } - return make_BookStepII(ctx, curIssue, {outCurrency, outIssuer}); + if (curAsset.holds() && outAsset.holds()) + return make_BookStepMI( + ctx, + curAsset.get(), + {outAsset.get(), outIssuer}); + if (curAsset.holds() && outAsset.holds()) + return make_BookStepIM( + ctx, curAsset.get(), outAsset.get()); + + if (curAsset.holds()) + return make_BookStepMM( + ctx, curAsset.get(), outAsset.get()); + return make_BookStepII( + ctx, curAsset.get(), {outAsset.get(), outIssuer}); } std::pair @@ -138,9 +174,9 @@ toStrand( ReadView const& view, AccountID const& src, AccountID const& dst, - Issue const& deliver, + Asset const& deliver, std::optional const& limitQuality, - std::optional const& sendMaxIssue, + std::optional const& sendMaxAsset, STPath const& path, bool ownerPaysTransferFee, OfferCrossing offerCrossing, @@ -148,12 +184,17 @@ toStrand( beast::Journal j) { if (isXRP(src) || isXRP(dst) || !isConsistent(deliver) || - (sendMaxIssue && !isConsistent(*sendMaxIssue))) + (sendMaxAsset && !isConsistent(*sendMaxAsset))) return {temBAD_PATH, Strand{}}; - if ((sendMaxIssue && sendMaxIssue->account == noAccount()) || + if ((sendMaxAsset && sendMaxAsset->getIssuer() == noAccount()) || (src == noAccount()) || (dst == noAccount()) || - (deliver.account == noAccount())) + (deliver.getIssuer() == noAccount())) + return {temBAD_PATH, Strand{}}; + + if ((deliver.holds() && deliver.getIssuer() == beast::zero) || + (sendMaxAsset && sendMaxAsset->holds() && + sendMaxAsset->getIssuer() == beast::zero)) return {temBAD_PATH, Strand{}}; for (auto const& pe : path) @@ -166,6 +207,7 @@ toStrand( bool const hasAccount = t & STPathElement::typeAccount; bool const hasIssuer = t & STPathElement::typeIssuer; bool const hasCurrency = t & STPathElement::typeCurrency; + bool const hasMPT = t & STPathElement::typeMPT; if (hasAccount && (hasIssuer || hasCurrency)) return {temBAD_PATH, Strand{}}; @@ -185,18 +227,27 @@ toStrand( if (hasAccount && (pe.getAccountID() == noAccount())) return {temBAD_PATH, Strand{}}; + + if (hasMPT && (hasCurrency || hasAccount)) + return {temBAD_PATH, Strand{}}; + + if (hasMPT && hasIssuer && + (pe.getIssuerID() != getMPTIssuer(pe.getMPTID()))) + return {temBAD_PATH, Strand{}}; } - Issue curIssue = [&] { - auto const& currency = - sendMaxIssue ? sendMaxIssue->currency : deliver.currency; - if (isXRP(currency)) + Asset curAsset = [&]() -> Asset { + auto const& asset = sendMaxAsset ? *sendMaxAsset : deliver; + if (isXRP(asset)) return xrpIssue(); - return Issue{currency, src}; + if (asset.holds()) + return asset; + return Issue{asset.get().currency, src}; }(); - auto hasCurrency = [](STPathElement const pe) { - return pe.getNodeType() & STPathElement::typeCurrency; + // Currency or MPT + auto hasAsset = [](STPathElement const pe) { + return pe.getNodeType() & STPathElement::typeAsset; }; std::vector normPath; @@ -204,15 +255,26 @@ toStrand( // sendmax and deliver. normPath.reserve(4 + path.size()); { - normPath.emplace_back( - STPathElement::typeAll, src, curIssue.currency, curIssue.account); + // Implied step: sender of the transaction and either sendmax or deliver + // asset + auto const t = [&]() { + auto const t = + STPathElement::typeAccount | STPathElement::typeIssuer; + if (curAsset.holds()) + return t | STPathElement::typeMPT; + return t | STPathElement::typeCurrency; + }(); + normPath.emplace_back(t, src, curAsset, curAsset.getIssuer()); - if (sendMaxIssue && sendMaxIssue->account != src && + // If transaction includes sendmax with the issuer, which is not + // the sender then the issuer is the second implied step, unless + // the path starts at address, which is the issuer of sendmax + if (sendMaxAsset && sendMaxAsset->getIssuer() != src && (path.empty() || !path[0].isAccount() || - path[0].getAccountID() != sendMaxIssue->account)) + path[0].getAccountID() != sendMaxAsset->getIssuer())) { normPath.emplace_back( - sendMaxIssue->account, std::nullopt, std::nullopt); + sendMaxAsset->getIssuer(), std::nullopt, std::nullopt); } for (auto const& i : path) @@ -220,29 +282,34 @@ toStrand( { // Note that for offer crossing (only) we do use an offer book - // even if all that is changing is the Issue.account. - STPathElement const& lastCurrency = - *std::find_if(normPath.rbegin(), normPath.rend(), hasCurrency); - if ((lastCurrency.getCurrency() != deliver.currency) || + // even if all that is changing is the Issue/MPTIssue.account. + STPathElement const& lastAsset = + *std::find_if(normPath.rbegin(), normPath.rend(), hasAsset); + if (lastAsset.getPathAsset() != deliver || (offerCrossing && - lastCurrency.getIssuerID() != deliver.account)) + lastAsset.getIssuerID() != deliver.getIssuer())) { normPath.emplace_back( - std::nullopt, deliver.currency, deliver.account); + std::nullopt, deliver, deliver.getIssuer()); } } if (!((normPath.back().isAccount() && - normPath.back().getAccountID() == deliver.account) || - (dst == deliver.account))) + normPath.back().getAccountID() == deliver.getIssuer()) || + (dst == deliver.getIssuer()))) { - normPath.emplace_back(deliver.account, std::nullopt, std::nullopt); + normPath.emplace_back( + deliver.getIssuer(), + std::nullopt, + std::nullopt, + STPathElement::PathAssetTag{}); } if (!normPath.back().isAccount() || normPath.back().getAccountID() != dst) { - normPath.emplace_back(dst, std::nullopt, std::nullopt); + normPath.emplace_back( + dst, std::nullopt, std::nullopt, STPathElement::PathAssetTag{}); } } @@ -261,11 +328,11 @@ toStrand( at most twice: once as a src and once as a dst (hence the two element array). The strandSrc and strandDst will only show up once each. */ - std::array, 2> seenDirectIssues; + std::array, 2> seenDirectAssets; // A strand may not include the same offer book more than once - boost::container::flat_set seenBookOuts; - seenDirectIssues[0].reserve(normPath.size()); - seenDirectIssues[1].reserve(normPath.size()); + boost::container::flat_set seenBookOuts; + seenDirectAssets[0].reserve(normPath.size()); + seenDirectAssets[1].reserve(normPath.size()); seenBookOuts.reserve(normPath.size()); auto ctx = [&](bool isLast = false) { return StrandContext{ @@ -279,7 +346,7 @@ toStrand( ownerPaysTransferFee, offerCrossing, isDefaultPath, - seenDirectIssues, + seenDirectAssets, seenBookOuts, ammContext, j}; @@ -298,36 +365,61 @@ toStrand( auto cur = &normPath[i]; auto const next = &normPath[i + 1]; - if (cur->isAccount()) - curIssue.account = cur->getAccountID(); - else if (cur->hasIssuer()) - curIssue.account = cur->getIssuerID(); + // Switch over from MPT to Currency. + if (curAsset.holds() && cur->hasCurrency()) + curAsset = Issue{}; + + // Can only update the account for Issue since MPTIssue's account + // is immutable as it is part of MPTID + if (curAsset.holds()) + { + if (cur->isAccount()) + curAsset.get().account = cur->getAccountID(); + else if (cur->hasIssuer()) + curAsset.get().account = cur->getIssuerID(); + } if (cur->hasCurrency()) { - curIssue.currency = cur->getCurrency(); - if (isXRP(curIssue.currency)) - curIssue.account = xrpAccount(); + curAsset = Issue{cur->getCurrency(), curAsset.getIssuer()}; + if (isXRP(curAsset)) + curAsset.get().account = xrpAccount(); } + else if (cur->hasMPT()) + curAsset = cur->getPathAsset().get(); + + auto getImpliedStep = [&](AccountID const& src_, + AccountID const& dst_, + Asset const& asset_) { + if (asset_.holds()) + return make_MPTEndpointStep( + ctx(), src_, dst_, asset_.get().getMptID()); + return make_DirectStepI( + ctx(), src_, dst_, asset_.get().currency); + }; if (cur->isAccount() && next->isAccount()) { - if (!isXRP(curIssue.currency) && - curIssue.account != cur->getAccountID() && - curIssue.account != next->getAccountID()) + // TODO MPT This code never executes if curAsset is Currency + // since curAsset's account is set to cur's account above. + // It should not execute for MPT either because MPT rippling + // is invalid. Should this block be removed? + if (!isXRP(curAsset) && + curAsset.getIssuer() != cur->getAccountID() && + curAsset.getIssuer() != next->getAccountID()) { + if (curAsset.holds()) + Throw( + tefEXCEPTION, "MPT is invalid with rippling"); JLOG(j.trace()) << "Inserting implied account"; - auto msr = make_DirectStepI( - ctx(), - cur->getAccountID(), - curIssue.account, - curIssue.currency); + auto msr = getImpliedStep( + cur->getAccountID(), curAsset.getIssuer(), curAsset); if (msr.first != tesSUCCESS) return {msr.first, Strand{}}; result.push_back(std::move(msr.second)); impliedPE.emplace( STPathElement::typeAccount, - curIssue.account, + curAsset.getIssuer(), xrpCurrency(), xrpAccount()); cur = &*impliedPE; @@ -335,20 +427,21 @@ toStrand( } else if (cur->isAccount() && next->isOffer()) { - if (curIssue.account != cur->getAccountID()) + // TODO MPT Same as above. + if (curAsset.getIssuer() != cur->getAccountID()) { + if (curAsset.holds()) + Throw( + tefEXCEPTION, "MPT is invalid with rippling"); JLOG(j.trace()) << "Inserting implied account before offer"; - auto msr = make_DirectStepI( - ctx(), - cur->getAccountID(), - curIssue.account, - curIssue.currency); + auto msr = getImpliedStep( + cur->getAccountID(), curAsset.getIssuer(), curAsset); if (msr.first != tesSUCCESS) return {msr.first, Strand{}}; result.push_back(std::move(msr.second)); impliedPE.emplace( STPathElement::typeAccount, - curIssue.account, + curAsset.getIssuer(), xrpCurrency(), xrpAccount()); cur = &*impliedPE; @@ -356,10 +449,10 @@ toStrand( } else if (cur->isOffer() && next->isAccount()) { - if (curIssue.account != next->getAccountID() && + if (curAsset.getIssuer() != next->getAccountID() && !isXRP(next->getAccountID())) { - if (isXRP(curIssue)) + if (isXRP(curAsset)) { if (i != normPath.size() - 2) return {temBAD_PATH, Strand{}}; @@ -376,11 +469,8 @@ toStrand( else { JLOG(j.trace()) << "Inserting implied account after offer"; - auto msr = make_DirectStepI( - ctx(), - curIssue.account, - next->getAccountID(), - curIssue.currency); + auto msr = getImpliedStep( + curAsset.getIssuer(), next->getAccountID(), curAsset); if (msr.first != tesSUCCESS) return {msr.first, Strand{}}; result.push_back(std::move(msr.second)); @@ -389,8 +479,8 @@ toStrand( continue; } - if (!next->isOffer() && next->hasCurrency() && - next->getCurrency() != curIssue.currency) + if (!next->isOffer() && next->hasAsset() && + next->getPathAsset() != curAsset) { // Should never happen UNREACHABLE("ripple::toStrand : offer currency mismatch"); @@ -398,7 +488,7 @@ toStrand( } auto s = toStep( - ctx(/*isLast*/ i == normPath.size() - 2), cur, next, curIssue); + ctx(/*isLast*/ i == normPath.size() - 2), cur, next, curAsset); if (s.first == tesSUCCESS) result.emplace_back(std::move(s.second)); else @@ -413,19 +503,20 @@ toStrand( if (auto r = s.directStepAccts()) return *r; if (auto const r = s.bookStepBook()) - return std::make_pair(r->in.account, r->out.account); + return std::make_pair(r->in.getIssuer(), r->out.getIssuer()); Throw( tefEXCEPTION, "Step should be either a direct or book step"); return std::make_pair(xrpAccount(), xrpAccount()); }; auto curAcc = src; - auto curIss = [&] { - auto& currency = - sendMaxIssue ? sendMaxIssue->currency : deliver.currency; - if (isXRP(currency)) + auto curAsset = [&]() -> Asset { + auto const& asset = sendMaxAsset ? *sendMaxAsset : deliver; + if (isXRP(asset)) return xrpIssue(); - return Issue{currency, src}; + if (asset.holds()) + return asset; + return Issue{asset.get().currency, src}; }(); for (auto const& s : result) @@ -436,22 +527,31 @@ toStrand( if (auto const b = s->bookStepBook()) { - if (curIss != b->in) + if (curAsset != b->in) return false; - curIss = b->out; + curAsset = b->out; } - else + else if (curAsset.holds()) { - curIss.account = accts.second; + curAsset.get().account = accts.second; } curAcc = accts.second; } if (curAcc != dst) return false; - if (curIss.currency != deliver.currency) + if (curAsset.holds() != deliver.holds() || + (curAsset.holds() && + curAsset.get().currency != deliver.get().currency) || + (curAsset.holds() && + curAsset.get() != deliver.get())) + { + std::cout << to_string(curAsset) << std::endl; + std::cout << to_string(deliver) << std::endl; return false; - if (curIss.account != deliver.account && curIss.account != dst) + } + if (curAsset.getIssuer() != deliver.getIssuer() && + curAsset.getIssuer() != dst) return false; return true; }; @@ -471,9 +571,9 @@ toStrands( ReadView const& view, AccountID const& src, AccountID const& dst, - Issue const& deliver, + Asset const& deliver, std::optional const& limitQuality, - std::optional const& sendMax, + std::optional const& sendMax, STPathSet const& paths, bool addDefaultPath, bool ownerPaysTransferFee, @@ -586,14 +686,14 @@ StrandContext::StrandContext( // replicates the source or destination. AccountID const& strandSrc_, AccountID const& strandDst_, - Issue const& strandDeliver_, + Asset const& strandDeliver_, std::optional const& limitQuality_, bool isLast_, bool ownerPaysTransferFee_, OfferCrossing offerCrossing_, bool isDefaultPath_, - std::array, 2>& seenDirectIssues_, - boost::container::flat_set& seenBookOuts_, + std::array, 2>& seenDirectAssets_, + boost::container::flat_set& seenBookOuts_, AMMContext& ammContext_, beast::Journal j_) : view(view_) @@ -608,7 +708,7 @@ StrandContext::StrandContext( , isDefaultPath(isDefaultPath_) , strandSize(strand_.size()) , prevStep(!strand_.empty() ? strand_.back().get() : nullptr) - , seenDirectIssues(seenDirectIssues_) + , seenDirectAssets(seenDirectAssets_) , seenBookOuts(seenBookOuts_) , ammContext(ammContext_) , j(j_) @@ -635,5 +735,15 @@ template bool isDirectXrpToXrp(Strand const& strand); template bool isDirectXrpToXrp(Strand const& strand); +template bool +isDirectXrpToXrp(Strand const& strand); +template bool +isDirectXrpToXrp(Strand const& strand); +template bool +isDirectXrpToXrp(Strand const& strand); +template bool +isDirectXrpToXrp(Strand const& strand); +template bool +isDirectXrpToXrp(Strand const& strand); } // namespace ripple diff --git a/src/xrpld/app/paths/detail/Steps.h b/src/xrpld/app/paths/detail/Steps.h index dee90f617a5..5f700ce8e8d 100644 --- a/src/xrpld/app/paths/detail/Steps.h +++ b/src/xrpld/app/paths/detail/Steps.h @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -30,7 +31,10 @@ #include #include +extern bool gf; + namespace ripple { + class PaymentSandbox; class ReadView; class ApplyView; @@ -362,8 +366,8 @@ std::pair normalizePath( AccountID const& src, AccountID const& dst, - Issue const& deliver, - std::optional const& sendMaxIssue, + Asset const& deliver, + std::optional const& sendMaxAsset, STPath const& path); /** @@ -378,7 +382,7 @@ normalizePath( optimization. If, during direct offer crossing, the quality of the tip of the book drops below this value, then evaluating the strand can stop. - @param sendMaxIssue Optional asset to send. + @param sendMaxAsset Optional asset to send. @param path Liquidity sources to use for this strand of the payment. The path contains an ordered collection of the offer books to use and accounts to ripple through. @@ -394,9 +398,9 @@ toStrand( ReadView const& sb, AccountID const& src, AccountID const& dst, - Issue const& deliver, + Asset const& deliver, std::optional const& limitQuality, - std::optional const& sendMaxIssue, + std::optional const& sendMaxAsset, STPath const& path, bool ownerPaysTransferFee, OfferCrossing offerCrossing, @@ -433,9 +437,9 @@ toStrands( ReadView const& sb, AccountID const& src, AccountID const& dst, - Issue const& deliver, + Asset const& deliver, std::optional const& limitQuality, - std::optional const& sendMax, + std::optional const& sendMax, STPathSet const& paths, bool addDefaultPath, bool ownerPaysTransferFee, @@ -444,7 +448,7 @@ toStrands( beast::Journal j); /// @cond INTERNAL -template +template struct StepImp : public Step { explicit StepImp() = default; @@ -517,6 +521,8 @@ bool checkNear(IOUAmount const& expected, IOUAmount const& actual); bool checkNear(XRPAmount const& expected, XRPAmount const& actual); +bool +checkNear(MPTAmount const& expected, MPTAmount const& actual); /// @endcond /** @@ -527,7 +533,7 @@ struct StrandContext ReadView const& view; ///< Current ReadView AccountID const strandSrc; ///< Strand source account AccountID const strandDst; ///< Strand destination account - Issue const strandDeliver; ///< Issue strand delivers + Asset const strandDeliver; ///< Asset strand delivers std::optional const limitQuality; ///< Worst accepted quality bool const isFirst; ///< true if Step is first in Strand bool const isLast = false; ///< true if Step is last in Strand @@ -545,11 +551,11 @@ struct StrandContext at most twice: once as a src and once as a dst (hence the two element array). The strandSrc and strandDst will only show up once each. */ - std::array, 2>& seenDirectIssues; + std::array, 2>& seenDirectAssets; /** A strand may not include an offer that output the same issue more than once */ - boost::container::flat_set& seenBookOuts; + boost::container::flat_set& seenBookOuts; AMMContext& ammContext; beast::Journal const j; @@ -561,15 +567,15 @@ struct StrandContext // replicates the source or destination. AccountID const& strandSrc_, AccountID const& strandDst_, - Issue const& strandDeliver_, + Asset const& strandDeliver_, std::optional const& limitQuality_, bool isLast_, bool ownerPaysTransferFee_, OfferCrossing offerCrossing_, bool isDefaultPath_, - std::array, 2>& - seenDirectIssues_, ///< For detecting currency loops - boost::container::flat_set& + std::array, 2>& + seenDirectAssets_, ///< For detecting currency loops + boost::container::flat_set& seenBookOuts_, ///< For detecting book loops AMMContext& ammContext_, beast::Journal j_); ///< Journal for logging @@ -599,6 +605,13 @@ make_DirectStepI( AccountID const& dst, Currency const& c); +std::pair> +make_MPTEndpointStep( + StrandContext const& ctx, + AccountID const& src, + AccountID const& dst, + MPTID const& a); + std::pair> make_BookStepII(StrandContext const& ctx, Issue const& in, Issue const& out); @@ -611,6 +624,24 @@ make_BookStepXI(StrandContext const& ctx, Issue const& out); std::pair> make_XRPEndpointStep(StrandContext const& ctx, AccountID const& acc); +std::pair> +make_BookStepMM( + StrandContext const& ctx, + MPTIssue const& in, + MPTIssue const& out); + +std::pair> +make_BookStepMX(StrandContext const& ctx, MPTIssue const& in); + +std::pair> +make_BookStepXM(StrandContext const& ctx, MPTIssue const& out); + +std::pair> +make_BookStepMI(StrandContext const& ctx, MPTIssue const& in, Issue const& out); + +std::pair> +make_BookStepIM(StrandContext const& ctx, Issue const& in, MPTIssue const& out); + template bool isDirectXrpToXrp(Strand const& strand); diff --git a/src/xrpld/app/paths/detail/StrandFlow.h b/src/xrpld/app/paths/detail/StrandFlow.h index 72ca97a42ac..2a5bb331af3 100644 --- a/src/xrpld/app/paths/detail/StrandFlow.h +++ b/src/xrpld/app/paths/detail/StrandFlow.h @@ -367,6 +367,15 @@ qualityUpperBound(ReadView const& v, Strand const& strand) * increases quality of AMM steps, increasing the strand's composite * quality as the result. */ +inline MPTAmount +limitOut( + ReadView const& v, + Strand const& strand, + MPTAmount const& remainingOut, + Quality const& limitQuality) +{ + return remainingOut; +} template inline TOutAmt limitOut( @@ -403,9 +412,11 @@ limitOut( return XRPAmount{*out}; else if constexpr (std::is_same_v) return IOUAmount{*out}; + else if constexpr (std::is_same_v) + return MPTAmount{*out}; else return STAmount{ - remainingOut.issue(), out->mantissa(), out->exponent()}; + remainingOut.asset(), out->mantissa(), out->exponent()}; }(); // A tiny difference could be due to the round off if (withinRelativeDistance(out, remainingOut, Number(1, -9))) @@ -557,7 +568,7 @@ class ActiveStrands @return Actual amount in and out from the strands, errors, and payment sandbox */ -template +template FlowResult flow( PaymentSandbox const& baseView, diff --git a/src/xrpld/app/paths/detail/XRPEndpointStep.cpp b/src/xrpld/app/paths/detail/XRPEndpointStep.cpp index ebc9510f4b2..863eccd3959 100644 --- a/src/xrpld/app/paths/detail/XRPEndpointStep.cpp +++ b/src/xrpld/app/paths/detail/XRPEndpointStep.cpp @@ -201,7 +201,8 @@ class XRPEndpointOfferCrossingStep static std::int32_t computeReserveReduction(StrandContext const& ctx, AccountID const& acc) { - if (ctx.isFirst && !ctx.view.read(keylet::line(acc, ctx.strandDeliver))) + if (ctx.isFirst && ctx.strandDeliver.holds() && + !ctx.view.read(keylet::line(acc, ctx.strandDeliver.get()))) return -1; return 0; } @@ -310,9 +311,9 @@ XRPEndpointStep::validFwd( return {false, EitherAmount(XRPAmount(beast::zero))}; } - ASSERT(in.native, "ripple::XRPEndpointStep::validFwd : input is XRP"); + ASSERT(in.native(), "ripple::XRPEndpointStep::validFwd : input is XRP"); - auto const& xrpIn = in.xrp; + auto const& xrpIn = in.xrp(); auto const balance = static_cast(this)->xrpLiquid(sb); if (!isLast_ && balance < xrpIn) @@ -365,7 +366,7 @@ XRPEndpointStep::check(StrandContext const& ctx) const if (ctx.view.rules().enabled(fix1781)) { auto const issuesIndex = isLast_ ? 0 : 1; - if (!ctx.seenDirectIssues[issuesIndex].insert(xrpIssue()).second) + if (!ctx.seenDirectAssets[issuesIndex].insert(xrpIssue()).second) { JLOG(j_.debug()) << "XRPEndpointStep: loop detected: Index: " << ctx.strandSize diff --git a/src/xrpld/app/tx/detail/AMMBid.cpp b/src/xrpld/app/tx/detail/AMMBid.cpp index 29ac826a5de..856b2dbb4c5 100644 --- a/src/xrpld/app/tx/detail/AMMBid.cpp +++ b/src/xrpld/app/tx/detail/AMMBid.cpp @@ -37,6 +37,11 @@ AMMBid::preflight(PreflightContext const& ctx) if (!ammEnabled(ctx.rules)) return temDISABLED; + if (!ctx.rules.enabled(featureMPTokensV2) && + (ctx.tx[sfAsset].holds() || + ctx.tx[sfAsset2].holds())) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; diff --git a/src/xrpld/app/tx/detail/AMMClawback.cpp b/src/xrpld/app/tx/detail/AMMClawback.cpp index cd1e3008e97..55417e9ae75 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.cpp +++ b/src/xrpld/app/tx/detail/AMMClawback.cpp @@ -39,6 +39,11 @@ AMMClawback::preflight(PreflightContext const& ctx) if (!ctx.rules.enabled(featureAMMClawback)) return temDISABLED; + std::optional const clawAmount = ctx.tx[~sfAmount]; + if (!ctx.rules.enabled(featureMPTokensV2) && clawAmount && + clawAmount->holds()) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; // LCOV_EXCL_LINE @@ -56,14 +61,13 @@ AMMClawback::preflight(PreflightContext const& ctx) return temMALFORMED; } - std::optional const clawAmount = ctx.tx[~sfAmount]; auto const asset = ctx.tx[sfAsset]; auto const asset2 = ctx.tx[sfAsset2]; if (isXRP(asset)) return temMALFORMED; - if (flags & tfClawTwoAssets && asset.account != asset2.account) + if (flags & tfClawTwoAssets && asset.getIssuer() != asset2.getIssuer()) { JLOG(ctx.j.trace()) << "AMMClawback: tfClawTwoAssets can only be enabled when two " @@ -71,7 +75,7 @@ AMMClawback::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - if (asset.account != issuer) + if (asset.getIssuer() != issuer) { JLOG(ctx.j.trace()) << "AMMClawback: Asset's account does not " "match Account field."; @@ -139,8 +143,8 @@ AMMClawback::applyGuts(Sandbox& sb) std::optional const clawAmount = ctx_.tx[~sfAmount]; AccountID const issuer = ctx_.tx[sfAccount]; AccountID const holder = ctx_.tx[sfHolder]; - Issue const asset = ctx_.tx[sfAsset]; - Issue const asset2 = ctx_.tx[sfAsset2]; + Asset const asset = ctx_.tx[sfAsset]; + Asset const asset2 = ctx_.tx[sfAsset2]; auto ammSle = sb.peek(keylet::amm(asset, asset2)); if (!ammSle) @@ -157,6 +161,7 @@ AMMClawback::applyGuts(Sandbox& sb) asset, asset2, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, ctx_.journal); if (!expected) diff --git a/src/xrpld/app/tx/detail/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index 31773166d4a..0fc167e70cd 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,11 @@ AMMCreate::preflight(PreflightContext const& ctx) if (!ammEnabled(ctx.rules)) return temDISABLED; + if (!ctx.rules.enabled(featureMPTokensV2) && + (ctx.tx[sfAmount].holds() || + ctx.tx[sfAmount2].holds())) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -50,10 +56,10 @@ AMMCreate::preflight(PreflightContext const& ctx) auto const amount = ctx.tx[sfAmount]; auto const amount2 = ctx.tx[sfAmount2]; - if (amount.issue() == amount2.issue()) + if (amount.asset() == amount2.asset()) { JLOG(ctx.j.debug()) - << "AMM Instance: tokens can not have the same currency/issuer."; + << "AMM Instance: tokens can not have the same Issue/MPT."; return temBAD_AMM_TOKENS; } @@ -93,50 +99,50 @@ AMMCreate::preclaim(PreclaimContext const& ctx) auto const amount2 = ctx.tx[sfAmount2]; // Check if AMM already exists for the token pair - if (auto const ammKeylet = keylet::amm(amount.issue(), amount2.issue()); + if (auto const ammKeylet = keylet::amm(amount.asset(), amount2.asset()); ctx.view.read(ammKeylet)) { JLOG(ctx.j.debug()) << "AMM Instance: ltAMM already exists."; return tecDUPLICATE; } - if (auto const ter = requireAuth(ctx.view, amount.issue(), accountID); + if (auto const ter = requireAuth(ctx.view, amount.asset(), accountID); ter != tesSUCCESS) { JLOG(ctx.j.debug()) - << "AMM Instance: account is not authorized, " << amount.issue(); + << "AMM Instance: account is not authorized, " << amount.asset(); return ter; } - if (auto const ter = requireAuth(ctx.view, amount2.issue(), accountID); + if (auto const ter = requireAuth(ctx.view, amount2.asset(), accountID); ter != tesSUCCESS) { JLOG(ctx.j.debug()) - << "AMM Instance: account is not authorized, " << amount2.issue(); + << "AMM Instance: account is not authorized, " << amount2.asset(); return ter; } // Globally or individually frozen - if (isFrozen(ctx.view, accountID, amount.issue()) || - isFrozen(ctx.view, accountID, amount2.issue())) + if (isFrozen(ctx.view, accountID, amount.asset()) || + isFrozen(ctx.view, accountID, amount2.asset())) { JLOG(ctx.j.debug()) << "AMM Instance: involves frozen asset."; return tecFROZEN; } - auto noDefaultRipple = [](ReadView const& view, Issue const& issue) { - if (isXRP(issue)) + auto noDefaultRipple = [](ReadView const& view, Asset const& asset) { + if (asset.holds() || isXRP(asset)) return false; if (auto const issuerAccount = - view.read(keylet::account(issue.account))) + view.read(keylet::account(asset.getIssuer()))) return (issuerAccount->getFlags() & lsfDefaultRipple) == 0; return false; }; - if (noDefaultRipple(ctx.view, amount.issue()) || - noDefaultRipple(ctx.view, amount2.issue())) + if (noDefaultRipple(ctx.view, amount.asset()) || + noDefaultRipple(ctx.view, amount2.asset())) { JLOG(ctx.j.debug()) << "AMM Instance: DefaultRipple not set"; return terNO_RIPPLE; @@ -151,16 +157,17 @@ AMMCreate::preclaim(PreclaimContext const& ctx) return tecINSUF_RESERVE_LINE; } - auto insufficientBalance = [&](STAmount const& asset) { - if (isXRP(asset)) - return xrpBalance < asset; - return accountID != asset.issue().account && + auto insufficientBalance = [&](STAmount const& amount) { + if (isXRP(amount)) + return xrpBalance < amount; + return accountID != amount.asset().getIssuer() && accountHolds( ctx.view, accountID, - asset.issue(), + amount.asset(), FreezeHandling::fhZERO_IF_FROZEN, - ctx.j) < asset; + AuthHandling::ahZERO_IF_UNAUTHORIZED, + ctx.j) < amount; }; if (insufficientBalance(amount) || insufficientBalance(amount2)) @@ -172,7 +179,7 @@ AMMCreate::preclaim(PreclaimContext const& ctx) auto isLPToken = [&](STAmount const& amount) -> bool { if (auto const sle = - ctx.view.read(keylet::account(amount.issue().account))) + ctx.view.read(keylet::account(amount.asset().getIssuer()))) return sle->isFieldPresent(sfAMMID); return false; }; @@ -191,20 +198,45 @@ AMMCreate::preclaim(PreclaimContext const& ctx) // Disallow AMM if the issuer has clawback enabled when featureAMMClawback // is not enabled - auto clawbackDisabled = [&](Issue const& issue) -> TER { - if (isXRP(issue)) + auto clawbackDisabled = [&](Asset const& asset) -> TER { + if (isXRP(asset)) return tesSUCCESS; - if (auto const sle = ctx.view.read(keylet::account(issue.account)); - !sle) - return tecINTERNAL; - else if (sle->getFlags() & lsfAllowTrustLineClawback) - return tecNO_PERMISSION; + if (asset.holds()) + { + if (auto const sle = ctx.view.read( + keylet::mptIssuance(asset.get().getMptID())); + !sle) + return tecINTERNAL; + else if (sle->getFlags() & lsfMPTCanClawback) + return tecNO_PERMISSION; + } + else + { + if (auto const sle = + ctx.view.read(keylet::account(asset.getIssuer())); + !sle) + return tecINTERNAL; + else if (sle->getFlags() & lsfAllowTrustLineClawback) + return tecNO_PERMISSION; + } return tesSUCCESS; }; - if (auto const ter = clawbackDisabled(amount.issue()); ter != tesSUCCESS) + if (auto const ter = clawbackDisabled(amount.asset()); ter != tesSUCCESS) + return ter; + if (auto const ter = clawbackDisabled(amount2.asset()); ter != tesSUCCESS) + return ter; + + if (auto const ter = + isMPTTxAllowed(ctx.view, ttAMM_CREATE, amount.asset(), accountID); + ter != tesSUCCESS) return ter; - return clawbackDisabled(amount2.issue()); + if (auto const ter = + isMPTTxAllowed(ctx.view, ttAMM_CREATE, amount2.asset(), accountID); + ter != tesSUCCESS) + return ter; + + return tesSUCCESS; } static std::pair @@ -217,7 +249,7 @@ applyCreate( auto const amount = ctx_.tx[sfAmount]; auto const amount2 = ctx_.tx[sfAmount2]; - auto const ammKeylet = keylet::amm(amount.issue(), amount2.issue()); + auto const ammKeylet = keylet::amm(amount.asset(), amount2.asset()); // Mitigate same account exists possibility auto const ammAccount = [&]() -> Expected { @@ -240,8 +272,8 @@ applyCreate( } // LP Token already exists. (should not happen) - auto const lptIss = ammLPTIssue( - amount.issue().currency, amount2.issue().currency, *ammAccount); + auto const lptIss = + ammLPTIssue(amount.asset(), amount2.asset(), *ammAccount); if (sb.read(keylet::line(*ammAccount, lptIss))) { JLOG(j_.error()) << "AMM Instance: LP Token already exists."; @@ -279,9 +311,9 @@ applyCreate( auto ammSle = std::make_shared(ammKeylet); ammSle->setAccountID(sfAccount, *ammAccount); ammSle->setFieldAmount(sfLPTokenBalance, lpTokens); - auto const& [issue1, issue2] = std::minmax(amount.issue(), amount2.issue()); - ammSle->setFieldIssue(sfAsset, STIssue{sfAsset, issue1}); - ammSle->setFieldIssue(sfAsset2, STIssue{sfAsset2, issue2}); + auto const& [asset1, asset2] = std::minmax(amount.asset(), amount2.asset()); + ammSle->setFieldIssue(sfAsset, STIssue{sfAsset, asset1}); + ammSle->setFieldIssue(sfAsset2, STIssue{sfAsset2, asset2}); // AMM creator gets the auction slot and the voting slot. initializeFeeAuctionVote( ctx_.view(), ammSle, account_, lptIss, ctx_.tx[sfTradingFee]); @@ -309,7 +341,29 @@ applyCreate( return {res, false}; } - auto sendAndTrustSet = [&](STAmount const& amount) -> TER { + auto sendAndInitTrustOrMPT = [&](STAmount const& amount) -> TER { + // Authorize MPT + if (amount.holds()) + { + auto const mptokenKey = + keylet::mptoken(amount.get().getMptID(), *ammAccount); + + auto const ownerNode = sb.dirInsert( + keylet::ownerDir(*ammAccount), + mptokenKey, + describeOwnerDir(*ammAccount)); + + if (!ownerNode) + return tecDIR_FULL; + + auto mptoken = std::make_shared(mptokenKey); + (*mptoken)[sfAccount] = *ammAccount; + (*mptoken)[sfMPTokenIssuanceID] = amount.get().getMptID(); + (*mptoken)[sfFlags] = 0; + (*mptoken)[sfOwnerNode] = *ownerNode; + sb.insert(mptoken); + } + if (auto const res = accountSend( sb, account_, @@ -318,8 +372,9 @@ applyCreate( ctx_.journal, WaiveTransferFee::Yes)) return res; + // Set AMM flag on AMM trustline - if (!isXRP(amount)) + if (amount.holds() && !isXRP(amount)) { if (SLE::pointer sleRippleState = sb.peek(keylet::line(*ammAccount, amount.issue())); @@ -332,11 +387,12 @@ applyCreate( sb.update(sleRippleState); } } + return tesSUCCESS; }; // Send asset1. - res = sendAndTrustSet(amount); + res = sendAndInitTrustOrMPT(amount); if (res != tesSUCCESS) { JLOG(j_.debug()) << "AMM Instance: failed to send " << amount; @@ -344,7 +400,7 @@ applyCreate( } // Send asset2. - res = sendAndTrustSet(amount2); + res = sendAndInitTrustOrMPT(amount2); if (res != tesSUCCESS) { JLOG(j_.debug()) << "AMM Instance: failed to send " << amount2; @@ -355,15 +411,15 @@ applyCreate( << ammKeylet.key << " " << lpTokens << " " << amount << " " << amount2; auto addOrderBook = - [&](Issue const& issueIn, Issue const& issueOut, std::uint64_t uRate) { - Book const book{issueIn, issueOut}; + [&](Asset const& assetIn, Asset const& assetOut, std::uint64_t uRate) { + Book const book{assetIn, assetOut}; auto const dir = keylet::quality(keylet::book(book), uRate); if (auto const bookExisted = static_cast(sb.read(dir)); !bookExisted) ctx_.app.getOrderBookDB().addOrderBook(book); }; - addOrderBook(amount.issue(), amount2.issue(), getRate(amount2, amount)); - addOrderBook(amount2.issue(), amount.issue(), getRate(amount, amount2)); + addOrderBook(amount.asset(), amount2.asset(), getRate(amount2, amount)); + addOrderBook(amount2.asset(), amount.asset(), getRate(amount, amount2)); return {res, res == tesSUCCESS}; } diff --git a/src/xrpld/app/tx/detail/AMMDelete.cpp b/src/xrpld/app/tx/detail/AMMDelete.cpp index 89ce34052d2..4592bbedf2c 100644 --- a/src/xrpld/app/tx/detail/AMMDelete.cpp +++ b/src/xrpld/app/tx/detail/AMMDelete.cpp @@ -35,6 +35,11 @@ AMMDelete::preflight(PreflightContext const& ctx) if (!ammEnabled(ctx.rules)) return temDISABLED; + if (!ctx.rules.enabled(featureMPTokensV2) && + (ctx.tx[sfAsset].holds() || + ctx.tx[sfAsset2].holds())) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 21d48d42c9a..886144ff402 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -38,6 +39,13 @@ AMMDeposit::preflight(PreflightContext const& ctx) if (!ammEnabled(ctx.rules)) return temDISABLED; + if (!ctx.rules.enabled(featureMPTokensV2) && + (ctx.tx[sfAsset].holds() || + ctx.tx[sfAsset2].holds() || + ctx.tx[~sfAmount].value_or(STAmount{}).holds() || + ctx.tx[~sfAmount2].value_or(STAmount{}).holds())) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -108,10 +116,10 @@ AMMDeposit::preflight(PreflightContext const& ctx) return res; } - if (amount && amount2 && amount->issue() == amount2->issue()) + if (amount && amount2 && amount->asset() == amount2->asset()) { JLOG(ctx.j.debug()) << "AMM Deposit: invalid tokens, same issue." - << amount->issue() << " " << amount2->issue(); + << amount->asset() << " " << amount2->asset(); return temBAD_AMM_TOKENS; } @@ -149,7 +157,7 @@ AMMDeposit::preflight(PreflightContext const& ctx) if (auto const res = invalidAMMAmount( *ePrice, std::make_optional( - std::make_pair(amount->issue(), amount->issue())))) + std::make_pair(amount->asset(), amount->asset())))) { JLOG(ctx.j.debug()) << "AMM Deposit: invalid EPrice"; return res; @@ -184,6 +192,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) std::nullopt, std::nullopt, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, ctx.j); if (!expected) return expected.error(); // LCOV_EXCL_LINE @@ -233,12 +242,13 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) return tecUNFUNDED_AMM; return tecINSUF_RESERVE_LINE; } - return (accountID == deposit.issue().account || + return (accountID == deposit.asset().getIssuer() || accountHolds( ctx.view, accountID, - deposit.issue(), + deposit.asset(), FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, ctx.j) >= deposit) ? TER(tesSUCCESS) : tecUNFUNDED_AMM; @@ -248,7 +258,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) { // Check if either of the assets is frozen, AMMDeposit is not allowed // if either asset is frozen - auto checkAsset = [&](Issue const& asset) -> TER { + auto checkAsset = [&](Asset const& asset) -> TER { if (auto const ter = requireAuth(ctx.view, asset, accountID)) { JLOG(ctx.j.debug()) @@ -260,7 +270,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) { JLOG(ctx.j.debug()) << "AMM Deposit: account or currency is frozen, " - << to_string(accountID) << " " << to_string(asset.currency); + << to_string(accountID) << " " << to_string(asset); return tecFROZEN; } @@ -287,17 +297,17 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) // Account is not authorized to hold the assets it's depositing, // or it doesn't even have a trust line for them if (auto const ter = - requireAuth(ctx.view, amount->issue(), accountID)) + requireAuth(ctx.view, amount->asset(), accountID)) { // LCOV_EXCL_START JLOG(ctx.j.debug()) << "AMM Deposit: account is not authorized, " - << amount->issue(); + << amount->asset(); return ter; // LCOV_EXCL_STOP } // AMM account or currency frozen - if (isFrozen(ctx.view, ammAccountID, amount->issue())) + if (isFrozen(ctx.view, ammAccountID, amount->asset())) { JLOG(ctx.j.debug()) << "AMM Deposit: AMM account or currency is frozen, " @@ -305,11 +315,11 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) return tecFROZEN; } // Account frozen - if (isIndividualFrozen(ctx.view, accountID, amount->issue())) + if (isIndividualFrozen(ctx.view, accountID, amount->asset())) { JLOG(ctx.j.debug()) << "AMM Deposit: account is frozen, " << to_string(accountID) << " " - << to_string(amount->issue().currency); + << to_string(amount->asset()); return tecFROZEN; } if (checkBalance) @@ -364,6 +374,15 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) } } + if (auto const ter = + isMPTTxAllowed(ctx.view, ttAMM_DEPOSIT, ctx.tx[sfAsset], accountID); + ter != tesSUCCESS) + return ter; + if (auto const ter = isMPTTxAllowed( + ctx.view, ttAMM_DEPOSIT, ctx.tx[sfAsset2], accountID); + ter != tesSUCCESS) + return ter; + return tesSUCCESS; } @@ -382,9 +401,10 @@ AMMDeposit::applyGuts(Sandbox& sb) auto const expected = ammHolds( sb, *ammSle, - amount ? amount->issue() : std::optional{}, - amount2 ? amount2->issue() : std::optional{}, + amount ? amount->asset() : std::optional{}, + amount2 ? amount2->asset() : std::optional{}, FreezeHandling::fhZERO_IF_FROZEN, + AuthHandling::ahIGNORE_AUTH, ctx_.journal); if (!expected) return {expected.error(), false}; // LCOV_EXCL_LINE @@ -525,12 +545,13 @@ AMMDeposit::deposit( return tesSUCCESS; } else if ( - account_ == depositAmount.issue().account || + account_ == depositAmount.asset().getIssuer() || accountHolds( view, account_, - depositAmount.issue(), + depositAmount.asset(), FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, ctx_.journal) >= depositAmount) return tesSUCCESS; return tecUNFUNDED_AMM; @@ -651,8 +672,8 @@ AMMDeposit::equalDepositTokens( view, ammAccount, amountBalance, - multiply(amountBalance, frac, amountBalance.issue()), - multiply(amount2Balance, frac, amount2Balance.issue()), + multiply(amountBalance, frac, amountBalance.asset()), + multiply(amount2Balance, frac, amount2Balance.asset()), lptAMMBalance, lpTokensDeposit, depositMin, @@ -711,7 +732,7 @@ AMMDeposit::equalDepositLimit( std::uint16_t tfee) { auto frac = Number{amount} / amountBalance; - auto tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); + auto tokens = toSTAmount(lptAMMBalance.asset(), lptAMMBalance * frac); if (tokens == beast::zero) return {tecAMM_FAILED, STAmount{}}; auto const amount2Deposit = amount2Balance * frac; @@ -721,7 +742,7 @@ AMMDeposit::equalDepositLimit( ammAccount, amountBalance, amount, - toSTAmount(amount2Balance.issue(), amount2Deposit), + toSTAmount(amount2Balance.asset(), amount2Deposit), lptAMMBalance, tokens, std::nullopt, @@ -901,7 +922,7 @@ AMMDeposit::singleDepositEPrice( auto const b1 = c * c * f2 * f2 + 2 * c - d * d; auto const c1 = 2 * c * f2 * f2 + 1 - 2 * d * f2; auto const amountDeposit = toSTAmount( - amountBalance.issue(), + amountBalance.asset(), f1 * amountBalance * solveQuadraticEq(a1, b1, c1)); if (amountDeposit <= beast::zero) return {tecAMM_FAILED, STAmount{}}; diff --git a/src/xrpld/app/tx/detail/AMMVote.cpp b/src/xrpld/app/tx/detail/AMMVote.cpp index 1269bf4c383..e9c8b15be43 100644 --- a/src/xrpld/app/tx/detail/AMMVote.cpp +++ b/src/xrpld/app/tx/detail/AMMVote.cpp @@ -35,6 +35,11 @@ AMMVote::preflight(PreflightContext const& ctx) if (!ammEnabled(ctx.rules)) return temDISABLED; + if (!ctx.rules.enabled(featureMPTokensV2) && + (ctx.tx[sfAsset].holds() || + ctx.tx[sfAsset2].holds())) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index d1cfebe0639..f280c29b437 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -21,7 +21,9 @@ #include #include +#include #include +#include #include #include #include @@ -37,6 +39,13 @@ AMMWithdraw::preflight(PreflightContext const& ctx) if (!ammEnabled(ctx.rules)) return temDISABLED; + if (!ctx.rules.enabled(featureMPTokensV2) && + (ctx.tx[sfAsset].holds() || + ctx.tx[sfAsset2].holds() || + ctx.tx[~sfAmount].value_or(STAmount{}).holds() || + ctx.tx[~sfAmount2].value_or(STAmount{}).holds())) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -108,10 +117,10 @@ AMMWithdraw::preflight(PreflightContext const& ctx) return res; } - if (amount && amount2 && amount->issue() == amount2->issue()) + if (amount && amount2 && amount->asset() == amount2->asset()) { JLOG(ctx.j.debug()) << "AMM Withdraw: invalid tokens, same issue." - << amount->issue() << " " << amount2->issue(); + << amount->asset() << " " << amount2->asset(); return temBAD_AMM_TOKENS; } @@ -186,9 +195,10 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) auto const expected = ammHolds( ctx.view, *ammSle, - amount ? amount->issue() : std::optional{}, - amount2 ? amount2->issue() : std::optional{}, + amount ? amount->asset() : std::optional{}, + amount2 ? amount2->asset() : std::optional{}, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, ctx.j); if (!expected) return expected.error(); @@ -217,15 +227,15 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) return tecAMM_BALANCE; } if (auto const ter = - requireAuth(ctx.view, amount->issue(), accountID)) + requireAuth(ctx.view, amount->asset(), accountID)) { JLOG(ctx.j.debug()) << "AMM Withdraw: account is not authorized, " - << amount->issue(); + << amount->asset(); return ter; } // AMM account or currency frozen - if (isFrozen(ctx.view, ammAccountID, amount->issue())) + if (isFrozen(ctx.view, ammAccountID, amount->asset())) { JLOG(ctx.j.debug()) << "AMM Withdraw: AMM account or currency is frozen, " @@ -233,11 +243,11 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) return tecFROZEN; } // Account frozen - if (isIndividualFrozen(ctx.view, accountID, amount->issue())) + if (isIndividualFrozen(ctx.view, accountID, amount->asset())) { JLOG(ctx.j.debug()) << "AMM Withdraw: account is frozen, " << to_string(accountID) << " " - << to_string(amount->issue().currency); + << to_string(amount->asset()); return tecFROZEN; } } @@ -288,6 +298,15 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) return ter; } + if (auto const ter = isMPTTxAllowed( + ctx.view, ttAMM_WITHDRAW, ctx.tx[sfAsset], accountID); + ter != tesSUCCESS) + return ter; + if (auto const ter = isMPTTxAllowed( + ctx.view, ttAMM_WITHDRAW, ctx.tx[sfAsset2], accountID); + ter != tesSUCCESS) + return ter; + return tesSUCCESS; } @@ -339,9 +358,10 @@ AMMWithdraw::applyGuts(Sandbox& sb) auto const expected = ammHolds( sb, *ammSle, - amount ? amount->issue() : std::optional{}, - amount2 ? amount2->issue() : std::optional{}, + amount ? amount->asset() : std::optional{}, + amount2 ? amount2->asset() : std::optional{}, FreezeHandling::fhZERO_IF_FROZEN, + AuthHandling::ahIGNORE_AUTH, ctx_.journal); if (!expected) return {expected.error(), false}; @@ -501,6 +521,7 @@ AMMWithdraw::withdraw( amountWithdraw.issue(), std::nullopt, freezeHandling, + AuthHandling::ahIGNORE_AUTH, // ??? journal); // LCOV_EXCL_START if (!expected) @@ -719,8 +740,8 @@ AMMWithdraw::deleteAMMAccountIfEmpty( Sandbox& sb, std::shared_ptr const ammSle, STAmount const& lpTokenBalance, - Issue const& issue1, - Issue const& issue2, + Asset const& issue1, + Asset const& issue2, beast::Journal const& journal) { TER ter; @@ -786,9 +807,9 @@ AMMWithdraw::equalWithdrawTokens( auto const frac = divide(lpTokensWithdraw, lptAMMBalance, noIssue()); auto const withdrawAmount = - multiply(amountBalance, frac, amountBalance.issue()); + multiply(amountBalance, frac, amountBalance.asset()); auto const withdraw2Amount = - multiply(amount2Balance, frac, amount2Balance.issue()); + multiply(amount2Balance, frac, amount2Balance.asset()); // LP is making equal withdrawal by tokens but the requested amount // of LP tokens is likely too small and results in one-sided pool // withdrawal due to round off. Fail so the user withdraws @@ -869,7 +890,7 @@ AMMWithdraw::equalWithdrawLimit( ammAccount, amountBalance, amount, - toSTAmount(amount2.issue(), amount2Withdraw), + toSTAmount(amount2.asset(), amount2Withdraw), lptAMMBalance, toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), tfee); @@ -885,7 +906,7 @@ AMMWithdraw::equalWithdrawLimit( ammSle, ammAccount, amountBalance, - toSTAmount(amount.issue(), amountWithdraw), + toSTAmount(amount.asset(), amountWithdraw), amount2, lptAMMBalance, toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), @@ -1009,7 +1030,7 @@ AMMWithdraw::singleWithdrawEPrice( (lptAMMBalance * f - ae); if (tokens <= 0) return {tecAMM_FAILED, STAmount{}}; - auto const amountWithdraw = toSTAmount(amount.issue(), tokens / ePrice); + auto const amountWithdraw = toSTAmount(amount.asset(), tokens / ePrice); if (amount == beast::zero || amountWithdraw >= amount) { return withdraw( diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.h b/src/xrpld/app/tx/detail/AMMWithdraw.h index ae9328cb05e..2636aa26912 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.h +++ b/src/xrpld/app/tx/detail/AMMWithdraw.h @@ -154,8 +154,8 @@ class AMMWithdraw : public Transactor Sandbox& sb, std::shared_ptr const ammSle, STAmount const& lpTokenBalance, - Issue const& issue1, - Issue const& issue2, + Asset const& issue1, + Asset const& issue2, beast::Journal const& journal); private: diff --git a/src/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index 8b5ef79b6d4..8ba46c2723e 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -38,6 +38,14 @@ CashCheck::preflight(PreflightContext const& ctx) if (!ctx.rules.enabled(featureChecks)) return temDISABLED; + auto isMPT = [&](TypedField const& field) { + return ctx.tx.isFieldPresent(field) && ctx.tx[field].holds(); + }; + + if (!ctx.rules.enabled(featureMPTokensV2) && + (isMPT(sfAmount) || isMPT(sfDeliverMin))) + return temDISABLED; + NotTEC const ret{preflight1(ctx)}; if (!isTesSuccess(ret)) return ret; @@ -70,7 +78,7 @@ CashCheck::preflight(PreflightContext const& ctx) return temBAD_AMOUNT; } - if (badCurrency() == value.getCurrency()) + if (badCurrency() == value.asset()) { JLOG(ctx.j.warn()) << "Malformed transaction: Bad currency."; return temBAD_CURRENCY; @@ -141,8 +149,7 @@ CashCheck::preclaim(PreclaimContext const& ctx) }(ctx.tx)}; STAmount const sendMax = sleCheck->at(sfSendMax); - Currency const currency{value.getCurrency()}; - if (currency != sendMax.getCurrency()) + if (!equalTokens(value.asset(), sendMax.asset())) { JLOG(ctx.j.warn()) << "Check cash does not match check currency."; return temMALFORMED; @@ -167,6 +174,7 @@ CashCheck::preclaim(PreclaimContext const& ctx) sleCheck->at(sfAccount), value, fhZERO_IF_FROZEN, + ahIGNORE_AUTH, ctx.j)}; // Note that src will have one reserve's worth of additional XRP @@ -187,62 +195,103 @@ CashCheck::preclaim(PreclaimContext const& ctx) // An issuer can always accept their own currency. if (!value.native() && (value.getIssuer() != dstId)) { - auto const sleTrustLine = - ctx.view.read(keylet::line(dstId, issuerId, currency)); - - if (!sleTrustLine && - !ctx.view.rules().enabled(featureCheckCashMakesTrustLine)) + if (value.holds()) { - JLOG(ctx.j.warn()) - << "Cannot cash check for IOU without trustline."; - return tecNO_LINE; - } + Currency const currency{value.get().currency}; + auto const sleTrustLine = + ctx.view.read(keylet::line(dstId, issuerId, currency)); - auto const sleIssuer = ctx.view.read(keylet::account(issuerId)); - if (!sleIssuer) - { - JLOG(ctx.j.warn()) - << "Can't receive IOUs from non-existent issuer: " - << to_string(issuerId); - return tecNO_ISSUER; - } + if (!sleTrustLine && + !ctx.view.rules().enabled(featureCheckCashMakesTrustLine)) + { + JLOG(ctx.j.warn()) + << "Cannot cash check for IOU without trustline."; + return tecNO_LINE; + } - if (sleIssuer->at(sfFlags) & lsfRequireAuth) - { - if (!sleTrustLine) + auto const sleIssuer = ctx.view.read(keylet::account(issuerId)); + if (!sleIssuer) { - // We can only create a trust line if the issuer does not - // have requireAuth set. - return tecNO_AUTH; + JLOG(ctx.j.warn()) + << "Can't receive IOUs from non-existent issuer: " + << to_string(issuerId); + return tecNO_ISSUER; } - // Entries have a canonical representation, determined by a - // lexicographical "greater than" comparison employing strict - // weak ordering. Determine which entry we need to access. - bool const canonical_gt(dstId > issuerId); + if (sleIssuer->at(sfFlags) & lsfRequireAuth) + { + if (!sleTrustLine) + { + // We can only create a trust line if the issuer does + // not have requireAuth set. + return tecNO_AUTH; + } + + // Entries have a canonical representation, determined by a + // lexicographical "greater than" comparison employing + // strict weak ordering. Determine which entry we need to + // access. + bool const canonical_gt(dstId > issuerId); + + bool const is_authorized( + sleTrustLine->at(sfFlags) & + (canonical_gt ? lsfLowAuth : lsfHighAuth)); + + if (!is_authorized) + { + JLOG(ctx.j.warn()) + << "Can't receive IOUs from issuer without auth."; + return tecNO_AUTH; + } + } - bool const is_authorized( - sleTrustLine->at(sfFlags) & - (canonical_gt ? lsfLowAuth : lsfHighAuth)); + // The trustline from source to issuer does not need to + // be checked for freezing, since we already verified that the + // source has sufficient non-frozen funds available. - if (!is_authorized) + // However, the trustline from destination to issuer may not + // be frozen. + if (isFrozen(ctx.view, dstId, currency, issuerId)) { JLOG(ctx.j.warn()) - << "Can't receive IOUs from issuer without auth."; - return tecNO_AUTH; + << "Cashing a check to a frozen trustline."; + return tecFROZEN; } } + else + { + // MPT TODO + auto const sleIssuer = ctx.view.read(keylet::account(issuerId)); + if (!sleIssuer) + { + JLOG(ctx.j.warn()) + << "Can't receive MPTs from non-existent issuer: " + << to_string(issuerId); + return tecNO_ISSUER; + } - // The trustline from source to issuer does not need to - // be checked for freezing, since we already verified that the - // source has sufficient non-frozen funds available. + // Can't use requireAuth since it checks if MPToken exists. + auto const issuanceID = + keylet::mptIssuance(value.get().getMptID()); + if (auto const sle = ctx.view.read(issuanceID)) + { + if (sle->isFlag(lsfMPTRequireAuth)) + { + auto const mptokenID = + keylet::mptoken(issuanceID.key, dstId); + if (auto const mptSle = ctx.view.read(mptokenID); + !mptSle || !mptSle->isFlag(lsfMPTAuthorized)) + return tecFROZEN; + } + } + else + return tecOBJECT_NOT_FOUND; - // However, the trustline from destination to issuer may not - // be frozen. - if (isFrozen(ctx.view, dstId, currency, issuerId)) - { - JLOG(ctx.j.warn()) << "Cashing a check to a frozen trustline."; - return tecFROZEN; + if (isFrozen(ctx.view, dstId, value.asset().get())) + { + JLOG(ctx.j.warn()) << "Cashing a check to a frozen MPT."; + return tecFROZEN; + } } } } @@ -339,103 +388,150 @@ CashCheck::doApply() // maximum possible currency because there might be a gateway // transfer rate to account for. Since the transfer rate cannot // exceed 200%, we use 1/2 maxValue as our limit. + auto const maxDeliverMin = [&]() { + if (optDeliverMin->holds()) + return STAmount( + optDeliverMin->asset(), + STAmount::cMaxValue / 2, + STAmount::cMaxOffset); + return STAmount(optDeliverMin->asset(), maxMPTokenAmount / 2); + }; STAmount const flowDeliver{ - optDeliverMin ? STAmount( - optDeliverMin->issue(), - STAmount::cMaxValue / 2, - STAmount::cMaxOffset) + optDeliverMin ? maxDeliverMin() : ctx_.tx.getFieldAmount(sfAmount)}; - // If a trust line does not exist yet create one. - Issue const& trustLineIssue = flowDeliver.issue(); - AccountID const issuer = flowDeliver.getIssuer(); - AccountID const truster = issuer == account_ ? srcId : account_; - Keylet const trustLineKey = keylet::line(truster, trustLineIssue); - bool const destLow = issuer > account_; - bool const checkCashMakesTrustLine = psb.rules().enabled(featureCheckCashMakesTrustLine); - if (checkCashMakesTrustLine && !psb.exists(trustLineKey)) + std::optional trustLineKey; + STAmount savedLimit; + bool destLow = false; + if (flowDeliver.holds()) { - // 1. Can the check casher meet the reserve for the trust line? - // 2. Create trust line between destination (this) account - // and the issuer. - // 3. Apply correct noRipple settings on trust line. Use... - // a. this (destination) account and - // b. issuing account (not sending account). - - auto const sleDst = psb.peek(keylet::account(account_)); - - // Can the account cover the trust line's reserve? - if (std::uint32_t const ownerCount = {sleDst->at(sfOwnerCount)}; - mPriorBalance < psb.fees().accountReserve(ownerCount + 1)) + // If a trust line does not exist yet create one. + Issue const& trustLineIssue = flowDeliver.get(); + AccountID const issuer = flowDeliver.getIssuer(); + AccountID const truster = issuer == account_ ? srcId : account_; + trustLineKey = keylet::line(truster, trustLineIssue); + destLow = issuer > account_; + + if (checkCashMakesTrustLine && !psb.exists(*trustLineKey)) { - JLOG(j_.trace()) << "Trust line does not exist. " - "Insufficent reserve to create line."; - - return tecNO_LINE_INSUF_RESERVE; + // 1. Can the check casher meet the reserve for the trust + // line? + // 2. Create trust line between destination (this) account + // and the issuer. + // 3. Apply correct noRipple settings on trust line. Use... + // a. this (destination) account and + // b. issuing account (not sending account). + + auto const sleDst = psb.peek(keylet::account(account_)); + + // Can the account cover the trust line's reserve? + if (std::uint32_t const ownerCount = {sleDst->at( + sfOwnerCount)}; + mPriorBalance < + psb.fees().accountReserve(ownerCount + 1)) + { + JLOG(j_.trace()) + << "Trust line does not exist. " + "Insufficent reserve to create line."; + + return tecNO_LINE_INSUF_RESERVE; + } + + Currency const currency = + flowDeliver.asset().get().currency; + STAmount initialBalance(flowDeliver.asset()); + initialBalance.setIssuer(noAccount()); + + // clang-format off + if (TER const ter = trustCreate( + psb, // payment sandbox + destLow, // is dest low? + issuer, // source + account_, // destination + trustLineKey->key, // ledger index + sleDst, // Account to add to + false, // authorize account + (sleDst->getFlags() & lsfDefaultRipple) == 0, + false, // freeze trust line + initialBalance, // zero initial balance + Issue(currency, account_), // limit of zero + 0, // quality in + 0, // quality out + viewJ); // journal + !isTesSuccess(ter)) { + return ter; + } + // clang-format on + + psb.update(sleDst); + + // Note that we _don't_ need to be careful about destroying + // the trust line if the check cashing fails. The + // transaction machinery will automatically clean it up. } - Currency const currency = flowDeliver.getCurrency(); - STAmount initialBalance(flowDeliver.issue()); - initialBalance.setIssuer(noAccount()); - - // clang-format off - if (TER const ter = trustCreate( - psb, // payment sandbox - destLow, // is dest low? - issuer, // source - account_, // destination - trustLineKey.key, // ledger index - sleDst, // Account to add to - false, // authorize account - (sleDst->getFlags() & lsfDefaultRipple) == 0, - false, // freeze trust line - initialBalance, // zero initial balance - Issue(currency, account_), // limit of zero - 0, // quality in - 0, // quality out - viewJ); // journal - !isTesSuccess(ter)) - { - return ter; - } - // clang-format on + // Since the destination is signing the check, they clearly want + // the funds even if their new total funds would exceed the + // limit on their trust line. So we tweak the trust line limits + // before calling flow and then restore the trust line limits + // afterwards. + auto const sleTrustLine = psb.peek(*trustLineKey); + if (!sleTrustLine) + return tecNO_LINE; - psb.update(sleDst); + SF_AMOUNT const& tweakedLimit = + destLow ? sfLowLimit : sfHighLimit; + savedLimit = sleTrustLine->at(tweakedLimit); - // Note that we _don't_ need to be careful about destroying - // the trust line if the check cashing fails. The transaction - // machinery will automatically clean it up. + if (checkCashMakesTrustLine) + { + // Set the trust line limit to the highest possible value + // while flow runs. + STAmount const bigAmount( + trustLineIssue, + STAmount::cMaxValue, + STAmount::cMaxOffset); + sleTrustLine->at(tweakedLimit) = bigAmount; + } } - - // Since the destination is signing the check, they clearly want - // the funds even if their new total funds would exceed the limit - // on their trust line. So we tweak the trust line limits before - // calling flow and then restore the trust line limits afterwards. - auto const sleTrustLine = psb.peek(trustLineKey); - if (!sleTrustLine) - return tecNO_LINE; - - SF_AMOUNT const& tweakedLimit = destLow ? sfLowLimit : sfHighLimit; - STAmount const savedLimit = sleTrustLine->at(tweakedLimit); - - // Make sure the tweaked limits are restored when we leave scope. - scope_exit fixup( - [&psb, &trustLineKey, &tweakedLimit, &savedLimit]() { - if (auto const sleTrustLine = psb.peek(trustLineKey)) - sleTrustLine->at(tweakedLimit) = savedLimit; - }); - - if (checkCashMakesTrustLine) + else if (account_ != flowDeliver.getIssuer()) { - // Set the trust line limit to the highest possible value - // while flow runs. - STAmount const bigAmount( - trustLineIssue, STAmount::cMaxValue, STAmount::cMaxOffset); - sleTrustLine->at(tweakedLimit) = bigAmount; + // Create MPT if it doesn't exist + auto const mptokenKey = keylet::mptoken( + flowDeliver.get().getMptID(), account_); + if (!psb.exists(mptokenKey)) + { + auto const ownerNode = psb.dirInsert( + keylet::ownerDir(account_), + mptokenKey, + describeOwnerDir(account_)); + + if (!ownerNode) + return tecDIR_FULL; + + auto mptoken = std::make_shared(mptokenKey); + (*mptoken)[sfAccount] = account_; + (*mptoken)[sfMPTokenIssuanceID] = + flowDeliver.get().getMptID(); + (*mptoken)[sfFlags] = 0; + (*mptoken)[sfOwnerNode] = *ownerNode; + psb.insert(mptoken); + } } + // Make sure the tweaked limits are restored when we leave + // scope. + scope_exit fixup([&psb, &trustLineKey, destLow, &savedLimit]() { + if (trustLineKey) + { + SF_AMOUNT const& tweakedLimit = + destLow ? sfLowLimit : sfHighLimit; + if (auto const sleTrustLine = psb.peek(*trustLineKey)) + sleTrustLine->at(tweakedLimit) = savedLimit; + } + }); // Let flow() do the heavy lifting on a check for an IOU. auto const result = flow( @@ -467,14 +563,16 @@ CashCheck::doApply() << "flow did not produce DeliverMin."; return tecPATH_PARTIAL; } - if (doFix1623 && !checkCashMakesTrustLine) + if (doFix1623 && !checkCashMakesTrustLine && + optDeliverMin->holds()) // Set the delivered_amount metadata. ctx_.deliver(result.actualAmountOut); } // Set the delivered amount metadata in all cases, not just // for DeliverMin. - if (checkCashMakesTrustLine) + if (checkCashMakesTrustLine || + result.actualAmountOut.holds()) ctx_.deliver(result.actualAmountOut); sleCheck = psb.peek(keylet::check(ctx_.tx[sfCheckID])); diff --git a/src/xrpld/app/tx/detail/CreateCheck.cpp b/src/xrpld/app/tx/detail/CreateCheck.cpp index 3a278eed738..5449787242c 100644 --- a/src/xrpld/app/tx/detail/CreateCheck.cpp +++ b/src/xrpld/app/tx/detail/CreateCheck.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -34,6 +35,10 @@ CreateCheck::preflight(PreflightContext const& ctx) if (!ctx.rules.enabled(featureChecks)) return temDISABLED; + if (!ctx.rules.enabled(featureMPTokensV2) && + ctx.tx[sfSendMax].holds()) + return temDISABLED; + NotTEC const ret{preflight1(ctx)}; if (!isTesSuccess(ret)) return ret; @@ -60,7 +65,7 @@ CreateCheck::preflight(PreflightContext const& ctx) return temBAD_AMOUNT; } - if (badCurrency() == sendMax.getCurrency()) + if (badCurrency() == sendMax.asset()) { JLOG(ctx.j.warn()) << "Malformed transaction: Bad currency."; return temBAD_CURRENCY; @@ -120,39 +125,54 @@ CreateCheck::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "Creating a check for frozen asset"; return tecFROZEN; } - // If this account has a trustline for the currency, that - // trustline may not be frozen. - // - // Note that we DO allow create check for a currency that the - // account does not yet have a trustline to. - AccountID const srcId{ctx.tx.getAccountID(sfAccount)}; - if (issuerId != srcId) + if (sendMax.holds()) { - // Check if the issuer froze the line - auto const sleTrust = ctx.view.read( - keylet::line(srcId, issuerId, sendMax.getCurrency())); - if (sleTrust && - sleTrust->isFlag( - (issuerId > srcId) ? lsfHighFreeze : lsfLowFreeze)) + // If this account has a trustline for the currency, that + // trustline may not be frozen. + // + // Note that we DO allow create check for a currency that the + // account does not yet have a trustline to. + AccountID const srcId{ctx.tx.getAccountID(sfAccount)}; + if (issuerId != srcId) { - JLOG(ctx.j.warn()) - << "Creating a check for frozen trustline."; - return tecFROZEN; + // Check if the issuer froze the line + auto const sleTrust = ctx.view.read(keylet::line( + srcId, issuerId, sendMax.get().currency)); + if (sleTrust && + sleTrust->isFlag( + (issuerId > srcId) ? lsfHighFreeze : lsfLowFreeze)) + { + JLOG(ctx.j.warn()) + << "Creating a check for frozen trustline."; + return tecFROZEN; + } + } + if (issuerId != dstId) + { + // Check if dst froze the line. + auto const sleTrust = ctx.view.read(keylet::line( + issuerId, dstId, sendMax.get().currency)); + if (sleTrust && + sleTrust->isFlag( + (dstId > issuerId) ? lsfHighFreeze : lsfLowFreeze)) + { + JLOG(ctx.j.warn()) << "Creating a check for " + "destination frozen trustline."; + return tecFROZEN; + } } } - if (issuerId != dstId) + else { - // Check if dst froze the line. - auto const sleTrust = ctx.view.read( - keylet::line(issuerId, dstId, sendMax.getCurrency())); - if (sleTrust && - sleTrust->isFlag( - (dstId > issuerId) ? lsfHighFreeze : lsfLowFreeze)) - { - JLOG(ctx.j.warn()) - << "Creating a check for destination frozen trustline."; + auto const& mptIssue = sendMax.get(); + auto const& srcId = ctx.tx[sfAccount]; + // TODO MPT + if (srcId != mptIssue.getIssuer() && + isFrozen(ctx.view, srcId, mptIssue)) + return tecFROZEN; + if (dstId != mptIssue.getIssuer() && + isFrozen(ctx.view, dstId, mptIssue)) return tecFROZEN; - } } } } @@ -161,6 +181,15 @@ CreateCheck::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "Creating a check that has already expired."; return tecEXPIRED; } + + if (auto const ter = isMPTTxAllowed( + ctx.view, + ttCHECK_CREATE, + ctx.tx[sfSendMax].asset(), + ctx.tx[sfAccount]); + ter != tesSUCCESS) + return ter; + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index 578600621f2..8a5139f8c8d 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -42,6 +43,11 @@ CreateOffer::makeTxConsequences(PreflightContext const& ctx) NotTEC CreateOffer::preflight(PreflightContext const& ctx) { + if (!ctx.rules.enabled(featureMPTokensV2) && + (ctx.tx[sfTakerPays].holds() || + ctx.tx[sfTakerGets].holds())) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -98,18 +104,18 @@ CreateOffer::preflight(PreflightContext const& ctx) } auto const& uPaysIssuerID = saTakerPays.getIssuer(); - auto const& uPaysCurrency = saTakerPays.getCurrency(); + auto const& uPaysAsset = saTakerPays.asset(); auto const& uGetsIssuerID = saTakerGets.getIssuer(); - auto const& uGetsCurrency = saTakerGets.getCurrency(); + auto const& uGetsAsset = saTakerGets.asset(); - if (uPaysCurrency == uGetsCurrency && uPaysIssuerID == uGetsIssuerID) + if (uPaysAsset == uGetsAsset) { JLOG(j.debug()) << "Malformed offer: redundant (IOU for IOU)"; return temREDUNDANT; } // We don't allow a non-native currency to use the currency code XRP. - if (badCurrency() == uPaysCurrency || badCurrency() == uGetsCurrency) + if (badCurrency() == uPaysAsset || badCurrency() == uGetsAsset) { JLOG(j.debug()) << "Malformed offer: bad currency"; return temBAD_CURRENCY; @@ -134,7 +140,7 @@ CreateOffer::preclaim(PreclaimContext const& ctx) auto saTakerGets = ctx.tx[sfTakerGets]; auto const& uPaysIssuerID = saTakerPays.getIssuer(); - auto const& uPaysCurrency = saTakerPays.getCurrency(); + auto const& uPaysAsset = saTakerPays.asset(); auto const& uGetsIssuerID = saTakerGets.getIssuer(); @@ -155,8 +161,22 @@ CreateOffer::preclaim(PreclaimContext const& ctx) return tecFROZEN; } - if (accountFunds(ctx.view, id, saTakerGets, fhZERO_IF_FROZEN, viewJ) <= - beast::zero) + if (auto const ter = + isMPTTxAllowed(ctx.view, ttOFFER_CREATE, saTakerPays.asset(), id); + ter != tesSUCCESS) + return ter; + if (auto const ter = + isMPTTxAllowed(ctx.view, ttOFFER_CREATE, saTakerGets.asset(), id); + ter != tesSUCCESS) + return ter; + + if (accountFunds( + ctx.view, + id, + saTakerGets, + fhZERO_IF_FROZEN, + ahIGNORE_AUTH, + viewJ) <= beast::zero) { JLOG(ctx.j.debug()) << "delay: Offers must be at least partially funded."; @@ -188,12 +208,8 @@ CreateOffer::preclaim(PreclaimContext const& ctx) // Make sure that we are authorized to hold what the taker will pay us. if (!saTakerPays.native()) { - auto result = checkAcceptAsset( - ctx.view, - ctx.flags, - id, - ctx.j, - Issue(uPaysCurrency, uPaysIssuerID)); + auto result = + checkAcceptAsset(ctx.view, ctx.flags, id, ctx.j, uPaysAsset); if (result != tesSUCCESS) return result; } @@ -207,20 +223,20 @@ CreateOffer::checkAcceptAsset( ApplyFlags const flags, AccountID const id, beast::Journal const j, - Issue const& issue) + Asset const& asset) { // Only valid for custom currencies ASSERT( - !isXRP(issue.currency), + !isXRP(asset), "ripple::CreateOffer::checkAcceptAsset : input is not XRP"); - auto const issuerAccount = view.read(keylet::account(issue.account)); + auto const issuerAccount = view.read(keylet::account(asset.getIssuer())); if (!issuerAccount) { JLOG(j.debug()) << "delay: can't receive IOUs from non-existent issuer: " - << to_string(issue.account); + << to_string(asset.getIssuer()); return (flags & tapRETRY) ? TER{terNO_ACCOUNT} : TER{tecNO_ISSUER}; } @@ -228,454 +244,45 @@ CreateOffer::checkAcceptAsset( // This code is attached to the DepositPreauth amendment as a matter of // convenience. The change is not significant enough to deserve its // own amendment. - if (view.rules().enabled(featureDepositPreauth) && (issue.account == id)) + if (view.rules().enabled(featureDepositPreauth) && + (asset.getIssuer() == id)) // An account can always accept its own issuance. return tesSUCCESS; - if ((*issuerAccount)[sfFlags] & lsfRequireAuth) + if (asset.holds()) { - auto const trustLine = - view.read(keylet::line(id, issue.account, issue.currency)); - - if (!trustLine) + if ((*issuerAccount)[sfFlags] & lsfRequireAuth) { - return (flags & tapRETRY) ? TER{terNO_LINE} : TER{tecNO_LINE}; - } - - // Entries have a canonical representation, determined by a - // lexicographical "greater than" comparison employing strict weak - // ordering. Determine which entry we need to access. - bool const canonical_gt(id > issue.account); - - bool const is_authorized( - (*trustLine)[sfFlags] & (canonical_gt ? lsfLowAuth : lsfHighAuth)); - - if (!is_authorized) - { - JLOG(j.debug()) - << "delay: can't receive IOUs from issuer without auth."; - - return (flags & tapRETRY) ? TER{terNO_AUTH} : TER{tecNO_AUTH}; - } - } - - return tesSUCCESS; -} - -bool -CreateOffer::dry_offer(ApplyView& view, Offer const& offer) -{ - if (offer.fully_consumed()) - return true; - auto const amount = accountFunds( - view, - offer.owner(), - offer.amount().out, - fhZERO_IF_FROZEN, - ctx_.app.journal("View")); - return (amount <= beast::zero); -} - -std::pair -CreateOffer::select_path( - bool have_direct, - OfferStream const& direct, - bool have_bridge, - OfferStream const& leg1, - OfferStream const& leg2) -{ - // If we don't have any viable path, why are we here?! - ASSERT( - have_direct || have_bridge, - "ripple::CreateOffer::select_path : valid inputs"); - - // If there's no bridged path, the direct is the best by default. - if (!have_bridge) - return std::make_pair(true, direct.tip().quality()); - - Quality const bridged_quality( - composed_quality(leg1.tip().quality(), leg2.tip().quality())); - - if (have_direct) - { - // We compare the quality of the composed quality of the bridged - // offers and compare it against the direct offer to pick the best. - Quality const direct_quality(direct.tip().quality()); - - if (bridged_quality < direct_quality) - return std::make_pair(true, direct_quality); - } - - // Either there was no direct offer, or it didn't have a better quality - // than the bridge. - return std::make_pair(false, bridged_quality); -} - -bool -CreateOffer::reachedOfferCrossingLimit(Taker const& taker) const -{ - auto const crossings = - taker.get_direct_crossings() + (2 * taker.get_bridge_crossings()); - - // The crossing limit is part of the Ripple protocol and - // changing it is a transaction-processing change. - return crossings >= 850; -} - -std::pair -CreateOffer::bridged_cross( - Taker& taker, - ApplyView& view, - ApplyView& view_cancel, - NetClock::time_point const when) -{ - auto const& takerAmount = taker.original_offer(); - - ASSERT( - !isXRP(takerAmount.in) && !isXRP(takerAmount.out), - "ripple::CreateOffer::bridged_cross : neither is XRP"); - - if (isXRP(takerAmount.in) || isXRP(takerAmount.out)) - Throw("Bridging with XRP and an endpoint."); - - OfferStream offers_direct( - view, - view_cancel, - Book(taker.issue_in(), taker.issue_out()), - when, - stepCounter_, - j_); - - OfferStream offers_leg1( - view, - view_cancel, - Book(taker.issue_in(), xrpIssue()), - when, - stepCounter_, - j_); - - OfferStream offers_leg2( - view, - view_cancel, - Book(xrpIssue(), taker.issue_out()), - when, - stepCounter_, - j_); - - TER cross_result = tesSUCCESS; - - // Note the subtle distinction here: self-offers encountered in the - // bridge are taken, but self-offers encountered in the direct book - // are not. - bool have_bridge = offers_leg1.step() && offers_leg2.step(); - bool have_direct = step_account(offers_direct, taker); - int count = 0; - - auto viewJ = ctx_.app.journal("View"); - - // Modifying the order or logic of the operations in the loop will cause - // a protocol breaking change. - while (have_direct || have_bridge) - { - bool leg1_consumed = false; - bool leg2_consumed = false; - bool direct_consumed = false; - - auto const [use_direct, quality] = select_path( - have_direct, offers_direct, have_bridge, offers_leg1, offers_leg2); - - // We are always looking at the best quality; we are done with - // crossing as soon as we cross the quality boundary. - if (taker.reject(quality)) - break; - - count++; + auto const trustLine = view.read(keylet::line( + id, asset.getIssuer(), asset.get().currency)); - if (use_direct) - { - if (auto stream = j_.debug()) + if (!trustLine) { - stream << count << " Direct:"; - stream << " offer: " << offers_direct.tip(); - stream << " in: " << offers_direct.tip().amount().in; - stream << " out: " << offers_direct.tip().amount().out; - stream << " owner: " << offers_direct.tip().owner(); - stream << " funds: " - << accountFunds( - view, - offers_direct.tip().owner(), - offers_direct.tip().amount().out, - fhIGNORE_FREEZE, - viewJ); + return (flags & tapRETRY) ? TER{terNO_LINE} : TER{tecNO_LINE}; } - cross_result = taker.cross(offers_direct.tip()); + // Entries have a canonical representation, determined by a + // lexicographical "greater than" comparison employing strict weak + // ordering. Determine which entry we need to access. + bool const canonical_gt(id > asset.getIssuer()); - JLOG(j_.debug()) << "Direct Result: " << transToken(cross_result); + bool const is_authorized( + (*trustLine)[sfFlags] & + (canonical_gt ? lsfLowAuth : lsfHighAuth)); - if (dry_offer(view, offers_direct.tip())) + if (!is_authorized) { - direct_consumed = true; - have_direct = step_account(offers_direct, taker); - } - } - else - { - if (auto stream = j_.debug()) - { - auto const owner1_funds_before = accountFunds( - view, - offers_leg1.tip().owner(), - offers_leg1.tip().amount().out, - fhIGNORE_FREEZE, - viewJ); - - auto const owner2_funds_before = accountFunds( - view, - offers_leg2.tip().owner(), - offers_leg2.tip().amount().out, - fhIGNORE_FREEZE, - viewJ); - - stream << count << " Bridge:"; - stream << " offer1: " << offers_leg1.tip(); - stream << " in: " << offers_leg1.tip().amount().in; - stream << " out: " << offers_leg1.tip().amount().out; - stream << " owner: " << offers_leg1.tip().owner(); - stream << " funds: " << owner1_funds_before; - stream << " offer2: " << offers_leg2.tip(); - stream << " in: " << offers_leg2.tip().amount().in; - stream << " out: " << offers_leg2.tip().amount().out; - stream << " owner: " << offers_leg2.tip().owner(); - stream << " funds: " << owner2_funds_before; - } - - cross_result = taker.cross(offers_leg1.tip(), offers_leg2.tip()); - - JLOG(j_.debug()) << "Bridge Result: " << transToken(cross_result); + JLOG(j.debug()) + << "delay: can't receive IOUs from issuer without auth."; - if (view.rules().enabled(fixTakerDryOfferRemoval)) - { - // have_bridge can be true the next time 'round only if - // neither of the OfferStreams are dry. - leg1_consumed = dry_offer(view, offers_leg1.tip()); - if (leg1_consumed) - have_bridge &= offers_leg1.step(); - - leg2_consumed = dry_offer(view, offers_leg2.tip()); - if (leg2_consumed) - have_bridge &= offers_leg2.step(); - } - else - { - // This old behavior may leave an empty offer in the book for - // the second leg. - if (dry_offer(view, offers_leg1.tip())) - { - leg1_consumed = true; - have_bridge = (have_bridge && offers_leg1.step()); - } - if (dry_offer(view, offers_leg2.tip())) - { - leg2_consumed = true; - have_bridge = (have_bridge && offers_leg2.step()); - } + return (flags & tapRETRY) ? TER{terNO_AUTH} : TER{tecNO_AUTH}; } } - if (cross_result != tesSUCCESS) - { - cross_result = tecFAILED_PROCESSING; - break; - } - - if (taker.done()) - { - JLOG(j_.debug()) << "The taker reports he's done during crossing!"; - break; - } - - if (reachedOfferCrossingLimit(taker)) - { - JLOG(j_.debug()) << "The offer crossing limit has been exceeded!"; - break; - } - - // Postcondition: If we aren't done, then we *must* have consumed at - // least one offer fully. - ASSERT( - direct_consumed || leg1_consumed || leg2_consumed, - "ripple::CreateOffer::bridged_cross : consumed an offer"); - - if (!direct_consumed && !leg1_consumed && !leg2_consumed) - Throw( - "bridged crossing: nothing was fully consumed."); - } - - return std::make_pair(cross_result, taker.remaining_offer()); -} - -std::pair -CreateOffer::direct_cross( - Taker& taker, - ApplyView& view, - ApplyView& view_cancel, - NetClock::time_point const when) -{ - OfferStream offers( - view, - view_cancel, - Book(taker.issue_in(), taker.issue_out()), - when, - stepCounter_, - j_); - - TER cross_result(tesSUCCESS); - int count = 0; - - bool have_offer = step_account(offers, taker); - - // Modifying the order or logic of the operations in the loop will cause - // a protocol breaking change. - while (have_offer) - { - bool direct_consumed = false; - auto& offer(offers.tip()); - - // We are done with crossing as soon as we cross the quality boundary - if (taker.reject(offer.quality())) - break; - - count++; - - if (auto stream = j_.debug()) - { - stream << count << " Direct:"; - stream << " offer: " << offer; - stream << " in: " << offer.amount().in; - stream << " out: " << offer.amount().out; - stream << "quality: " << offer.quality(); - stream << " owner: " << offer.owner(); - stream << " funds: " - << accountFunds( - view, - offer.owner(), - offer.amount().out, - fhIGNORE_FREEZE, - ctx_.app.journal("View")); - } - - cross_result = taker.cross(offer); - - JLOG(j_.debug()) << "Direct Result: " << transToken(cross_result); - - if (dry_offer(view, offer)) - { - direct_consumed = true; - have_offer = step_account(offers, taker); - } - - if (cross_result != tesSUCCESS) - { - cross_result = tecFAILED_PROCESSING; - break; - } - - if (taker.done()) - { - JLOG(j_.debug()) << "The taker reports he's done during crossing!"; - break; - } - - if (reachedOfferCrossingLimit(taker)) - { - JLOG(j_.debug()) << "The offer crossing limit has been exceeded!"; - break; - } - - // Postcondition: If we aren't done, then we *must* have consumed the - // offer on the books fully! - ASSERT( - direct_consumed, - "ripple::CreateOffer::direct_cross : consumed an offer"); - - if (!direct_consumed) - Throw( - "direct crossing: nothing was fully consumed."); - } - - return std::make_pair(cross_result, taker.remaining_offer()); -} - -// Step through the stream for as long as possible, skipping any offers -// that are from the taker or which cross the taker's threshold. -// Return false if the is no offer in the book, true otherwise. -bool -CreateOffer::step_account(OfferStream& stream, Taker const& taker) -{ - while (stream.step()) - { - auto const& offer = stream.tip(); - - // This offer at the tip crosses the taker's threshold. We're done. - if (taker.reject(offer.quality())) - return true; - - // This offer at the tip is not from the taker. We're done. - if (offer.owner() != taker.account()) - return true; - } - - // We ran out of offers. Can't advance. - return false; -} - -// Fill as much of the offer as possible by consuming offers -// already on the books. Return the status and the amount of -// the offer to left unfilled. -std::pair -CreateOffer::takerCross( - Sandbox& sb, - Sandbox& sbCancel, - Amounts const& takerAmount) -{ - NetClock::time_point const when = sb.parentCloseTime(); - - beast::WrappedSink takerSink(j_, "Taker "); - - Taker taker( - cross_type_, - sb, - account_, - takerAmount, - ctx_.tx.getFlags(), - beast::Journal(takerSink)); - - // If the taker is unfunded before we begin crossing - // there's nothing to do - just return an error. - // - // We check this in preclaim, but when selling XRP - // charged fees can cause a user's available balance - // to go to 0 (by causing it to dip below the reserve) - // so we check this case again. - if (taker.unfunded()) - { - JLOG(j_.debug()) << "Not crossing: taker is unfunded."; - return {tecUNFUNDED_OFFER, takerAmount}; - } - - try - { - if (cross_type_ == CrossType::IouToIou) - return bridged_cross(taker, sb, sbCancel, when); - - return direct_cross(taker, sb, sbCancel, when); - } - catch (std::exception const& e) - { - JLOG(j_.error()) << "Exception during offer crossing: " << e.what(); - return {tecINTERNAL, taker.remaining_offer()}; + return tesSUCCESS; } + else + return requireAuth(view, asset.get(), id); } std::pair @@ -692,8 +299,8 @@ CreateOffer::flowCross( // We check this in preclaim, but when selling XRP charged fees can // cause a user's available balance to go to 0 (by causing it to dip // below the reserve) so we check this case again. - STAmount const inStartBalance = - accountFunds(psb, account_, takerAmount.in, fhZERO_IF_FROZEN, j_); + STAmount const inStartBalance = accountFunds( + psb, account_, takerAmount.in, fhZERO_IF_FROZEN, ahIGNORE_AUTH, j_); if (inStartBalance <= beast::zero) { // The account balance can't cover even part of the offer. @@ -708,13 +315,19 @@ CreateOffer::flowCross( STAmount sendMax = takerAmount.in; if (!sendMax.native() && (account_ != sendMax.getIssuer())) { - gatewayXferRate = transferRate(psb, sendMax.getIssuer()); + gatewayXferRate = [&]() { + if (sendMax.holds()) + return transferRate(psb, sendMax.getIssuer()); + else + return transferRate( + psb, sendMax.get().getMptID()); + }(); if (gatewayXferRate.value != QUALITY_ONE) { sendMax = multiplyRound( takerAmount.in, gatewayXferRate, - takerAmount.in.issue(), + takerAmount.in.asset(), true); } } @@ -741,7 +354,11 @@ CreateOffer::flowCross( if (!takerAmount.in.native() && !takerAmount.out.native()) { STPath path; - path.emplace_back(std::nullopt, xrpCurrency(), std::nullopt); + path.emplace_back( + std::nullopt, + xrpCurrency(), + std::nullopt, + STPathElement::PathAssetTag{}); paths.emplace_back(std::move(path)); } // Special handling for the tfSell flag. @@ -755,13 +372,15 @@ CreateOffer::flowCross( // we allow delivery of the largest possible amount. if (deliver.native()) deliver = STAmount{STAmount::cMaxNative}; + // We can't use the maximum possible currency here because + // there might be a gateway transfer rate to account for. + // Since the transfer rate cannot exceed 200%, we use 1/2 + // maxValue for our limit. + else if (deliver.holds()) + deliver = STAmount{deliver.asset(), maxMPTokenAmount / 2}; else - // We can't use the maximum possible currency here because - // there might be a gateway transfer rate to account for. - // Since the transfer rate cannot exceed 200%, we use 1/2 - // maxValue for our limit. deliver = STAmount{ - takerAmount.out.issue(), + takerAmount.out.asset(), STAmount::cMaxValue / 2, STAmount::cMaxOffset}; } @@ -795,7 +414,12 @@ CreateOffer::flowCross( if (isTesSuccess(result.result())) { STAmount const takerInBalance = accountFunds( - psb, account_, takerAmount.in, fhZERO_IF_FROZEN, j_); + psb, + account_, + takerAmount.in, + fhZERO_IF_FROZEN, + ahIGNORE_AUTH, + j_); if (takerInBalance <= beast::zero) { @@ -824,7 +448,7 @@ CreateOffer::flowCross( nonGatewayAmountIn = divideRound( result.actualAmountIn, gatewayXferRate, - takerAmount.in.issue(), + takerAmount.in.asset(), true); afterCross.in -= nonGatewayAmountIn; @@ -846,11 +470,11 @@ CreateOffer::flowCross( return divRoundStrict( afterCross.in, rate, - takerAmount.out.issue(), + takerAmount.out.asset(), false); return divRound( - afterCross.in, rate, takerAmount.out.issue(), true); + afterCross.in, rate, takerAmount.out.asset(), true); }(); } else @@ -865,7 +489,7 @@ CreateOffer::flowCross( if (afterCross.out < beast::zero) afterCross.out.clear(); afterCross.in = mulRound( - afterCross.out, rate, takerAmount.in.issue(), true); + afterCross.out, rate, takerAmount.in.asset(), true); } } } @@ -883,21 +507,11 @@ CreateOffer::flowCross( std::pair CreateOffer::cross(Sandbox& sb, Sandbox& sbCancel, Amounts const& takerAmount) { - if (sb.rules().enabled(featureFlowCross)) - { - PaymentSandbox psbFlow{&sb}; - PaymentSandbox psbCancelFlow{&sbCancel}; - auto const ret = flowCross(psbFlow, psbCancelFlow, takerAmount); - psbFlow.apply(sb); - psbCancelFlow.apply(sbCancel); - return ret; - } - - Sandbox sbTaker{&sb}; - Sandbox sbCancelTaker{&sbCancel}; - auto const ret = takerCross(sbTaker, sbCancelTaker, takerAmount); - sbTaker.apply(sb); - sbCancelTaker.apply(sbCancel); + PaymentSandbox psbFlow{&sb}; + PaymentSandbox psbCancelFlow{&sbCancel}; + auto const ret = flowCross(psbFlow, psbCancelFlow, takerAmount); + psbFlow.apply(sb); + psbCancelFlow.apply(sbCancel); return ret; } @@ -906,24 +520,13 @@ CreateOffer::format_amount(STAmount const& amount) { std::string txt = amount.getText(); txt += "/"; - txt += to_string(amount.issue().currency); + if (amount.holds()) + txt += to_string(amount.get().currency); + else + txt += to_string(amount.get()); return txt; } -void -CreateOffer::preCompute() -{ - cross_type_ = CrossType::IouToIou; - bool const pays_xrp = ctx_.tx.getFieldAmount(sfTakerPays).native(); - bool const gets_xrp = ctx_.tx.getFieldAmount(sfTakerGets).native(); - if (pays_xrp && !gets_xrp) - cross_type_ = CrossType::IouToXrp; - else if (gets_xrp && !pays_xrp) - cross_type_ = CrossType::XrpToIou; - - return Transactor::preCompute(); -} - std::pair CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) { @@ -1018,12 +621,12 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) if (bSell) { // this is a sell, round taker pays - saTakerPays = multiply(saTakerGets, rate, saTakerPays.issue()); + saTakerPays = multiply(saTakerGets, rate, saTakerPays.asset()); } else { // this is a buy, round taker gets - saTakerGets = divide(saTakerPays, rate, saTakerGets.issue()); + saTakerGets = divide(saTakerPays, rate, saTakerGets.asset()); } if (!saTakerGets || !saTakerPays) { @@ -1043,8 +646,8 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) Amounts place_offer; JLOG(j_.debug()) << "Attempting cross: " - << to_string(takerAmount.in.issue()) << " -> " - << to_string(takerAmount.out.issue()); + << to_string(takerAmount.in.asset()) << " -> " + << to_string(takerAmount.out.asset()); if (auto stream = j_.trace()) { @@ -1080,10 +683,10 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) } ASSERT( - saTakerGets.issue() == place_offer.in.issue(), + saTakerGets.asset() == place_offer.in.asset(), "ripple::CreateOffer::applyGuts : taker gets issue match"); ASSERT( - saTakerPays.issue() == place_offer.out.issue(), + saTakerPays.asset() == place_offer.out.asset(), "ripple::CreateOffer::applyGuts : taker pays issue match"); if (takerAmount != place_offer) @@ -1195,10 +798,10 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) // Update owner count. adjustOwnerCount(sb, sleCreator, 1, viewJ); - JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.issue()) - << " : " << to_string(saTakerGets.issue()); + JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.asset()) + << " : " << to_string(saTakerGets.asset()); - Book const book{saTakerPays.issue(), saTakerGets.issue()}; + Book const book{saTakerPays.asset(), saTakerGets.asset()}; // Add offer to order book, using the original rate // before any crossing occured. @@ -1206,10 +809,30 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) bool const bookExisted = static_cast(sb.peek(dir)); auto const bookNode = sb.dirAppend(dir, offer_index, [&](SLE::ref sle) { - sle->setFieldH160(sfTakerPaysCurrency, saTakerPays.issue().currency); - sle->setFieldH160(sfTakerPaysIssuer, saTakerPays.issue().account); - sle->setFieldH160(sfTakerGetsCurrency, saTakerGets.issue().currency); - sle->setFieldH160(sfTakerGetsIssuer, saTakerGets.issue().account); + if (saTakerPays.holds()) + { + sle->setFieldH160( + sfTakerPaysCurrency, saTakerPays.get().currency); + sle->setFieldH160( + sfTakerPaysIssuer, saTakerPays.get().account); + } + else + { + sle->setFieldH192( + sfTakerPaysMPT, saTakerPays.get().getMptID()); + } + if (saTakerGets.holds()) + { + sle->setFieldH160( + sfTakerGetsCurrency, saTakerGets.get().currency); + sle->setFieldH160( + sfTakerGetsIssuer, saTakerGets.get().account); + } + else + { + sle->setFieldH192( + sfTakerGetsMPT, saTakerGets.get().getMptID()); + } sle->setFieldU64(sfExchangeRate, uRate); }); diff --git a/src/xrpld/app/tx/detail/CreateOffer.h b/src/xrpld/app/tx/detail/CreateOffer.h index 234267804c9..5f130c29785 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.h +++ b/src/xrpld/app/tx/detail/CreateOffer.h @@ -21,7 +21,6 @@ #define RIPPLE_TX_CREATEOFFER_H_INCLUDED #include -#include #include #include @@ -37,8 +36,7 @@ class CreateOffer : public Transactor static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; /** Construct a Transactor subclass that creates an offer in the ledger. */ - explicit CreateOffer(ApplyContext& ctx) - : Transactor(ctx), stepCounter_(1000, j_) + explicit CreateOffer(ApplyContext& ctx) : Transactor(ctx) { } @@ -53,10 +51,6 @@ class CreateOffer : public Transactor static TER preclaim(PreclaimContext const& ctx); - /** Gather information beyond what the Transactor base class gathers. */ - void - preCompute() override; - /** Precondition: fee collection is likely. Attempt to create the offer. */ TER doApply() override; @@ -72,50 +66,7 @@ class CreateOffer : public Transactor ApplyFlags const flags, AccountID const id, beast::Journal const j, - Issue const& issue); - - bool - dry_offer(ApplyView& view, Offer const& offer); - - static std::pair - select_path( - bool have_direct, - OfferStream const& direct, - bool have_bridge, - OfferStream const& leg1, - OfferStream const& leg2); - - std::pair - bridged_cross( - Taker& taker, - ApplyView& view, - ApplyView& view_cancel, - NetClock::time_point const when); - - std::pair - direct_cross( - Taker& taker, - ApplyView& view, - ApplyView& view_cancel, - NetClock::time_point const when); - - // Step through the stream for as long as possible, skipping any offers - // that are from the taker or which cross the taker's threshold. - // Return false if the is no offer in the book, true otherwise. - static bool - step_account(OfferStream& stream, Taker const& taker); - - // True if the number of offers that have been crossed - // exceeds the limit. - bool - reachedOfferCrossingLimit(Taker const& taker) const; - - // Fill offer as much as possible by consuming offers already on the books, - // and adjusting account balances accordingly. - // - // Charges fees on top to taker. - std::pair - takerCross(Sandbox& sb, Sandbox& sbCancel, Amounts const& takerAmount); + Asset const& asset); // Use the payment flow code to perform offer crossing. std::pair @@ -133,13 +84,6 @@ class CreateOffer : public Transactor static std::string format_amount(STAmount const& amount); - -private: - // What kind of offer we are placing - CrossType cross_type_; - - // The number of steps to take through order books while crossing - OfferStream::StepCounter stepCounter_; }; using OfferCreate = CreateOffer; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 5101eba60aa..cf2ee459dca 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -926,8 +926,23 @@ ValidClawback::finalize( 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); + STAmount const holderBalance = [&]() { + if (amount.holds()) + return accountHolds( + view, + holder, + amount.get().currency, + issuer, + fhIGNORE_FREEZE, + j); + return accountHolds( + view, + holder, + amount.get(), + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + j); + }(); if (holderBalance.signum() < 0) { @@ -1100,6 +1115,66 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensCreated_ == 0 && mptokensDeleted_ == 0; } + + if (tx.getTxnType() == ttAMM_CREATE || tx.getTxnType() == ttCHECK_CASH) + { + 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"; + } + // AMM can be created with IOU/MPT or MPT/MPT + else if ( + (tx.getTxnType() == ttAMM_CREATE && mptokensCreated_ > 2) || + (tx.getTxnType() == ttCHECK_CASH && mptokensCreated_ > 1)) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while creating MPTokens"; + } + + return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && + ((tx.getTxnType() == ttAMM_CREATE && mptokensCreated_ <= 2) || + (tx.getTxnType() == ttCHECK_CASH && mptokensCreated_ <= 1)) && + mptokensDeleted_ == 0; + } + + if (tx.getTxnType() == ttAMM_DELETE || + tx.getTxnType() == ttAMM_WITHDRAW) + { + 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_ > 2) + { + 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_ <= 2; + } } if (mptIssuancesCreated_ != 0) diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index 79dc1734b5b..ea987c464d2 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.h +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -27,7 +27,7 @@ namespace ripple { struct MPTAuthorizeArgs { XRPAmount const& priorBalance; - uint192 const& mptIssuanceID; + MPTID const& mptIssuanceID; AccountID const& account; std::uint32_t flags; std::optional holderID; diff --git a/src/xrpld/app/tx/detail/Offer.h b/src/xrpld/app/tx/detail/Offer.h index 23129952c3d..38eb36b1804 100644 --- a/src/xrpld/app/tx/detail/Offer.h +++ b/src/xrpld/app/tx/detail/Offer.h @@ -35,8 +35,8 @@ template class TOfferBase { protected: - Issue issIn_; - Issue issOut_; + Asset assetIn_; + Asset assetOut_; }; template <> @@ -132,10 +132,10 @@ class TOffer : private TOfferBase return m_entry->key(); } - Issue const& - issueIn() const; - Issue const& - issueOut() const; + Asset const& + assetIn() const; + Asset const& + assetOut() const; TAmounts limitOut( @@ -155,7 +155,7 @@ class TOffer : private TOfferBase isFunded() const { // Offer owner is issuer; they have unlimited funds - return m_account == issueOut().account; + return m_account == assetOut().getIssuer(); } static std::pair @@ -187,8 +187,8 @@ TOffer::TOffer(SLE::pointer const& entry, Quality quality) auto const tg = m_entry->getFieldAmount(sfTakerGets); m_amounts.in = toAmount(tp); m_amounts.out = toAmount(tg); - this->issIn_ = tp.issue(); - this->issOut_ = tg.issue(); + this->assetIn_ = tp.asset(); + this->assetOut_ = tg.asset(); } template <> @@ -208,11 +208,21 @@ template void TOffer::setFieldAmounts() { -#ifdef _MSC_VER - UNREACHABLE("ripple::TOffer::setFieldAmounts : must be specialized"); -#else - static_assert(sizeof(TOut) == -1, "Must be specialized"); -#endif + if constexpr (std::is_same_v) + m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in)); + else if constexpr (std::is_same_v) + m_entry->setFieldAmount(sfTakerPays, m_amounts.in); + else + m_entry->setFieldAmount( + sfTakerPays, toSTAmount(m_amounts.in, assetIn())); + + if constexpr (std::is_same_v) + m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out)); + else if constexpr (std::is_same_v) + m_entry->setFieldAmount(sfTakerGets, m_amounts.out); + else + m_entry->setFieldAmount( + sfTakerGets, toSTAmount(m_amounts.out, assetOut())); } template @@ -257,64 +267,32 @@ TOffer::send(Args&&... args) return accountSend(std::forward(args)...); } -template <> -inline void -TOffer::setFieldAmounts() -{ - m_entry->setFieldAmount(sfTakerPays, m_amounts.in); - m_entry->setFieldAmount(sfTakerGets, m_amounts.out); -} - -template <> -inline void -TOffer::setFieldAmounts() -{ - m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in, issIn_)); - m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out, issOut_)); -} - -template <> -inline void -TOffer::setFieldAmounts() -{ - m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in, issIn_)); - m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out)); -} - -template <> -inline void -TOffer::setFieldAmounts() -{ - m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in)); - m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out, issOut_)); -} - template -Issue const& -TOffer::issueIn() const +Asset const& +TOffer::assetIn() const { - return this->issIn_; + return this->assetIn_; } template <> -inline Issue const& -TOffer::issueIn() const +inline Asset const& +TOffer::assetIn() const { - return m_amounts.in.issue(); + return m_amounts.in.asset(); } template -Issue const& -TOffer::issueOut() const +Asset const& +TOffer::assetOut() const { - return this->issOut_; + return this->assetOut_; } template <> -inline Issue const& -TOffer::issueOut() const +inline Asset const& +TOffer::assetOut() const { - return m_amounts.out.issue(); + return m_amounts.out.asset(); } template diff --git a/src/xrpld/app/tx/detail/OfferStream.cpp b/src/xrpld/app/tx/detail/OfferStream.cpp index 2fb9ad6a143..73502a42dca 100644 --- a/src/xrpld/app/tx/detail/OfferStream.cpp +++ b/src/xrpld/app/tx/detail/OfferStream.cpp @@ -27,8 +27,9 @@ namespace { bool checkIssuers(ReadView const& view, Book const& book) { - auto issuerExists = [](ReadView const& view, Issue const& iss) -> bool { - return isXRP(iss.account) || view.read(keylet::account(iss.account)); + auto issuerExists = [](ReadView const& view, Asset const& iss) -> bool { + return isXRP(iss.getIssuer()) || + view.read(keylet::account(iss.getIssuer())); }; return issuerExists(view, book.in) && issuerExists(view, book.out); } @@ -97,28 +98,32 @@ accountFundsHelper( ReadView const& view, AccountID const& id, STAmount const& saDefault, - Issue const&, + Asset const&, FreezeHandling freezeHandling, + AuthHandling authHandling, beast::Journal j) { - return accountFunds(view, id, saDefault, freezeHandling, j); + return accountFunds(view, id, saDefault, freezeHandling, authHandling, j); } -static IOUAmount +template + requires(std::is_same_v || std::is_same_v) +static T accountFundsHelper( ReadView const& view, AccountID const& id, - IOUAmount const& amtDefault, - Issue const& issue, + T const& amtDefault, + Asset const& asset, FreezeHandling freezeHandling, + AuthHandling authHandling, beast::Journal j) { - if (issue.account == id) + if (asset.getIssuer() == id) // self funded return amtDefault; - return toAmount(accountHolds( - view, id, issue.currency, issue.account, freezeHandling, j)); + return toAmount( + accountHolds(view, id, asset, freezeHandling, authHandling, j)); } static XRPAmount @@ -126,34 +131,21 @@ accountFundsHelper( ReadView const& view, AccountID const& id, XRPAmount const& amtDefault, - Issue const& issue, + Asset const& asset, FreezeHandling freezeHandling, + AuthHandling authHandling, beast::Journal j) { - return toAmount(accountHolds( - view, id, issue.currency, issue.account, freezeHandling, j)); + return toAmount( + accountHolds(view, id, asset, freezeHandling, authHandling, j)); } template template + requires ValidTaker bool TOfferStreamBase::shouldRmSmallIncreasedQOffer() const { - static_assert( - std::is_same_v || - std::is_same_v, - "STAmount is not supported"); - - static_assert( - std::is_same_v || - std::is_same_v, - "STAmount is not supported"); - - static_assert( - !std::is_same_v || - !std::is_same_v, - "Cannot have XRP/XRP offers"); - if (!view_.rules().enabled(fixRmSmallIncreasedQOffers)) return false; @@ -178,7 +170,7 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const if constexpr (!inIsXRP && !outIsXRP) { - if (ofrAmts.in >= ofrAmts.out) + if (Number(ofrAmts.in) >= Number(ofrAmts.out)) return false; } @@ -186,7 +178,7 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const bool const fixReduced = view_.rules().enabled(fixReducedOffersV1); auto const effectiveAmounts = [&] { - if (offer_.owner() != offer_.issueOut().account && + if (offer_.owner() != offer_.assetOut().getIssuer() && ownerFunds < ofrAmts.out) { // adjust the amounts by owner funds. @@ -278,8 +270,9 @@ TOfferStreamBase::step() view_, offer_.owner(), amount.out, - offer_.issueOut(), + offer_.assetOut(), fhZERO_IF_FROZEN, + ahZERO_IF_UNAUTHORIZED, j_); // Check for unfunded offer @@ -292,8 +285,9 @@ TOfferStreamBase::step() cancelView_, offer_.owner(), amount.out, - offer_.issueOut(), + offer_.assetOut(), fhZERO_IF_FROZEN, + ahZERO_IF_UNAUTHORIZED, j_); if (original_funds == *ownerFunds_) @@ -311,39 +305,48 @@ TOfferStreamBase::step() continue; } - bool const rmSmallIncreasedQOffer = [&] { - bool const inIsXRP = isXRP(offer_.issueIn()); - bool const outIsXRP = isXRP(offer_.issueOut()); - if (inIsXRP && !outIsXRP) + using Var = + std::variant; + auto toTypedAmt = [&](T const& amt) -> Var { + static auto xrp = XRPAmount{}; + static auto mpt = MPTAmount{}; + static auto iou = IOUAmount{}; + if constexpr (std::is_same_v) { - // Without the `if constexpr`, the - // `shouldRmSmallIncreasedQOffer` template will be instantiated - // even if it is never used. This can cause compiler errors in - // some cases, hence the `if constexpr` guard. - // Note that TIn can be XRPAmount or STAmount, and TOut can be - // IOUAmount or STAmount. - if constexpr (!(std::is_same_v || - std::is_same_v)) - return shouldRmSmallIncreasedQOffer(); + if (isXRP(amt)) + return &xrp; + if (amt.template holds()) + return &mpt; + return &iou; } - if (!inIsXRP && outIsXRP) - { - // See comment above for `if constexpr` rationale - if constexpr (!(std::is_same_v || - std::is_same_v)) - return shouldRmSmallIncreasedQOffer(); - } - if (!inIsXRP && !outIsXRP) + if constexpr (!std::is_same_v) + return amt; + }; + + bool const rmSmallIncreasedQOffer = [&] { + bool ret = false; + if constexpr ( + !std::is_same_v && + !std::is_same_v) + return shouldRmSmallIncreasedQOffer(); + else if constexpr ( + std::is_same_v && std::is_same_v) { - // See comment above for `if constexpr` rationale - if constexpr (!(std::is_same_v || - std::is_same_v)) - return shouldRmSmallIncreasedQOffer(); + std::visit( + [&]( + TInAmt const*&&, TOutAmt const*&&) { + if constexpr ( + !std::is_same_v || + !std::is_same_v) + ret = + shouldRmSmallIncreasedQOffer(); + }, + toTypedAmt(offer_.amount().in), + toTypedAmt(offer_.amount().out)); + return ret; } - UNREACHABLE( - "rippls::TOfferStreamBase::step::rmSmallIncreasedQOffer : XRP " - "vs XRP offer"); - return false; + assert(0); + return ret; }(); if (rmSmallIncreasedQOffer) @@ -352,8 +355,9 @@ TOfferStreamBase::step() cancelView_, offer_.owner(), amount.out, - offer_.issueOut(), + offer_.assetOut(), fhZERO_IF_FROZEN, + ahZERO_IF_UNAUTHORIZED, j_); if (original_funds == *ownerFunds_) @@ -397,9 +401,19 @@ template class FlowOfferStream; template class FlowOfferStream; template class FlowOfferStream; template class FlowOfferStream; +template class FlowOfferStream; +template class FlowOfferStream; +template class FlowOfferStream; +template class FlowOfferStream; +template class FlowOfferStream; template class TOfferStreamBase; template class TOfferStreamBase; template class TOfferStreamBase; template class TOfferStreamBase; +template class TOfferStreamBase; +template class TOfferStreamBase; +template class TOfferStreamBase; +template class TOfferStreamBase; +template class TOfferStreamBase; } // namespace ripple diff --git a/src/xrpld/app/tx/detail/OfferStream.h b/src/xrpld/app/tx/detail/OfferStream.h index be224a67b4e..c1c66e5ab92 100644 --- a/src/xrpld/app/tx/detail/OfferStream.h +++ b/src/xrpld/app/tx/detail/OfferStream.h @@ -86,6 +86,7 @@ class TOfferStreamBase permRmOffer(uint256 const& offerIndex) = 0; template + requires ValidTaker bool shouldRmSmallIncreasedQOffer() const; diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index d6f7c60fabc..bdc26c332dd 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -77,14 +77,16 @@ Payment::preflight(PreflightContext const& ctx) auto& j = ctx.j; STAmount const dstAmount(tx.getFieldAmount(sfAmount)); - bool const mptDirect = dstAmount.holds(); + bool const isMPT = dstAmount.holds(); + bool const MPTokensV2 = ctx.rules.enabled(featureMPTokensV2); - if (mptDirect && !ctx.rules.enabled(featureMPTokensV1)) + if (!ctx.rules.enabled(featureMPTokensV1) && isMPT) return temDISABLED; std::uint32_t const txFlags = tx.getFlags(); - std::uint32_t paymentMask = mptDirect ? tfMPTPaymentMask : tfPaymentMask; + std::uint32_t paymentMask = + (isMPT && !MPTokensV2) ? tfMPTPaymentMask : tfPaymentMask; if (txFlags & paymentMask) { @@ -92,8 +94,9 @@ Payment::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - if (mptDirect && ctx.tx.isFieldPresent(sfPaths)) - return temMALFORMED; + // In V1 the error was temMALFORMED + if (!MPTokensV2 && isMPT && ctx.tx.isFieldPresent(sfPaths)) + return temDISABLED; bool const partialPaymentAllowed = txFlags & tfPartialPayment; bool const limitQuality = txFlags & tfLimitQuality; @@ -107,8 +110,9 @@ Payment::preflight(PreflightContext const& ctx) STAmount const maxSourceAmount = getMaxSourceAmount(account, dstAmount, tx[~sfSendMax]); - if ((mptDirect && dstAmount.asset() != maxSourceAmount.asset()) || - (!mptDirect && maxSourceAmount.holds())) + if (!MPTokensV2 && + ((isMPT && dstAmount.asset() != maxSourceAmount.asset()) || + (!isMPT && maxSourceAmount.holds()))) { JLOG(j.trace()) << "Malformed transaction: inconsistent issues: " << dstAmount.getFullText() << " " @@ -166,7 +170,7 @@ Payment::preflight(PreflightContext const& ctx) << "SendMax specified for XRP to XRP."; return temBAD_SEND_XRP_MAX; } - if ((xrpDirect || mptDirect) && hasPaths) + if ((xrpDirect || (!MPTokensV2 && isMPT)) && hasPaths) { // XRP is sent without paths. JLOG(j.trace()) << "Malformed transaction: " @@ -180,7 +184,7 @@ Payment::preflight(PreflightContext const& ctx) << "Partial payment specified for XRP to XRP."; return temBAD_SEND_XRP_PARTIAL; } - if ((xrpDirect || mptDirect) && limitQuality) + if ((xrpDirect || (!MPTokensV2 && isMPT)) && limitQuality) { // Consistent but redundant transaction. JLOG(j.trace()) @@ -188,7 +192,7 @@ Payment::preflight(PreflightContext const& ctx) << "Limit quality specified for XRP to XRP or MPT to MPT."; return temBAD_SEND_XRP_LIMIT; } - if ((xrpDirect || mptDirect) && !defaultPathsAllowed) + if ((xrpDirect || (!MPTokensV2 && isMPT)) && !defaultPathsAllowed) { // Consistent but redundant transaction. JLOG(j.trace()) @@ -341,7 +345,7 @@ Payment::doApply() AccountID const dstAccountID(ctx_.tx.getAccountID(sfDestination)); STAmount const dstAmount(ctx_.tx.getFieldAmount(sfAmount)); - bool const mptDirect = dstAmount.holds(); + bool const isMPT = dstAmount.holds(); STAmount const maxSourceAmount = getMaxSourceAmount(account_, dstAmount, sendMax); @@ -379,9 +383,10 @@ Payment::doApply() sleDst->getFlags() & lsfDepositAuth && depositAuth; bool const depositPreauth = view().rules().enabled(featureDepositPreauth); + bool const MPTokensV2 = view().rules().enabled(featureMPTokensV2); bool const ripple = - (hasPaths || sendMax || !dstAmount.native()) && !mptDirect; + (hasPaths || sendMax || !dstAmount.native()) && (!isMPT || MPTokensV2); // If the destination has lsfDepositAuth set, then only direct XRP // payments (no intermediate steps) are allowed to the destination. @@ -452,7 +457,7 @@ Payment::doApply() terResult = tecPATH_DRY; return terResult; } - else if (mptDirect) + else if (isMPT) { JLOG(j_.trace()) << " dstAmount=" << dstAmount.getFullText(); auto const& mptIssue = dstAmount.get(); diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 74027752486..dc453397ace 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -182,6 +182,15 @@ accountHolds( AuthHandling zeroIfUnauthorized, beast::Journal j); +[[nodiscard]] STAmount +accountHolds( + ReadView const& view, + AccountID const& account, + Asset 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 @@ -195,6 +204,15 @@ accountFunds( FreezeHandling freezeHandling, beast::Journal j); +[[nodiscard]] STAmount +accountFunds( + ReadView const& view, + AccountID const& id, + STAmount const& saDefault, + FreezeHandling freezeHandling, + AuthHandling authHandling, + beast::Journal j); + // Return the account's liquid (not reserved) XRP. Generally prefer // calling accountHolds() over this interface. However, this interface // allows the caller to temporarily adjust the owner count should that be @@ -535,6 +553,17 @@ requireAuth( ReadView const& view, MPTIssue const& mptIssue, AccountID const& account); +[[nodiscard]] TER inline requireAuth( + ReadView const& view, + Asset const& asset, + AccountID const& account) +{ + return std::visit( + [&](TIss const& issue_) { + return requireAuth(view, issue_, account); + }, + asset.value()); +} /** Check if the destination account is allowed * to receive MPT. Return tecNO_AUTH if it doesn't diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 23dbd81d5ad..e224ffbe380 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -363,6 +363,26 @@ accountHolds( return amount; } +STAmount +accountHolds( + ReadView const& view, + AccountID const& account, + Asset const& issue, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j) +{ + return std::visit( + [&](TIss const& issue_) { + if constexpr (std::is_same_v) + return accountHolds(view, account, issue_, zeroIfFrozen, j); + else + return accountHolds( + view, account, issue_, zeroIfFrozen, zeroIfUnauthorized, j); + }, + issue.value()); +} + STAmount accountFunds( ReadView const& view, @@ -383,6 +403,22 @@ accountFunds( j); } +STAmount +accountFunds( + ReadView const& view, + AccountID const& id, + STAmount const& saDefault, + FreezeHandling freezeHandling, + AuthHandling authHandling, + beast::Journal j) +{ + if (!saDefault.native() && saDefault.getIssuer() == id) + return saDefault; + + return accountHolds( + view, id, saDefault.asset(), freezeHandling, authHandling, j); +} + // Prevent ownerCount from wrapping under error conditions. // // adjustment allows the ownerCount to be adjusted up or down in multiple steps. diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h b/src/xrpld/rpc/MPTokenIssuanceID.h index ef194bd398c..87e9ff73147 100644 --- a/src/xrpld/rpc/MPTokenIssuanceID.h +++ b/src/xrpld/rpc/MPTokenIssuanceID.h @@ -45,7 +45,7 @@ canHaveMPTokenIssuanceID( std::shared_ptr const& serializedTx, TxMeta const& transactionMeta); -std::optional +std::optional getIDFromCreatedIssuance(TxMeta const& transactionMeta); void diff --git a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp index 721be652622..8b4da3988c0 100644 --- a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp +++ b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp @@ -47,7 +47,7 @@ canHaveMPTokenIssuanceID( return true; } -std::optional +std::optional getIDFromCreatedIssuance(TxMeta const& transactionMeta) { for (STObject const& node : transactionMeta.getNodes()) @@ -74,7 +74,7 @@ insertMPTokenIssuanceID( if (!canHaveMPTokenIssuanceID(transaction, transactionMeta)) return; - std::optional result = getIDFromCreatedIssuance(transactionMeta); + std::optional result = getIDFromCreatedIssuance(transactionMeta); if (result) response[jss::mpt_issuance_id] = to_string(result.value()); } diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 3623b26d356..feba24c9cb2 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -214,7 +214,9 @@ checkPayment( return RPC::invalid_field_error("tx_json.Destination"); if (params.isMember(jss::build_path) && - ((doPath == false) || amount.holds())) + ((doPath == false) || + (!app.openLedger().current()->rules().enabled(featureMPTokensV2) && + amount.holds()))) return RPC::make_error( rpcINVALID_PARAMS, "Field 'build_path' not allowed in this context."); @@ -254,8 +256,8 @@ checkPayment( if (auto ledger = app.openLedger().current()) { Pathfinder pf( - std::make_shared( - ledger, app.journal("RippleLineCache")), + std::make_shared( + ledger, app.journal("AssetCache")), srcAddressID, *dstAccountID, sendMax.issue().currency, diff --git a/src/xrpld/rpc/handlers/AMMInfo.cpp b/src/xrpld/rpc/handlers/AMMInfo.cpp index e6a1d713a4a..ac8299f5e81 100644 --- a/src/xrpld/rpc/handlers/AMMInfo.cpp +++ b/src/xrpld/rpc/handlers/AMMInfo.cpp @@ -165,8 +165,8 @@ doAMMInfo(RPC::JsonContext& context) return Unexpected(rpcACT_NOT_FOUND); if (!issue1 && !issue2) { - issue1 = (*amm)[sfAsset]; - issue2 = (*amm)[sfAsset2]; + issue1 = (*amm)[sfAsset].get(); + issue2 = (*amm)[sfAsset2].get(); } return ValuesFromContextParams{ @@ -191,6 +191,7 @@ doAMMInfo(RPC::JsonContext& context) issue1, issue2, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, context.j); auto const lptAMMBalance = accountID ? ammLPHolds(*ledger, *amm, *accountID, context.j) diff --git a/src/xrpld/rpc/handlers/BookOffers.cpp b/src/xrpld/rpc/handlers/BookOffers.cpp index dccc03de5be..dcdbb97c437 100644 --- a/src/xrpld/rpc/handlers/BookOffers.cpp +++ b/src/xrpld/rpc/handlers/BookOffers.cpp @@ -62,104 +62,162 @@ doBookOffers(RPC::JsonContext& context) if (!taker_gets.isObjectOrNull()) return RPC::object_field_error(jss::taker_gets); - if (!taker_pays.isMember(jss::currency)) + if (!taker_pays.isMember(jss::currency) && + !taker_pays.isMember(jss::mpt_issuance_id)) return RPC::missing_field_error("taker_pays.currency"); - if (!taker_pays[jss::currency].isString()) + if (taker_pays.isMember(jss::mpt_issuance_id) && + (taker_pays.isMember(jss::currency) || + taker_pays.isMember(jss::issuer))) + return RPC::invalid_field_error("taker_pays"); + + if ((taker_pays.isMember(jss::currency) && + !taker_pays[jss::currency].isString()) || + (taker_pays.isMember(jss::mpt_issuance_id) && + !taker_pays[jss::mpt_issuance_id].isString())) return RPC::expected_field_error("taker_pays.currency", "string"); - if (!taker_gets.isMember(jss::currency)) + if (!taker_gets.isMember(jss::currency) && + !taker_gets.isMember(jss::mpt_issuance_id)) return RPC::missing_field_error("taker_gets.currency"); - if (!taker_gets[jss::currency].isString()) + if (taker_gets.isMember(jss::mpt_issuance_id) && + (taker_gets.isMember(jss::currency) || + taker_gets.isMember(jss::issuer))) + return RPC::invalid_field_error("taker_gets"); + + if ((taker_gets.isMember(jss::currency) && + !taker_gets[jss::currency].isString()) || + (taker_gets.isMember(jss::mpt_issuance_id) && + !taker_gets[jss::mpt_issuance_id].isString())) return RPC::expected_field_error("taker_gets.currency", "string"); - Currency pay_currency; + Book book; - if (!to_currency(pay_currency, taker_pays[jss::currency].asString())) + if (taker_pays.isMember(jss::currency)) { - JLOG(context.j.info()) << "Bad taker_pays currency."; - return RPC::make_error( - rpcSRC_CUR_MALFORMED, - "Invalid field 'taker_pays.currency', bad currency."); - } + Currency pay_currency; - Currency get_currency; + if (!to_currency(pay_currency, taker_pays[jss::currency].asString())) + { + JLOG(context.j.info()) << "Bad taker_pays currency."; + return RPC::make_error( + rpcSRC_CUR_MALFORMED, + "Invalid field 'taker_pays.currency', bad currency."); + } + book.in.get().currency = pay_currency; + } - if (!to_currency(get_currency, taker_gets[jss::currency].asString())) + if (taker_gets.isMember(jss::currency)) { - JLOG(context.j.info()) << "Bad taker_gets currency."; - return RPC::make_error( - rpcDST_AMT_MALFORMED, - "Invalid field 'taker_gets.currency', bad currency."); - } + Currency get_currency; - AccountID pay_issuer; + if (!to_currency(get_currency, taker_gets[jss::currency].asString())) + { + JLOG(context.j.info()) << "Bad taker_gets currency."; + return RPC::make_error( + rpcDST_AMT_MALFORMED, + "Invalid field 'taker_gets.currency', bad currency."); + } + book.out.get().currency = get_currency; + } - if (taker_pays.isMember(jss::issuer)) + if (taker_pays.isMember(jss::currency)) { - if (!taker_pays[jss::issuer].isString()) - return RPC::expected_field_error("taker_pays.issuer", "string"); - - if (!to_issuer(pay_issuer, taker_pays[jss::issuer].asString())) + AccountID pay_issuer; + + if (taker_pays.isMember(jss::issuer)) + { + if (!taker_pays[jss::issuer].isString()) + return RPC::expected_field_error("taker_pays.issuer", "string"); + + if (!to_issuer(pay_issuer, taker_pays[jss::issuer].asString())) + return RPC::make_error( + rpcSRC_ISR_MALFORMED, + "Invalid field 'taker_pays.issuer', bad issuer."); + + if (pay_issuer == noAccount()) + return RPC::make_error( + rpcSRC_ISR_MALFORMED, + "Invalid field 'taker_pays.issuer', bad issuer account " + "one."); + } + else + { + pay_issuer = xrpAccount(); + } + + book.in.get().account = pay_issuer; + + if (isXRP(book.in.get().currency) && !isXRP(pay_issuer)) return RPC::make_error( rpcSRC_ISR_MALFORMED, - "Invalid field 'taker_pays.issuer', bad issuer."); + "Unneeded field 'taker_pays.issuer' for " + "XRP currency specification."); - if (pay_issuer == noAccount()) + if (!isXRP(book.in.get().currency) && isXRP(pay_issuer)) return RPC::make_error( rpcSRC_ISR_MALFORMED, - "Invalid field 'taker_pays.issuer', bad issuer account one."); + "Invalid field 'taker_pays.issuer', expected non-XRP issuer."); } else { - pay_issuer = xrpAccount(); + MPTID mptid; + if (!mptid.parseHex(taker_pays[jss::mpt_issuance_id].asString())) + return RPC::make_error( + rpcSRC_CUR_MALFORMED, + "Invalid field 'taker_pays.mpt_issuance_id'"); + book.in = mptid; } - if (isXRP(pay_currency) && !isXRP(pay_issuer)) - return RPC::make_error( - rpcSRC_ISR_MALFORMED, - "Unneeded field 'taker_pays.issuer' for " - "XRP currency specification."); - - if (!isXRP(pay_currency) && isXRP(pay_issuer)) - return RPC::make_error( - rpcSRC_ISR_MALFORMED, - "Invalid field 'taker_pays.issuer', expected non-XRP issuer."); - - AccountID get_issuer; - - if (taker_gets.isMember(jss::issuer)) + if (taker_gets.isMember(jss::currency)) { - if (!taker_gets[jss::issuer].isString()) - return RPC::expected_field_error("taker_gets.issuer", "string"); - - if (!to_issuer(get_issuer, taker_gets[jss::issuer].asString())) + AccountID get_issuer; + + if (taker_gets.isMember(jss::issuer)) + { + if (!taker_gets[jss::issuer].isString()) + return RPC::expected_field_error("taker_gets.issuer", "string"); + + if (!to_issuer(get_issuer, taker_gets[jss::issuer].asString())) + return RPC::make_error( + rpcDST_ISR_MALFORMED, + "Invalid field 'taker_gets.issuer', bad issuer."); + + if (get_issuer == noAccount()) + return RPC::make_error( + rpcDST_ISR_MALFORMED, + "Invalid field 'taker_gets.issuer', bad issuer account " + "one."); + } + else + { + get_issuer = xrpAccount(); + } + + book.out.get().account = get_issuer; + + if (isXRP(book.out.get().currency) && !isXRP(get_issuer)) return RPC::make_error( rpcDST_ISR_MALFORMED, - "Invalid field 'taker_gets.issuer', bad issuer."); + "Unneeded field 'taker_gets.issuer' for " + "XRP currency specification."); - if (get_issuer == noAccount()) + if (!isXRP(book.out.get().currency) && isXRP(get_issuer)) return RPC::make_error( rpcDST_ISR_MALFORMED, - "Invalid field 'taker_gets.issuer', bad issuer account one."); + "Invalid field 'taker_gets.issuer', expected non-XRP issuer."); } else { - get_issuer = xrpAccount(); + MPTID mptid; + if (!mptid.parseHex(taker_gets[jss::mpt_issuance_id].asString())) + return RPC::make_error( + rpcSRC_CUR_MALFORMED, + "Invalid field 'taker_gets.mpt_issuance_id'"); + book.in = mptid; } - if (isXRP(get_currency) && !isXRP(get_issuer)) - return RPC::make_error( - rpcDST_ISR_MALFORMED, - "Unneeded field 'taker_gets.issuer' for " - "XRP currency specification."); - - if (!isXRP(get_currency) && isXRP(get_issuer)) - return RPC::make_error( - rpcDST_ISR_MALFORMED, - "Invalid field 'taker_gets.issuer', expected non-XRP issuer."); - std::optional takerID; if (context.params.isMember(jss::taker)) { @@ -171,7 +229,7 @@ doBookOffers(RPC::JsonContext& context) return RPC::invalid_field_error(jss::taker); } - if (pay_currency == get_currency && pay_issuer == get_issuer) + if (book.in == book.out) { JLOG(context.j.info()) << "taker_gets same as taker_pays."; return RPC::make_error(rpcBAD_MARKET); @@ -189,7 +247,7 @@ doBookOffers(RPC::JsonContext& context) context.netOps.getBookPage( lpLedger, - {{pay_currency, pay_issuer}, {get_currency, get_issuer}}, + book, takerID ? *takerID : beast::zero, bProof, limit, diff --git a/src/xrpld/rpc/handlers/Subscribe.cpp b/src/xrpld/rpc/handlers/Subscribe.cpp index 66fe89dea04..92d72560a48 100644 --- a/src/xrpld/rpc/handlers/Subscribe.cpp +++ b/src/xrpld/rpc/handlers/Subscribe.cpp @@ -244,52 +244,89 @@ doSubscribe(RPC::JsonContext& context) Json::Value taker_pays = j[jss::taker_pays]; Json::Value taker_gets = j[jss::taker_gets]; - // Parse mandatory currency. - if (!taker_pays.isMember(jss::currency) || - !to_currency( - book.in.currency, taker_pays[jss::currency].asString())) + if (taker_pays.isMember(jss::currency)) { - JLOG(context.j.info()) << "Bad taker_pays currency."; - return rpcError(rpcSRC_CUR_MALFORMED); - } + Issue issue = xrpIssue(); + // Parse mandatory currency. + if (!taker_pays.isMember(jss::currency) || + !to_currency( + issue.currency, taker_pays[jss::currency].asString())) + { + JLOG(context.j.info()) << "Bad taker_pays currency."; + return rpcError(rpcSRC_CUR_MALFORMED); + } - // Parse optional issuer. - if (((taker_pays.isMember(jss::issuer)) && - (!taker_pays[jss::issuer].isString() || - !to_issuer( - book.in.account, taker_pays[jss::issuer].asString()))) - // Don't allow illegal issuers. - || (!book.in.currency != !book.in.account) || - noAccount() == book.in.account) + // Parse optional issuer. + if (((taker_pays.isMember(jss::issuer)) && + (!taker_pays[jss::issuer].isString() || + !to_issuer( + issue.account, taker_pays[jss::issuer].asString()))) + // Don't allow illegal issuers. + || (!issue.currency != !issue.account) || + noAccount() == issue.account) + { + JLOG(context.j.info()) << "Bad taker_pays issuer."; + return rpcError(rpcSRC_ISR_MALFORMED); + } + book.in = issue; + } + else if (taker_pays.isMember(jss::mpt_issuance_id)) { - JLOG(context.j.info()) << "Bad taker_pays issuer."; - return rpcError(rpcSRC_ISR_MALFORMED); + if (taker_pays.isMember(jss::currency) || + taker_pays.isMember(jss::issuer)) + return rpcError(rpcINVALID_PARAMS); + + MPTID mptid; + if (!mptid.parseHex( + taker_pays[jss::mpt_issuance_id].asString())) + return rpcError(rpcSRC_CUR_MALFORMED); + book.in = mptid; } + else + return rpcError(rpcSRC_CUR_MALFORMED); - // Parse mandatory currency. - if (!taker_gets.isMember(jss::currency) || - !to_currency( - book.out.currency, taker_gets[jss::currency].asString())) + if (taker_gets.isMember(jss::currency)) { - JLOG(context.j.info()) << "Bad taker_gets currency."; - return rpcError(rpcDST_AMT_MALFORMED); - } + Issue issue; + // Parse mandatory currency. + if (!taker_gets.isMember(jss::currency) || + !to_currency( + issue.currency, taker_gets[jss::currency].asString())) + { + JLOG(context.j.info()) << "Bad taker_gets currency."; + return rpcError(rpcDST_AMT_MALFORMED); + } - // Parse optional issuer. - if (((taker_gets.isMember(jss::issuer)) && - (!taker_gets[jss::issuer].isString() || - !to_issuer( - book.out.account, taker_gets[jss::issuer].asString()))) - // Don't allow illegal issuers. - || (!book.out.currency != !book.out.account) || - noAccount() == book.out.account) + // Parse optional issuer. + if (((taker_gets.isMember(jss::issuer)) && + (!taker_gets[jss::issuer].isString() || + !to_issuer( + issue.account, taker_gets[jss::issuer].asString()))) + // Don't allow illegal issuers. + || (!issue.currency != !issue.account) || + noAccount() == issue.account) + { + JLOG(context.j.info()) << "Bad taker_gets issuer."; + return rpcError(rpcDST_ISR_MALFORMED); + } + book.out = issue; + } + else if (taker_gets.isMember(jss::mpt_issuance_id)) { - JLOG(context.j.info()) << "Bad taker_gets issuer."; - return rpcError(rpcDST_ISR_MALFORMED); + if (taker_gets.isMember(jss::currency) || + taker_gets.isMember(jss::issuer)) + return rpcError(rpcINVALID_PARAMS); + + MPTID mptid; + if (!mptid.parseHex( + taker_gets[jss::mpt_issuance_id].asString())) + return rpcError(rpcDST_AMT_MALFORMED); + book.in = mptid; } + else + return rpcError(rpcDST_AMT_MALFORMED); - if (book.in.currency == book.out.currency && - book.in.account == book.out.account) + if (book.in == book.out) { JLOG(context.j.info()) << "taker_gets same as taker_pays."; return rpcError(rpcBAD_MARKET); diff --git a/src/xrpld/rpc/handlers/Unsubscribe.cpp b/src/xrpld/rpc/handlers/Unsubscribe.cpp index bab0d99744c..49d9e99ee2b 100644 --- a/src/xrpld/rpc/handlers/Unsubscribe.cpp +++ b/src/xrpld/rpc/handlers/Unsubscribe.cpp @@ -178,50 +178,88 @@ doUnsubscribe(RPC::JsonContext& context) Book book; - // Parse mandatory currency. - if (!taker_pays.isMember(jss::currency) || - !to_currency( - book.in.currency, taker_pays[jss::currency].asString())) + if (taker_pays.isMember(jss::currency)) { - JLOG(context.j.info()) << "Bad taker_pays currency."; - return rpcError(rpcSRC_CUR_MALFORMED); + Issue issue; + // Parse mandatory currency. + if (!taker_pays.isMember(jss::currency) || + !to_currency( + issue.currency, taker_pays[jss::currency].asString())) + { + JLOG(context.j.info()) << "Bad taker_pays currency."; + return rpcError(rpcSRC_CUR_MALFORMED); + } + // Parse optional issuer. + else if ( + ((taker_pays.isMember(jss::issuer)) && + (!taker_pays[jss::issuer].isString() || + !to_issuer( + issue.account, taker_pays[jss::issuer].asString()))) + // Don't allow illegal issuers. + || !isConsistent(book.in) || noAccount() == issue.account) + { + JLOG(context.j.info()) << "Bad taker_pays issuer."; + + return rpcError(rpcSRC_ISR_MALFORMED); + } + book.in = issue; } - // Parse optional issuer. - else if ( - ((taker_pays.isMember(jss::issuer)) && - (!taker_pays[jss::issuer].isString() || - !to_issuer( - book.in.account, taker_pays[jss::issuer].asString()))) - // Don't allow illegal issuers. - || !isConsistent(book.in) || noAccount() == book.in.account) + else if (taker_pays.isMember(jss::mpt_issuance_id)) { - JLOG(context.j.info()) << "Bad taker_pays issuer."; - - return rpcError(rpcSRC_ISR_MALFORMED); + if (taker_pays.isMember(jss::currency) || + taker_pays.isMember(jss::issuer)) + return rpcError(rpcINVALID_PARAMS); + + MPTID mptid; + if (!mptid.parseHex( + taker_pays[jss::mpt_issuance_id].asString())) + return rpcError(rpcSRC_CUR_MALFORMED); + book.in = mptid; } + else + return rpcError(rpcSRC_CUR_MALFORMED); - // Parse mandatory currency. - if (!taker_gets.isMember(jss::currency) || - !to_currency( - book.out.currency, taker_gets[jss::currency].asString())) + if (taker_gets.isMember(jss::currency)) { - JLOG(context.j.info()) << "Bad taker_gets currency."; - - return rpcError(rpcDST_AMT_MALFORMED); + Issue issue; + // Parse mandatory currency. + if (!taker_gets.isMember(jss::currency) || + !to_currency( + issue.currency, taker_gets[jss::currency].asString())) + { + JLOG(context.j.info()) << "Bad taker_gets currency."; + + return rpcError(rpcDST_AMT_MALFORMED); + } + // Parse optional issuer. + else if ( + ((taker_gets.isMember(jss::issuer)) && + (!taker_gets[jss::issuer].isString() || + !to_issuer( + issue.account, taker_gets[jss::issuer].asString()))) + // Don't allow illegal issuers. + || !isConsistent(book.out) || noAccount() == issue.account) + { + JLOG(context.j.info()) << "Bad taker_gets issuer."; + + return rpcError(rpcDST_ISR_MALFORMED); + } + book.out = issue; } - // Parse optional issuer. - else if ( - ((taker_gets.isMember(jss::issuer)) && - (!taker_gets[jss::issuer].isString() || - !to_issuer( - book.out.account, taker_gets[jss::issuer].asString()))) - // Don't allow illegal issuers. - || !isConsistent(book.out) || noAccount() == book.out.account) + else if (taker_gets.isMember(jss::mpt_issuance_id)) { - JLOG(context.j.info()) << "Bad taker_gets issuer."; - - return rpcError(rpcDST_ISR_MALFORMED); + if (taker_gets.isMember(jss::currency) || + taker_gets.isMember(jss::issuer)) + return rpcError(rpcINVALID_PARAMS); + + MPTID mptid; + if (!mptid.parseHex( + taker_gets[jss::mpt_issuance_id].asString())) + return rpcError(rpcDST_AMT_MALFORMED); + book.in = mptid; } + else + return rpcError(rpcDST_AMT_MALFORMED); if (book.in == book.out) {