From 7c611fe25b4014ec59135636dbf0f20a2f6409c2 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 19 Dec 2024 07:01:44 -0500 Subject: [PATCH] MPT integration into DEX --- include/xrpl/protocol/AMMCore.h | 20 +- include/xrpl/protocol/AmountConversions.h | 87 +- include/xrpl/protocol/Asset.h | 75 +- include/xrpl/protocol/Book.h | 93 +- include/xrpl/protocol/Concepts.h | 72 + include/xrpl/protocol/Feature.h | 2 +- include/xrpl/protocol/MPTAmount.h | 24 + include/xrpl/protocol/MPTIssue.h | 40 +- include/xrpl/protocol/PathAsset.h | 143 ++ include/xrpl/protocol/SOTemplate.h | 1 - include/xrpl/protocol/STObject.h | 2 + include/xrpl/protocol/STPathSet.h | 86 +- 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 | 48 +- src/libxrpl/protocol/AMMCore.cpp | 44 +- src/libxrpl/protocol/Asset.cpp | 24 + src/libxrpl/protocol/Indexes.cpp | 81 +- src/libxrpl/protocol/MPTIssue.cpp | 13 + src/libxrpl/protocol/PathAsset.cpp | 39 + src/libxrpl/protocol/STAmount.cpp | 2 +- src/libxrpl/protocol/STObject.cpp | 6 + src/libxrpl/protocol/STParsedJSON.cpp | 86 +- src/libxrpl/protocol/STPathSet.cpp | 41 +- src/libxrpl/protocol/TER.cpp | 1 + src/test/app/AMMCalc_test.cpp | 13 +- src/test/app/AMM_test.cpp | 2 +- src/test/app/Check_test.cpp | 3 +- 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 | 1643 +++++++++++++++-- src/test/app/Offer_test.cpp | 93 +- src/test/app/PayStrand_test.cpp | 3 - src/test/app/SetAuth_test.cpp | 1 - src/test/app/Taker_test.cpp | 11 +- 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/AMMTest.cpp | 2 +- src/test/jtx/impl/TestHelpers.cpp | 160 +- src/test/jtx/impl/amount.cpp | 15 +- src/test/jtx/impl/balance.cpp | 22 +- src/test/jtx/impl/paths.cpp | 13 +- src/test/rpc/AMMInfo_test.cpp | 29 +- 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 | 35 +- 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 | 109 +- 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} | 38 +- .../paths/{RippleLineCache.h => AssetCache.h} | 11 +- src/xrpld/app/paths/Flow.cpp | 137 +- src/xrpld/app/paths/PathRequest.cpp | 219 ++- 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 | 392 ++-- 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 | 211 ++- src/xrpld/app/paths/detail/BookStep.cpp | 229 ++- 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 | 342 ++-- src/xrpld/app/paths/detail/Steps.h | 64 +- src/xrpld/app/paths/detail/StrandFlow.h | 6 +- .../app/paths/detail/XRPEndpointStep.cpp | 10 +- src/xrpld/app/tx/detail/AMMBid.cpp | 8 +- src/xrpld/app/tx/detail/AMMClawback.cpp | 28 +- src/xrpld/app/tx/detail/AMMCreate.cpp | 150 +- src/xrpld/app/tx/detail/AMMDelete.cpp | 9 +- src/xrpld/app/tx/detail/AMMDeposit.cpp | 74 +- src/xrpld/app/tx/detail/AMMVote.cpp | 8 +- src/xrpld/app/tx/detail/AMMWithdraw.cpp | 132 +- 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 | 629 ++----- 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/app/tx/detail/Taker.cpp | 9 +- src/xrpld/ledger/View.h | 47 +- src/xrpld/ledger/detail/View.cpp | 43 +- src/xrpld/rpc/MPTokenIssuanceID.h | 2 +- src/xrpld/rpc/detail/MPTokenIssuanceID.cpp | 4 +- src/xrpld/rpc/detail/TransactionSign.cpp | 14 +- src/xrpld/rpc/handlers/AMMInfo.cpp | 49 +- src/xrpld/rpc/handlers/AccountLines.cpp | 2 +- src/xrpld/rpc/handlers/BookOffers.cpp | 184 +- src/xrpld/rpc/handlers/Subscribe.cpp | 109 +- src/xrpld/rpc/handlers/Unsubscribe.cpp | 108 +- 116 files changed, 6539 insertions(+), 2479 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} (84%) 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/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 a65f7fcad8b..6ba5cb95e40 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -21,6 +21,7 @@ #define RIPPLE_PROTOCOL_AMOUNTCONVERSION_H_INCLUDED #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) { + XRPL_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) { - XRPL_ASSERT( - isXRP(iss.account) && isXRP(iss.currency), - "ripple::toSTAmount : is XRP"); + XRPL_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) +{ + XRPL_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) +{ + XRPL_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 0d12cd40580..c4a528af35b 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -21,21 +21,20 @@ #define RIPPLE_PROTOCOL_ASSET_H_INCLUDED #include +#include #include #include namespace ripple { -class Asset; - -template -concept ValidIssueType = - std::is_same_v || std::is_same_v; - -template -concept AssetType = - std::is_convertible_v || std::is_convertible_v || - std::is_convertible_v || std::is_convertible_v; +template + requires( + std::is_same_v || std::is_same_v || + std::is_same_v) +struct AmountType +{ + using amount_type = T; +}; /* 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 @@ -68,6 +67,12 @@ class Asset { } + explicit + operator Issue() const; + + explicit + operator MPTIssue() const; + AccountID const& getIssuer() const; @@ -98,6 +103,12 @@ class Asset return holds() && get().native(); } + std::variant< + AmountType, + AmountType, + AmountType> + getAmountType() const; + friend constexpr bool operator==(Asset const& lhs, Asset const& rhs); @@ -222,6 +233,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 18a9b9498aa..8821d531ff0 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/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index 244d6839156..761e52da99b 100644 --- a/include/xrpl/protocol/MPTAmount.h +++ b/include/xrpl/protocol/MPTAmount.h @@ -50,6 +50,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; @@ -100,6 +101,9 @@ class MPTAmount : private boost::totally_ordered, constexpr value_type value() const; + friend std::istream& + operator>>(std::istream& s, MPTAmount& val); + static MPTAmount minPositiveAmount(); }; @@ -108,6 +112,11 @@ constexpr MPTAmount::MPTAmount(value_type value) : value_(value) { } +constexpr MPTAmount::MPTAmount(beast::Zero) +{ + *this = beast::zero; +} + constexpr MPTAmount& MPTAmount::operator=(beast::Zero) { @@ -138,6 +147,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/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index 028051ab1ae..c9991b708bb 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; @@ -84,6 +86,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 +118,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..79b1a687136 --- /dev/null +++ b/include/xrpl/protocol/PathAsset.h @@ -0,0 +1,143 @@ +//------------------------------------------------------------------------------ +/* + 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 { + +/* Represent STPathElement's asset, which can be Currency or MPTID. + */ +class PathAsset +{ +private: + std::variant easset_; + +public: + PathAsset() = default; + // Enables comparing Asset and PathAsset + 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; + + 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 std::visit( + [&](A const& a) { return ripple::isXRP(a); }, + easset_); +} + +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); + +} // namespace ripple + +#endif // RIPPLE_APP_PATHASSET_H_INCLUDED diff --git a/include/xrpl/protocol/SOTemplate.h b/include/xrpl/protocol/SOTemplate.h index b86fb63b858..547ca302a59 100644 --- a/include/xrpl/protocol/SOTemplate.h +++ b/include/xrpl/protocol/SOTemplate.h @@ -72,7 +72,6 @@ class SOElement { init(fieldName); } - template requires(std::is_same_v || std::is_same_v) SOElement( diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index 4c8db2e01e4..4bcbe7dde33 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 953a209c150..d5d474540a1 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,7 +37,7 @@ class STPathElement final : public CountedObject { unsigned int mType; AccountID mAccountID; - Currency mCurrencyID; + PathAsset mAssetID; AccountID mIssuerID; bool is_offer_; @@ -48,8 +50,10 @@ class STPathElement final : public CountedObject 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 +64,19 @@ class STPathElement final : public CountedObject STPathElement( std::optional const& account, - std::optional const& currency, + std::optional const& asset, std::optional const& issuer); STPathElement( AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer, - bool forceCurrency = false); + bool forceAsset = false); STPathElement( unsigned int uType, AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer); auto @@ -90,6 +94,12 @@ class STPathElement final : public CountedObject bool hasCurrency() const; + bool + hasMPT() const; + + bool + hasAsset() const; + bool isNone() const; @@ -98,9 +108,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 +156,7 @@ class STPath final : public CountedObject bool hasSeen( AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer) const; Json::Value getJson(JsonOptions) const; @@ -244,7 +260,7 @@ 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) : mType(typeNone) { @@ -262,10 +278,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,20 +298,20 @@ inline STPathElement::STPathElement( inline STPathElement::STPathElement( AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer, - bool forceCurrency) + bool forceAsset) : mType(typeNone) , mAccountID(account) - , mCurrencyID(currency) + , mAssetID(asset) , mIssuerID(issuer) , is_offer_(isXRP(mAccountID)) { if (!is_offer_) mType |= typeAccount; - if (forceCurrency || !isXRP(currency)) - mType |= typeCurrency; + if (forceAsset || !isXRP(mAssetID)) + mType |= asset.holds() ? typeMPT : typeCurrency; if (!isXRP(issuer)) mType |= typeIssuer; @@ -306,14 +322,20 @@ inline STPathElement::STPathElement( inline STPathElement::STPathElement( unsigned int uType, AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer) : mType(uType) , mAccountID(account) - , mCurrencyID(currency) + , mAssetID(asset) , mIssuerID(issuer) , is_offer_(isXRP(mAccountID)) { + // uType could be assetType; i.e. either Currency or MPTID. + // Get the actual type. + 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 +369,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 +395,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 +424,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/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..9bf9ac683f5 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. */ @@ -236,24 +236,24 @@ TRANSACTION(ttCLAWBACK, 30, Clawback, ({ /** This transaction claws back tokens from an AMM pool. */ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, ({ {sfHolder, soeREQUIRED}, - {sfAsset, soeREQUIRED}, - {sfAsset2, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAsset2, soeREQUIRED, soeMPTSupported}, + {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}, })) /** This transaction type deposits into an AMM instance */ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, ({ - {sfAsset, soeREQUIRED}, - {sfAsset2, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, - {sfAmount2, soeOPTIONAL}, + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAsset2, soeREQUIRED, soeMPTSupported}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, + {sfAmount2, soeOPTIONAL, soeMPTSupported}, {sfEPrice, soeOPTIONAL}, {sfLPTokenOut, soeOPTIONAL}, {sfTradingFee, soeOPTIONAL}, @@ -261,25 +261,25 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, ({ /** This transaction type withdraws from an AMM instance */ TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, ({ - {sfAsset, soeREQUIRED}, - {sfAsset2, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, - {sfAmount2, soeOPTIONAL}, + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAsset2, soeREQUIRED, soeMPTSupported}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, + {sfAmount2, soeOPTIONAL, soeMPTSupported}, {sfEPrice, soeOPTIONAL}, {sfLPTokenIn, soeOPTIONAL}, })) /** This transaction type votes for the trading fee */ TRANSACTION(ttAMM_VOTE, 38, AMMVote, ({ - {sfAsset, soeREQUIRED}, - {sfAsset2, soeREQUIRED}, + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAsset2, soeREQUIRED, soeMPTSupported}, {sfTradingFee, soeREQUIRED}, })) /** This transaction type bids for the auction slot */ TRANSACTION(ttAMM_BID, 39, AMMBid, ({ - {sfAsset, soeREQUIRED}, - {sfAsset2, soeREQUIRED}, + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAsset2, soeREQUIRED, soeMPTSupported}, {sfBidMin, soeOPTIONAL}, {sfBidMax, soeOPTIONAL}, {sfAuthAccounts, soeOPTIONAL}, @@ -287,8 +287,8 @@ TRANSACTION(ttAMM_BID, 39, AMMBid, ({ /** This transaction type deletes AMM in the empty state */ TRANSACTION(ttAMM_DELETE, 40, AMMDelete, ({ - {sfAsset, soeREQUIRED}, - {sfAsset2, soeREQUIRED}, + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAsset2, soeREQUIRED, soeMPTSupported}, })) /** This transactions creates a crosschain sequence number */ diff --git a/src/libxrpl/protocol/AMMCore.cpp b/src/libxrpl/protocol/AMMCore.cpp index 3bebfc4659a..5fdae19e315 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..b152c68e38b 100644 --- a/src/libxrpl/protocol/Asset.cpp +++ b/src/libxrpl/protocol/Asset.cpp @@ -43,6 +43,20 @@ Asset::setJson(Json::Value& jv) const std::visit([&](auto&& issue) { issue.setJson(jv); }, issue_); } +std:: + variant, AmountType, AmountType> + Asset::getAmountType() const +{ + static AmountType xrp; + static AmountType iou; + static AmountType mpt; + if (holds()) + return mpt; + if (native()) + return xrp; + return iou; +} + std::string to_string(Asset const& asset) { @@ -77,4 +91,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 c7f4441c7bc..bf3dafbf023 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -99,12 +99,37 @@ getBookBase(Book const& book) XRPL_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); @@ -420,14 +445,42 @@ nft_sells(uint256 const& id) noexcept Keylet amm(Asset const& issue1, Asset const& issue2) noexcept { - auto const& [minI, maxI] = - std::minmax(issue1.get(), issue2.get()); - return amm(indexHash( - LedgerNameSpace::AMM, - minI.account, - minI.currency, - maxI.account, - maxI.currency)); + 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..d1a7a09e533 100644 --- a/src/libxrpl/protocol/MPTIssue.cpp +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include @@ -27,6 +28,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 +110,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..77aa6cbfd0a --- /dev/null +++ b/src/libxrpl/protocol/PathAsset.cpp @@ -0,0 +1,39 @@ +//------------------------------------------------------------------------------ +/* + 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 { + +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; +} + +} // namespace ripple diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 37830830ade..fc5078a0ebe 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -974,7 +974,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/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 821f8f05c96..034afd4539d 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..cf800dc908b 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"]; - bool hasCurrency = false; + 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 hasAsset = 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; + hasAsset = 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, hasAsset); } tail.push_back(p); diff --git a/src/libxrpl/protocol/STPathSet.cpp b/src/libxrpl/protocol/STPathSet.cpp index 57bcaca6b2c..e3abfcbb593 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(); + XRPL_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,12 @@ STPathSet::isDefault() const bool STPath::hasSeen( AccountID const& account, - Currency const& currency, + 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 +195,16 @@ STPath::getJson(JsonOptions) const if (iType & STPathElement::typeAccount) elem[jss::account] = to_string(it.getAccountID()); + XRPL_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()); @@ -230,6 +254,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/AMMCalc_test.cpp b/src/test/app/AMMCalc_test.cpp index 058cdfd1d2d..9fb578dcec9 100644 --- a/src/test/app/AMMCalc_test.cpp +++ b/src/test/app/AMMCalc_test.cpp @@ -176,9 +176,8 @@ class AMMCalc_test : public beast::unit_test::suite std::string toString(STAmount const& a) { - std::stringstream str; - str << a.getText() << "/" << to_string(a.issue().currency); - return str.str(); + return std::format( + "{}/{}", a.getText(), to_string(a.get().currency)); } STAmount @@ -203,8 +202,8 @@ class AMMCalc_test : public beast::unit_test::suite STAmount sin{}; int limitingStep = vp.size(); STAmount limitStepOut{}; - auto trate = [&](auto const& amt) { - auto const currency = to_string(amt.issue().currency); + auto trate = [&](STAmount const& amt) { + auto const currency = to_string(amt.get().currency); return rates.find(currency) != rates.end() ? rates.at(currency) : QUALITY_ONE; }; @@ -268,8 +267,8 @@ class AMMCalc_test : public beast::unit_test::suite STAmount sout{}; int limitingStep = 0; STAmount limitStepIn{}; - auto trate = [&](auto const& amt) { - auto const currency = to_string(amt.issue().currency); + auto trate = [&](STAmount const& amt) { + auto const currency = to_string(amt.get().currency); return rates.find(currency) != rates.end() ? rates.at(currency) : QUALITY_ONE; }; 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/Check_test.cpp b/src/test/app/Check_test.cpp index 2c4f44ce79f..621c20edb15 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -2146,8 +2146,7 @@ class Check_test : public beast::unit_test::suite return; BEAST_EXPECT( - offerAmount.issue().account == - checkAmount.issue().account); + offerAmount.getIssuer() == checkAmount.getIssuer()); BEAST_EXPECT( offerAmount.negative() == checkAmount.negative()); BEAST_EXPECT( 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 9fd4927d5eb..423595ed18d 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" @@ -1530,9 +1544,6 @@ class MPToken_test : public beast::unit_test::suite jrr = env.rpc("json", "sign", to_string(jv1)); BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); }; - auto toSFieldRef = [](SField const& field) { - return std::ref(field); - }; auto setMPTFields = [&](SField const& field, Json::Value& jv, bool withAmount = true) { @@ -1552,22 +1563,6 @@ class MPToken_test : public beast::unit_test::suite // Transactions with amount fields, which can't be MPT. // Transactions with issue 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; @@ -1578,12 +1573,7 @@ class MPToken_test : public beast::unit_test::suite test(jv, field.fieldName); }; for (SField const& field : - {toSFieldRef(sfAmount), - toSFieldRef(sfAmount2), - toSFieldRef(sfEPrice), - toSFieldRef(sfLPTokenOut), - toSFieldRef(sfAsset), - toSFieldRef(sfAsset2)}) + {std::ref(sfEPrice), std::ref(sfLPTokenOut)}) ammDeposit(field); // AMMWithdraw auto ammWithdraw = [&](SField const& field) { @@ -1594,13 +1584,8 @@ class MPToken_test : public beast::unit_test::suite setMPTFields(field, jv); test(jv, field.fieldName); }; - ammWithdraw(sfAmount); for (SField const& field : - {toSFieldRef(sfAmount2), - toSFieldRef(sfEPrice), - toSFieldRef(sfLPTokenIn), - toSFieldRef(sfAsset), - toSFieldRef(sfAsset2)}) + {std::ref(sfEPrice), std::ref(sfLPTokenIn)}) ammWithdraw(field); // AMMBid auto ammBid = [&](SField const& field) { @@ -1610,67 +1595,8 @@ class MPToken_test : public beast::unit_test::suite setMPTFields(field, jv); test(jv, field.fieldName); }; - for (SField const& field : - {toSFieldRef(sfBidMin), - toSFieldRef(sfBidMax), - toSFieldRef(sfAsset), - toSFieldRef(sfAsset2)}) - ammBid(field); - // AMMClawback - auto ammClawback = [&](SField const& field) { - Json::Value jv; - jv[jss::TransactionType] = jss::AMMClawback; - jv[jss::Account] = alice.human(); - jv[jss::Holder] = carol.human(); - setMPTFields(field, jv); - test(jv, field.fieldName); - }; - for (SField const& field : - {toSFieldRef(sfAmount), - toSFieldRef(sfAsset), - toSFieldRef(sfAsset2)}) - ammClawback(field); - // AMMDelete - auto ammDelete = [&](SField const& field) { - Json::Value jv; - jv[jss::TransactionType] = jss::AMMDelete; - jv[jss::Account] = alice.human(); - setMPTFields(field, jv, false); - test(jv, field.fieldName); - }; - ammDelete(sfAsset); - ammDelete(sfAsset2); - // AMMVote - auto ammVote = [&](SField const& field) { - Json::Value jv; - jv[jss::TransactionType] = jss::AMMVote; - jv[jss::Account] = alice.human(); - jv[jss::TradingFee] = 100; - setMPTFields(field, jv, false); - test(jv, field.fieldName); - }; - ammVote(sfAsset); - ammVote(sfAsset2); - // 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()); - } + ammBid(sfBidMin); + ammBid(sfBidMax); // EscrowCreate { Json::Value jv; @@ -1680,13 +1606,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; @@ -2272,48 +2191,1462 @@ class MPToken_test : public beast::unit_test::suite } } -public: void - run() override + testOfferCrossing(FeatureBitset features) { + testcase("Offer Crossing"); using namespace test::jtx; - FeatureBitset const all{supported_amendments()}; + 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}; - // MPTokenIssuanceCreate - testCreateValidation(all); - testCreateEnabled(all); + MPTTester mpt(env, gw, {.holders = {alice}}); - // MPTokenIssuanceDestroy - testDestroyValidation(all); - testDestroyEnabled(all); + auto const lockMPToken = + (flags & (tfMPTCanLock | tfMPTCanTransfer)) == tfMPTCanLock; + auto const lockMPTIssue = + (flags & (tfMPTCanLock | tfMPTCanTransfer)) == + (tfMPTCanLock | tfMPTCanTransfer); + flags = lockMPToken ? (flags | tfMPTCanTransfer) : flags; - // MPTokenAuthorize - testAuthorizeValidation(all); - testAuthorizeEnabled(all); + mpt.create({.ownerCount = 1, .holderCount = 0, .flags = flags}); + auto const MPT = mpt["MPT"]; - // MPTokenIssuanceSet - testSetValidation(all); - testSetEnabled(all); + 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}); - // MPT clawback - testClawbackValidation(all); - testClawback(all); + auto const err = + flags & tfMPTRequireAuth ? tecUNFUNDED_OFFER : tecNO_PERMISSION; - // Test Direct Payment - testPayment(all); - testDepositPreauth(); + env(offer(alice, XRP(100), MPT(101)), ter(err)); + env.close(); + } - // Test MPT Amount is invalid in Tx, which don't support MPT - testMPTInvalidInTx(all); + // MPTokenV2 is disabled + { + Env env{*this, features - featureMPTokensV2}; - // Test parsed MPTokenIssuanceID in API response metadata - testTxJsonMetaFields(all); + MPTTester mpt(env, gw, {.holders = {alice}}); - // Test tokens equality - testTokensEquality(); + mpt.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); - // Test helpers - testHelperFunctions(); + 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 + { + Env env{*this, features}; + MPTTester mpt(env, gw, {.holders = {carol, bob}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + mpt.authorize({.account = bob}); + + // holder to holder + env(pay(carol, bob, MPT(1)), + test::jtx::path(~MPT, ~USD, ~MPT), + sendmax(XRP(1)), + txflags(tfPartialPayment), + ter(temBAD_PATH_LOOP)); + env.close(); + + // issuer to holder + env(pay(gw, bob, MPT(1)), + test::jtx::path(~MPT, ~USD, ~MPT), + sendmax(XRP(1)), + txflags(tfPartialPayment), + ter(temBAD_PATH_LOOP)); + env.close(); + + // holder to issuer + env(pay(bob, gw, MPT(1)), + test::jtx::path(~MPT, ~USD, ~MPT), + sendmax(XRP(1)), + txflags(tfPartialPayment), + ter(temBAD_PATH_LOOP)); + env.close(); + } + + // Rippling + { + Env env{*this, features}; + MPTTester mpt(env, gw, {.holders = {carol, bob}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + mpt.authorize({.account = bob}); + + // holder to holder + env(pay(carol, bob, MPT(1)), + test::jtx::path(~MPT, gw), + sendmax(XRP(1)), + txflags(tfPartialPayment), + ter(temBAD_PATH)); + env.close(); + + // issuer to holder + env(pay(gw, bob, MPT(1)), + test::jtx::path(~MPT, carol), + sendmax(XRP(1)), + txflags(tfPartialPayment), + ter(temBAD_PATH)); + env.close(); + + // holder to issuer + env(pay(bob, gw, MPT(1)), + test::jtx::path(~MPT, carol), + sendmax(XRP(1)), + txflags(tfPartialPayment), + ter(temBAD_PATH)); + env.close(); + } + + // 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}); + mpt2.authorize({.account = bob}); + mpt2.pay(gw, bob, 200); + + env(offer(alice, MPT2(100), MPT1(100))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{MPT2(100), MPT1(100)}}})); + + // holder to holder + env(pay(carol, bob, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 190)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 10)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(200)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 10)); + + // issuer to holder + env(pay(gw, bob, MPT1(20)), + test::jtx::path(~MPT1), + sendmax(MPT2(20)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 170)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 30)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(200)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(420)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 30)); + + // holder to issuer + env(pay(bob, gw, MPT1(70)), + test::jtx::path(~MPT1), + sendmax(MPT2(70)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 100)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 100)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(130)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(420)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 30)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 130)); + } + + // MPT/MPT, issuer owns the offer + { + Env env{*this, features}; + + MPTTester mpt1(env, gw, {.holders = {carol, bob}}); + mpt1.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + + MPTTester mpt2(env, gw, {.holders = {carol, bob}, .fund = false}); + mpt2.create( + {.ownerCount = 2, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT2 = mpt2["MPT2"]; + + mpt2.authorize({.account = carol}); + mpt2.pay(gw, carol, 200); + + mpt1.authorize({.account = bob}); + mpt2.authorize({.account = bob}); + mpt2.pay(gw, bob, 200); + + env(offer(gw, MPT2(100), MPT1(100))); + env.close(); + BEAST_EXPECT( + expectOffers(env, gw, 1, {{Amounts{MPT2(100), MPT1(100)}}})); + + // holder to holder + env(pay(carol, bob, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, gw, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(10)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(390)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 10)); + + // issuer to holder + env(pay(gw, bob, MPT1(20)), + test::jtx::path(~MPT1), + sendmax(MPT2(20)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, gw, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(30)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(390)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 30)); + + // holder to issuer + env(pay(bob, gw, MPT1(70)), + test::jtx::path(~MPT1), + sendmax(MPT2(70)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, gw, 0)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(30)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(320)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 30)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 130)); + } + + // MPT/MPT, different issuer + { + Env env{*this, features}; + Account gw1{"gw1"}; + + MPTTester mpt1(env, gw, {.holders = {alice, carol, bob}}); + mpt1.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + + env.fund(XRP(1'000), gw1); + MPTTester mpt2( + env, gw1, {.holders = {alice, carol, bob}, .fund = false}); + mpt2.create( + {.ownerCount = 1, + .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(gw1, carol, 200); + + mpt1.authorize({.account = bob}); + mpt1.pay(gw, bob, 200); + mpt2.authorize({.account = bob}); + mpt2.pay(gw1, bob, 200); + + mpt1.authorize({.account = gw1}); + mpt1.pay(gw, gw1, 200); + + mpt2.authorize({.account = gw}); + mpt2.pay(gw1, gw, 200); + + env(offer(alice, MPT2(100), MPT1(100))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{MPT2(100), MPT1(100)}}})); + + env(pay(carol, bob, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(600)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(600)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 200)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 200)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 210)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 200)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 190)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 10)); + + env(pay(bob, gw, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(600)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 200)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 200)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 210)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 180)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 20)); + + env(pay(gw, bob, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(600)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 200)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 220)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 170)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 30)); + + env(pay(bob, gw1, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(600)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 210)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 220)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 180)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 160)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 40)); + + env(pay(gw1, bob, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(610)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 210)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 230)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 180)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 150)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 50)); + + env(pay(gw, gw1, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(610)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 220)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 180)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 140)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 60)); + + env(pay(gw1, gw, MPT1(40)), + test::jtx::path(~MPT1), + sendmax(MPT2(40)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(550)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(650)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 220)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 180)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 100)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 100)); + } + + // MPT/IOU IOU/MPT1 + { + Env env = pathTestEnv(*this); + Account const gw1{"gw1"}; + Account const gw2{"gw2"}; + Account const dan{"dan"}; + env.fund(XRP(1'000), gw2); + auto const USD = 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, USD(400))); + env(pay(gw2, alice, USD(200))); + env(trust(bob, USD(400))); + + env(offer(alice, MPT(100), USD(100))); + env(offer(bob, USD(100), MPT1(100))); + env.close(); + + env(pay(carol, dan, MPT1(100)), + sendmax(MPT(100)), + path(~USD, ~MPT1), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(expectOffers(env, bob, 0)); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 100)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(dan, 100)); + } + + // 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 + // EUR/MPT1 MPT1/MPT2 MPT2/USD USD/CRN AMM:CRN/MPT MPT/YAN + { + 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(carol, EUR) == EUR(900)); + 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)); + } + + // Multi-steps with AMM and MPT endpoints + // MPT1/EUR EUR/MPT2 MPT2/USD USD/CRN AMM:CRN/MPT3 MPT3/MPT4 + { + Env env{*this, features}; + auto const USD = gw["USD"]; + auto const EUR = gw["EUR"]; + auto const CRN = gw["CRN"]; + + fund( + env, + gw, + {alice, carol, bob}, + XRP(1'000), + {USD(1'000), EUR(1'000), CRN(2'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(); + auto [mpt4, MPT4] = createMPT(); + mpt4.authorize({.account = bob}); + + 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), MPT4(100))); + + env(pay(carol, bob, MPT4(100)), + test::jtx::path(~MPT1, ~MPT2, ~USD, ~CRN, ~MPT3, ~MPT4), + sendmax(EUR(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT(env.balance(carol, EUR) == EUR(900)); + BEAST_EXPECT(mpt4.checkMPTokenAmount(bob, 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, {.fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + AMM amm(env, gw, MPT(100), XRP(100)); + amm.deposit(DepositArg{.account = alice, .asset1In = XRP(10)}); + amm::ammClawback( + gw, alice, MPTIssue(mpt.issuanceID()), xrpIssue(), MPT(10)); + } + + { + 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, {.fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 1'000); + auto const MPT = mpt["MPT"]; + AMM amm(env, gw, MPT(100), XRP(100)); + amm.deposit(DepositArg{.account = alice, .tokens = 10'000}); + amm::ammClawback( + gw, alice, MPTIssue(mpt.issuanceID()), xrpIssue(), MPT(10)); + } + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + + // MPTokenIssuanceCreate + testCreateValidation(all); + testCreateEnabled(all); + + // MPTokenIssuanceDestroy + testDestroyValidation(all); + testDestroyValidation(all - featureMPTokensV2); + testDestroyEnabled(all); + + // MPTokenAuthorize + testAuthorizeValidation(all); + testAuthorizeValidation(all - featureMPTokensV2); + testAuthorizeEnabled(all); + + // MPTokenIssuanceSet + testSetValidation(all); + testSetEnabled(all); + + // MPT clawback + testClawbackValidation(all); + testClawbackValidation(all - featureMPTokensV2); + testClawback(all); + testClawback(all - featureMPTokensV2); + + // Test Direct Payment + testPayment(all); + testPayment(all - featureMPTokensV2); + + testDepositPreauth(all); + testDepositPreauth(all - featureMPTokensV2); + + // Test MPT Amount is invalid in Tx, which don't support MPT + testMPTInvalidInTx(all); + + // Test parsed MPTokenIssuanceID in API response metadata + testTxJsonMetaFields(all); + + // Test tokens equality + testTokensEquality(); + + // 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..8f1f646eb5f 100644 --- a/src/test/app/PayStrand_test.cpp +++ b/src/test/app/PayStrand_test.cpp @@ -1255,13 +1255,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/Taker_test.cpp b/src/test/app/Taker_test.cpp index 89e44b2b98b..b294e9f827b 100644 --- a/src/test/app/Taker_test.cpp +++ b/src/test/app/Taker_test.cpp @@ -180,10 +180,13 @@ class Taker_test : public beast::unit_test::suite std::string format_amount(STAmount const& amount) { - std::string txt = amount.getText(); - txt += "/"; - txt += to_string(amount.issue().currency); - return txt; + if (amount.holds()) + return std::format( + "{}/{}", + amount.getText(), + to_string(amount.get().currency)); + return std::format( + "{}/{}", amount.getText(), to_string(amount.get())); } void 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 9990c77c38c..3491f1de47f 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/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index 575e2e1d889..fb7753c6ad4 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -63,7 +63,7 @@ fund( for (auto const& amt : amts) { env.trust(amt + amt, account); - env(pay(amt.issue().account, account, amt)); + env(pay(amt.getIssuer(), account, amt)); } } env.close(); diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index b39cac7dcc1..8f08d37acbe 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -76,7 +76,7 @@ stpath_append_one(STPath& st, STPathElement const& pe) bool equal(STAmount const& sa1, STAmount const& sa2) { - return sa1 == sa2 && sa1.issue().account == sa2.issue().account; + return sa1 == sa2 && sa1.getIssuer() == sa2.getIssuer(); } // Issue path element @@ -86,9 +86,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)); +} /******************************************************************************/ @@ -131,7 +285,7 @@ expectLine( } auto amount = sle->getFieldAmount(sfBalance); - amount.setIssuer(value.issue().account); + amount.setIssuer(value.getIssuer()); if (!accountLow) amount.negate(); return amount == value && expectDefaultTrustLine; diff --git a/src/test/jtx/impl/amount.cpp b/src/test/jtx/impl/amount.cpp index 5be53dc0a95..450f4d7b634 100644 --- a/src/test/jtx/impl/amount.cpp +++ b/src/test/jtx/impl/amount.cpp @@ -90,10 +90,16 @@ operator<<(std::ostream& os, PrettyAmount const& amount) os << to_places(d, 6) << " XRP"; } + else if (amount.value().holds()) + { + os << amount.value().getText() << "/" + << to_string(amount.value().get().currency) << "(" + << amount.name() << ")"; + } else { os << amount.value().getText() << "/" - << to_string(amount.value().issue().currency) << "(" << amount.name() + << to_string(amount.value().get()) << "(" << amount.name() << ")"; } return os; @@ -123,6 +129,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/balance.cpp b/src/test/jtx/impl/balance.cpp index 42330658eb0..afc6acfb18e 100644 --- a/src/test/jtx/impl/balance.cpp +++ b/src/test/jtx/impl/balance.cpp @@ -38,7 +38,7 @@ balance::operator()(Env& env) const env.test.expect(sle->getFieldAmount(sfBalance) == value_); } } - else + else if (value_.holds()) { auto const sle = env.le(keylet::line(account_.id(), value_.issue())); if (none_) @@ -48,12 +48,28 @@ balance::operator()(Env& env) const else if (env.test.expect(sle)) { auto amount = sle->getFieldAmount(sfBalance); - amount.setIssuer(value_.issue().account); - if (account_.id() > value_.issue().account) + amount.setIssuer(value_.getIssuer()); + if (account_.id() > value_.getIssuer()) amount.negate(); env.test.expect(amount == value_); } } + else + { + auto const issuanceKey = + keylet::mptIssuance(value_.get().getMptID()); + auto const mptokenKey = keylet::mptoken(issuanceKey.key, account_); + auto const sle = env.le(mptokenKey); + if (none_) + { + env.test.expect(!sle); + } + else if (env.test.expect(sle)) + { + auto amount = sle->getFieldU64(sfMPTAmount); + env.test.expect(amount == value_.mpt().value()); + } + } } } // 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/test/rpc/AMMInfo_test.cpp b/src/test/rpc/AMMInfo_test.cpp index c1e059a3ead..fd72de804d5 100644 --- a/src/test/rpc/AMMInfo_test.cpp +++ b/src/test/rpc/AMMInfo_test.cpp @@ -190,8 +190,6 @@ class AMMInfo_test : public jtx::AMMTestBase using namespace jtx; testAMM([&](AMM& ammAlice, Env&) { - BEAST_EXPECT(ammAlice.expectAmmRpcInfo( - XRP(10000), USD(10000), IOUAmount{10000000, 0})); BEAST_EXPECT(ammAlice.expectAmmRpcInfo( XRP(10000), USD(10000), @@ -200,6 +198,33 @@ class AMMInfo_test : public jtx::AMMTestBase std::nullopt, ammAlice.ammAccount())); }); + + { + Env env{*this}; + env.fund(XRP(1'000), gw); + MPTTester mpt(env, gw, {.fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + MPTTester mpt1(env, gw, {.fund = false}); + mpt1.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + auto const MPT1 = mpt1["MPT"]; + std::vector> + pools = { + {XRP(100), MPT(100), IOUAmount{100000}}, + {USD(100), MPT(100), IOUAmount{100}}, + {MPT(100), MPT1(100), IOUAmount{100}}}; + for (auto& pool : pools) + { + AMM amm(env, gw, std::get<0>(pool), std::get<1>(pool)); + BEAST_EXPECT(amm.expectAmmRpcInfo( + std::get<0>(pool), + std::get<1>(pool), + std::get<2>(pool), + std::nullopt, + std::nullopt, + amm.ammAccount())); + } + } } void diff --git a/src/xrpld/app/ledger/AcceptedLedgerTx.cpp b/src/xrpld/app/ledger/AcceptedLedgerTx.cpp index 6bdb602fd83..1097986267d 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 265e0b62905..720dea6f93b 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].get(); - auto const issue2 = (*sle)[sfAsset2].get(); - 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 c6c0c808bfe..dd393bb74fa 100644 --- a/src/xrpld/app/misc/AMMHelpers.h +++ b/src/xrpld/app/misc/AMMHelpers.h @@ -148,11 +148,11 @@ withinRelativeDistance( * @param dist requested relative distance * @return true if within dist, false otherwise */ -// clang-format off template requires( 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) bool withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist) { @@ -161,7 +161,6 @@ withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist) auto const [min, max] = std::minmax(calc, req); return ((max - min) / max) < dist; } -// clang-format on /** Solve quadratic equation to find takerGets or takerPays. Round * to minimize the amount in order to maximize the quality. @@ -226,7 +225,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 +296,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 +373,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 +384,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 +408,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 +500,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 +509,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 +568,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 +582,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 8e483811145..dc13a008d39 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(); } @@ -4152,8 +4153,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; @@ -4163,7 +4164,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) @@ -4211,7 +4212,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. @@ -4240,9 +4241,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) @@ -4262,9 +4263,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. @@ -4287,7 +4288,7 @@ NetworkOPsImp::getBookPage( std::min( saTakerPays, multiply( - saTakerGetsFunded, saDirRate, saTakerPays.issue())) + saTakerGetsFunded, saDirRate, saTakerPays.asset())) .setJson(jvOffer[jss::taker_pays_funded]); } @@ -4438,7 +4439,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 8724c413a68..0181dc7361b 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 f5f6ae6612c..efa0832fad1 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,14 +46,15 @@ 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 issue1 = ammSle[sfAsset].get(); - auto const issue2 = ammSle[sfAsset2].get(); + auto const issues = [&]() -> std::optional> { + auto const issue1 = ammSle[sfAsset]; + auto const issue2 = ammSle[sfAsset2]; if (optIssue1 && optIssue2) { if (invalidAMMAssetPair( @@ -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].get().currency, - ammSle[sfAsset2].get().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..22e808352f3 --- /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; + XRPL_ASSERT(allMPTTx || isDEX, "ripple::isMPTAllowed : all MPT tx or DEX"); + + 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 84% rename from src/xrpld/app/paths/RippleLineCache.cpp rename to src/xrpld/app/paths/AssetCache.cpp index 38b630eeb7e..99082d39782 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); @@ -131,4 +129,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 3df8f6f9992..02472bda247 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,32 @@ 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( + // 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&) { + using TIn_ = typename TIn::amount_type; + using TOut_ = typename TOut::amount_type; + 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)); - } - - XRPL_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)); + }, + srcAsset.getAmountType(), + dstAsset.getAmountType()); } } // namespace ripple diff --git a/src/xrpld/app/paths/PathRequest.cpp b/src/xrpld/app/paths/PathRequest.cpp index 643923320a2..d08b3febd39 100644 --- a/src/xrpld/app/paths/PathRequest.cpp +++ b/src/xrpld/app/paths/PathRequest.cpp @@ -170,7 +170,7 @@ PathRequest::updateComplete() } bool -PathRequest::isValid(std::shared_ptr const& crCache) +PathRequest::isValid(std::shared_ptr const& crCache) { if (!raSrcAccount || !raDstAccount) return false; @@ -223,6 +223,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); } @@ -243,7 +250,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; @@ -314,11 +321,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); @@ -336,11 +341,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; @@ -357,47 +360,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 (srcPathAsset == saSendMax->asset()) { // If neither is the source and they are not equal, then the // source issuer is illegal. @@ -411,26 +439,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()}); } } } @@ -466,21 +505,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, @@ -489,54 +528,58 @@ 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, - dst_amount, - level, - continueCallback); + cache, pathasset_map, asset, dst_amount, level, continueCallback); if (!pathfinder) { JLOG(m_journal.debug()) << iIdentifier << " No paths found"; @@ -547,23 +590,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"; @@ -620,7 +672,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); @@ -648,14 +702,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) { @@ -675,11 +729,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 5864357aec4..cfe47ce3daa 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")) { XRPL_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 (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 = 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,37 @@ Pathfinder::addLink( { bool const bRequireAuth( sleEnd->getFieldU32(sfFlags) & lsfRequireAuth); - bool const bIsEndCurrency( - uEndCurrency == mDstAmount.getCurrency()); + bool const bIsEndAsset(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 +1096,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 +1158,10 @@ Pathfinder::addLink( { // save this candidate int out = getPathsOut( - uEndCurrency, + uEndPathAsset, acct, direction, - bIsEndCurrency, + bIsEndAsset, mEffectiveDst, continueCallback); if (out) @@ -1077,40 +1169,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 +1233,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 +1248,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 +1257,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 +1273,7 @@ Pathfinder::addLink( xrpCurrency(), xrpAccount()); - if (mDstAmount.getCurrency().isZero()) + if (isXRP(mDstAmount.asset())) { // destination is XRP, add account and path is // complete @@ -1180,10 +1286,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 +1301,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 +1334,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 813554ba7ff..073a30c4671 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 d57e9140f80..a58174994db 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,20 +179,27 @@ get(EitherAmount& amt) } template <> -inline IOUAmount& +inline IOUAmount const& get(EitherAmount& amt) { - XRPL_ASSERT( - !amt.native, "ripple::get(EitherAmount&) : is not XRP"); - return amt.iou; + XRPL_ASSERT(amt.isIOU(), "ripple::get(EitherAmount&) : is IOU"); + return amt.iou(); } template <> -inline XRPAmount& +inline XRPAmount const& get(EitherAmount& amt) { - XRPL_ASSERT(amt.native, "ripple::get(EitherAmount&) : is XRP"); - return amt.xrp; + XRPL_ASSERT(amt.native(), "ripple::get(EitherAmount&) : is XRP"); + return amt.xrp(); +} + +template <> +inline MPTAmount const& +get(EitherAmount& amt) +{ + XRPL_ASSERT(amt.isMPT(), "ripple::get(EitherAmount&) : is MPT"); + return amt.mpt(); } template @@ -151,9 +215,9 @@ inline IOUAmount const& get(EitherAmount const& amt) { XRPL_ASSERT( - !amt.native, + !amt.native(), "ripple::get(EitherAmount const&) : is not XRP"); - return amt.iou; + return amt.iou(); } template <> @@ -161,8 +225,17 @@ inline XRPAmount const& get(EitherAmount const& amt) { XRPL_ASSERT( - amt.native, "ripple::get(EitherAmount const&) : is XRP"); - return amt.xrp; + amt.native(), "ripple::get(EitherAmount const&) : is XRP"); + return amt.xrp(); +} + +template <> +inline MPTAmount const& +get(EitherAmount const& amt) +{ + XRPL_ASSERT( + amt.isMPT(), "ripple::get(EitherAmount const&) : is MPT"); + return amt.mpt(); } inline AmountSpec @@ -176,16 +249,20 @@ 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.amount = XRPAmount(sMant); + } + else if (amt.holds()) { - result.xrp = XRPAmount(sMant); + result.mptid = amt.get().getMptID(); + result.amount = amt.mpt(); } else { - result.iou = IOUAmount(sMant, amt.exponent()); - result.issuer = amt.issue().account; - result.currency = amt.issue().currency; + result.amount = IOUAmount(sMant, amt.exponent()); + result.issuer = amt.get().account; + result.currency = amt.get().currency; } return result; @@ -196,27 +273,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; XRPL_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 1d35f80b183..e92b28d8f90 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,13 +190,18 @@ 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(); } + Rate + rate(ReadView const& view, Asset const& asset, AccountID const& dstAccount) + const; + private: friend bool operator==(BookStep const& lhs, BookStep const& rhs) @@ -337,19 +343,14 @@ 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_) - return parityRate; - return transferRate(v, id); - }; - - auto const trIn = - redeems(prevStepDir) ? rate(this->book_.in.account) : parityRate; + auto const trIn = redeems(prevStepDir) + ? this->rate(v, this->book_.in, this->strandDst_) + : 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) + ? this->rate(v, this->book_.out, this->strandDst_) : parityRate; Quality const q1{getRate(STAmount(trOut.value), STAmount(trIn.value))}; @@ -391,8 +392,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)) @@ -540,14 +541,9 @@ class BookOfferCrossingStep (this->ammLiquidity_ && this->ammLiquidity_->multiPath())) return ofrQ; - auto rate = [&](AccountID const& id) { - if (isXRP(id) || id == this->strandDst_) - return parityRate; - return transferRate(v, id); - }; - - auto const trIn = - redeems(prevStepDir) ? rate(this->book_.in.account) : parityRate; + auto const trIn = redeems(prevStepDir) + ? this->rate(v, this->book_.in, this->strandDst_) + : parityRate; // AMM doesn't pay the transfer fee on the out amount auto const trOut = parityRate; @@ -723,17 +719,13 @@ 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_) - return QUALITY_ONE; - return transferRate(sb, id).value; - }; - - std::uint32_t const trIn = - redeems(prevStepDir) ? rate(book_.in.account) : QUALITY_ONE; + std::uint32_t const trIn = redeems(prevStepDir) + ? rate(sb, book_.in, this->strandDst_).value + : 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; + std::uint32_t const trOut = ownerPaysTransferFee_ + ? rate(sb, book_.out, this->strandDst_).value + : QUALITY_ONE; typename FlowOfferStream::StepCounter counter( maxOffersToConsume_, j_); @@ -741,7 +733,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) { @@ -756,36 +747,19 @@ BookStep::forEachOffer( strandSrc_, strandDst_, offer, ofrQ, offers, offerAttempted)) return true; - // 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)) + // Make sure offer owner has authorization to own Assets from issuer. + // An account can always own XRP or their own Assets. + 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 +867,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 +881,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) @@ -1354,20 +1328,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)) @@ -1381,20 +1356,53 @@ 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 issuanceID = + keylet::mptIssuance(book_.in.get().getMptID()); + if (!view.exists(issuanceID)) + return tecOBJECT_NOT_FOUND; + + if (auto const ter = isMPTDEXAllowed( + view, + book_.in, + book_.in.getIssuer(), + book_.in.getIssuer()); + ter != tesSUCCESS) + return ter; + } } } return tesSUCCESS; } +template +Rate +BookStep::rate( + ReadView const& view, + Asset const& asset, + AccountID const& dstAccount) const +{ + if (isXRP(asset) || asset.getIssuer() == dstAccount) + return parityRate; + if (asset.holds()) + return transferRate(view, asset.getIssuer()); + return transferRate(view, asset.get().getMptID()); +}; + //------------------------------------------------------------------------------ namespace test { @@ -1412,29 +1420,20 @@ equalHelper(Step const& step, ripple::Book const& book) 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; + return std::visit( + [&](TIn const&, TOut const&) { + using TIn_ = typename TIn::amount_type; + using TOut_ = typename TOut::amount_type; + return equalHelper>( + step, book); + }, + book.in.getAmountType(), + book.out.getAmountType()); } } // namespace test @@ -1442,7 +1441,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; @@ -1484,4 +1483,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 95e64b337bc..00832972062 100644 --- a/src/xrpld/app/paths/detail/DirectStep.cpp +++ b/src/xrpld/app/paths/detail/DirectStep.cpp @@ -722,7 +722,8 @@ DirectStepI::validFwd( auto const savCache = *cache_; - XRPL_ASSERT(!in.native, "ripple::DirectStepI::validFwd : input is not XRP"); + XRPL_ASSERT( + !in.native(), "ripple::DirectStepI::validFwd : input is not XRP"); auto const [maxSrcToDst, srcDebtDir] = static_cast(this)->maxFlow(sb, cache_->srcToDst); @@ -731,7 +732,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 +940,13 @@ DirectStepI::check(StrandContext const& ctx) const // issue if (auto book = ctx.prevStep->bookStepBook()) { - if (book->out != srcIssue) + if (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 4c3ea5faf1b..8223f31ca5d 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..6d69d82a82a --- /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); + XRPL_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) +{ + XRPL_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_; + + XRPL_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 + + XRPL_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 b73b1ac8acc..b642fa18b3e 100644 --- a/src/xrpld/app/paths/detail/PaySteps.cpp +++ b/src/xrpld/app/paths/detail/PaySteps.cpp @@ -57,12 +57,6 @@ checkNear(IOUAmount const& expected, IOUAmount const& actual) return r <= ratTol; }; -bool -checkNear(XRPAmount const& expected, XRPAmount const& actual) -{ - return expected == actual; -}; - static bool isXRPAccount(STPathElement const& pe) { @@ -76,13 +70,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()); } @@ -90,10 +84,34 @@ toStep( if (ctx.isLast && isXRPAccount(*e1) && e2->isAccount()) return make_XRPEndpointStep(ctx, e2->getAccountID()); + // MPTEndpointStep is created in following cases: + // 1 Direct payment between an issuer and a holder + // e1 is issuer and e2 is holder or vise versa + // There is only one step in this case: holder->issuer or + // issuer->holder + // 2 Direct payment between the holders + // e1 is issuer and e2 is holder or vise versa + // There are two steps in this case: holder->issuer->holder1 + // 3 Cross-token payment with Amount or SendMax or both MPT + // If destination is an issuer then the last step is BookStep, + // otherwise the last step is MPTEndpointStep where e1 is + // the issuer and e2 is the holder. + // In all cases MPTEndpointStep is always first or last step, + // e1/e2 are always account types, and curAsset is always MPT. + 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 +124,17 @@ toStep( } XRPL_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 +142,34 @@ toStep( XRPL_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 +177,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,16 +187,22 @@ 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{}}; - for (auto const& pe : path) + if ((deliver.holds() && deliver.getIssuer() == beast::zero) || + (sendMaxAsset && sendMaxAsset->holds() && + sendMaxAsset->getIssuer() == beast::zero)) + return {temBAD_PATH, Strand{}}; + + for (std::size_t i = 0; i < path.size(); ++i) { + auto const& pe = path[i]; auto const t = pe.getNodeType(); if ((t & ~STPathElement::typeAll) || !t) @@ -166,6 +211,8 @@ 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; + bool const hasAsset = t & STPathElement::typeAsset; if (hasAccount && (hasIssuer || hasCurrency)) return {temBAD_PATH, Strand{}}; @@ -185,18 +232,33 @@ 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{}}; + + // No rippling if MPT + if (i > 0 && path[i - 1].hasMPT() && + (hasAccount || (hasIssuer && !hasAsset))) + 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; + // First step ripples from the source to the issuer. + 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 +266,30 @@ toStrand( // sendmax and deliver. normPath.reserve(4 + path.size()); { - normPath.emplace_back( - STPathElement::typeAll, src, curIssue.currency, curIssue.account); - - if (sendMaxIssue && sendMaxIssue->account != src && + // The first step of a path is always implied to be the sender of the + // transaction, as defined by the transaction's Account field. The Asset + // is either SendMax or Deliver. + auto const t = [&]() { + auto const t = + STPathElement::typeAccount | STPathElement::typeIssuer; + if (curAsset.holds()) + return t | STPathElement::typeMPT; + return t | STPathElement::typeCurrency; + }(); + // If MPT then the issuer is the actual issuer, it is never the source + // account. + normPath.emplace_back(t, src, curAsset, curAsset.getIssuer()); + + // If transaction includes SendMax with the issuer, which is not + // the sender of the transaction, that issuer is implied to be + // the second step of the path. Unless the path starts at an 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,25 +297,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.account. Note + // that MPTIssue can't change the 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 the Amount field of the transaction includes an issuer that is not + // the same as the Destination of the transaction, that issuer is + // implied to be the second-to-last step of the path. If normPath.back + // is an offer, which sells MPT then the added path element account is + // the MPT's issuer. 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); } + // Last step of a path is always implied to be the receiver of a + // transaction, as defined by the transaction's Destination field. if (!normPath.back().isAccount() || normPath.back().getAccountID() != dst) { @@ -261,11 +347,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 +365,7 @@ toStrand( ownerPaysTransferFee, offerCrossing, isDefaultPath, - seenDirectIssues, + seenDirectAssets, seenBookOuts, ammContext, j}; @@ -298,36 +384,66 @@ 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_) -> std::pair> { + if (asset_.holds()) + { + JLOG(j.error()) << "MPT is invalid with rippling"; + return {temBAD_PATH, nullptr}; + } + 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()) + { + JLOG(j.error()) << "MPT is invalid with rippling"; + return {temBAD_PATH, Strand{}}; + } 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 +451,23 @@ 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()) + { + JLOG(j.error()) << "MPT is invalid with rippling"; + return {temBAD_PATH, Strand{}}; + } 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 +475,12 @@ toStrand( } else if (cur->isOffer() && next->isAccount()) { - if (curIssue.account != next->getAccountID() && + // If the offer sells MPT, then next's account is always the issuer. + // Therefore, this block never executes if MPT. + 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 +497,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 +507,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 +516,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 +531,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 +555,27 @@ 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())) return false; - if (curIss.account != deliver.account && curIss.account != dst) + if (curAsset.getIssuer() != deliver.getIssuer() && + curAsset.getIssuer() != dst) return false; return true; }; @@ -471,9 +595,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 +710,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 +732,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 +759,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..931a9479b95 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 @@ -31,6 +32,7 @@ #include namespace ripple { + class PaymentSandbox; class ReadView; class ApplyView; @@ -362,8 +364,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 +380,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 +396,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 +435,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 +446,7 @@ toStrands( beast::Journal j); /// @cond INTERNAL -template +template struct StepImp : public Step { explicit StepImp() = default; @@ -515,8 +517,13 @@ class FlowException : public std::runtime_error // Check equal with tolerance bool checkNear(IOUAmount const& expected, IOUAmount const& actual); +template + requires(std::is_same_v || std::is_same_v) bool -checkNear(XRPAmount const& expected, XRPAmount const& actual); +checkNear(T const& expected, T const& actual) +{ + return expected == actual; +} /// @endcond /** @@ -527,7 +534,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 +552,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 +568,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 +606,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 +625,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 0e168b73cce..4e1d830827a 100644 --- a/src/xrpld/app/paths/detail/StrandFlow.h +++ b/src/xrpld/app/paths/detail/StrandFlow.h @@ -403,9 +403,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 +559,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 ab211a7c856..80220d2e1dd 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; } @@ -309,9 +310,10 @@ XRPEndpointStep::validFwd( return {false, EitherAmount(XRPAmount(beast::zero))}; } - XRPL_ASSERT(in.native, "ripple::XRPEndpointStep::validFwd : input is XRP"); + XRPL_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) @@ -364,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 e8a14c14922..aec7d953f81 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; @@ -46,8 +51,7 @@ AMMBid::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - if (auto const res = invalidAMMAssetPair( - ctx.tx[sfAsset].get(), ctx.tx[sfAsset2].get())) + if (auto const res = invalidAMMAssetPair(ctx.tx[sfAsset], ctx.tx[sfAsset2])) { JLOG(ctx.j.debug()) << "AMM Bid: Invalid asset pair."; return res; diff --git a/src/xrpld/app/tx/detail/AMMClawback.cpp b/src/xrpld/app/tx/detail/AMMClawback.cpp index 162224ff913..bba09a80040 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.cpp +++ b/src/xrpld/app/tx/detail/AMMClawback.cpp @@ -39,6 +39,15 @@ AMMClawback::preflight(PreflightContext const& ctx) if (!ctx.rules.enabled(featureAMMClawback)) return temDISABLED; + std::optional const clawAmount = ctx.tx[~sfAmount]; + auto const asset = ctx.tx[sfAsset]; + auto const asset2 = ctx.tx[sfAsset2]; + + if (!ctx.rules.enabled(featureMPTokensV2) && + ((clawAmount && clawAmount->holds()) || + asset.holds() || asset2.holds())) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; // LCOV_EXCL_LINE @@ -56,14 +65,10 @@ AMMClawback::preflight(PreflightContext const& ctx) return temMALFORMED; } - std::optional const clawAmount = ctx.tx[~sfAmount]; - auto const asset = ctx.tx[sfAsset].get(); - auto const asset2 = ctx.tx[sfAsset2].get(); - 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,14 +76,14 @@ 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."; return temMALFORMED; } - if (clawAmount && clawAmount->get() != asset) + if (clawAmount && clawAmount->issue() != asset) { JLOG(ctx.j.trace()) << "AMMClawback: Amount's issuer/currency subfield " "does not match Asset field"; @@ -94,8 +99,8 @@ AMMClawback::preflight(PreflightContext const& ctx) TER AMMClawback::preclaim(PreclaimContext const& ctx) { - auto const asset = ctx.tx[sfAsset].get(); - auto const asset2 = ctx.tx[sfAsset2].get(); + auto const asset = ctx.tx[sfAsset]; + auto const asset2 = ctx.tx[sfAsset2]; auto const sleIssuer = ctx.view.read(keylet::account(ctx.tx[sfAccount])); if (!sleIssuer) return terNO_ACCOUNT; // LCOV_EXCL_LINE @@ -139,8 +144,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].get(); - Issue const asset2 = ctx_.tx[sfAsset2].get(); + Asset const asset = ctx_.tx[sfAsset]; + Asset const asset2 = ctx_.tx[sfAsset2]; auto ammSle = sb.peek(keylet::amm(asset, asset2)); if (!ammSle) @@ -157,6 +162,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..1b37836cc7d 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,35 @@ applyCreate( return {res, false}; } - auto sendAndTrustSet = [&](STAmount const& amount) -> TER { + auto sendAndInitTrustOrMPT = [&](STAmount const& amount) -> TER { + // Authorize MPT + if (amount.holds()) + { + auto const& mptIssue = amount.get(); + if (auto const err = requireAuth( + ctx_.view(), mptIssue, account_, MPTAuthType::WeakAuth); + err != tesSUCCESS) + return err; + + auto const& mptID = mptIssue.getMptID(); + auto const mptokenKey = keylet::mptoken(mptID, *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] = mptID; + (*mptoken)[sfFlags] = 0; + (*mptoken)[sfOwnerNode] = *ownerNode; + sb.insert(mptoken); + } + if (auto const res = accountSend( sb, account_, @@ -318,8 +378,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 +393,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 +406,7 @@ applyCreate( } // Send asset2. - res = sendAndTrustSet(amount2); + res = sendAndInitTrustOrMPT(amount2); if (res != tesSUCCESS) { JLOG(j_.debug()) << "AMM Instance: failed to send " << amount2; @@ -355,15 +417,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 430ac17e87b..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; @@ -72,8 +77,8 @@ AMMDelete::doApply() // as we go on processing transactions. Sandbox sb(&ctx_.view()); - auto const ter = deleteAMMAccount( - sb, ctx_.tx[sfAsset].get(), ctx_.tx[sfAsset2].get(), j_); + auto const ter = + deleteAMMAccount(sb, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], j_); if (ter == tesSUCCESS || ter == tecINCOMPLETE) sb.apply(ctx_.rawView()); diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 675f560098c..416bb4cc2d5 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; @@ -100,18 +108,18 @@ AMMDeposit::preflight(PreflightContext const& ctx) return temMALFORMED; } - auto const asset = ctx.tx[sfAsset].get(); - auto const asset2 = ctx.tx[sfAsset2].get(); + auto const asset = ctx.tx[sfAsset]; + auto const asset2 = ctx.tx[sfAsset2]; if (auto const res = invalidAMMAssetPair(asset, asset2)) { JLOG(ctx.j.debug()) << "AMM Deposit: invalid asset pair."; 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,8 +258,9 @@ 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 { - if (auto const ter = requireAuth(ctx.view, asset, accountID)) + auto checkAsset = [&](Asset const& asset) -> TER { + if (auto const ter = requireAuth( + ctx.view, asset, accountID, MPTAuthType::WeakAuth)) { JLOG(ctx.j.debug()) << "AMM Deposit: account is not authorized, " << asset; @@ -260,7 +271,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; } @@ -268,10 +279,10 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) return tesSUCCESS; }; - if (auto const ter = checkAsset(ctx.tx[sfAsset].get())) + if (auto const ter = checkAsset(ctx.tx[sfAsset])) return ter; - if (auto const ter = checkAsset(ctx.tx[sfAsset2].get())) + if (auto const ter = checkAsset(ctx.tx[sfAsset2])) return ter; } @@ -287,17 +298,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 +316,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 +375,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 +402,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 +546,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 +673,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 +733,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 +743,7 @@ AMMDeposit::equalDepositLimit( ammAccount, amountBalance, amount, - toSTAmount(amount2Balance.issue(), amount2Deposit), + toSTAmount(amount2Balance.asset(), amount2Deposit), lptAMMBalance, tokens, std::nullopt, @@ -901,7 +923,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 1b8b91e518a..c57198162aa 100644 --- a/src/xrpld/app/tx/detail/AMMVote.cpp +++ b/src/xrpld/app/tx/detail/AMMVote.cpp @@ -35,11 +35,15 @@ 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; - if (auto const res = invalidAMMAssetPair( - ctx.tx[sfAsset].get(), ctx.tx[sfAsset2].get())) + if (auto const res = invalidAMMAssetPair(ctx.tx[sfAsset], ctx.tx[sfAsset2])) { JLOG(ctx.j.debug()) << "AMM Vote: invalid asset pair."; return res; diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 23e8529cfc9..de9a45655d4 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; @@ -100,18 +109,18 @@ AMMWithdraw::preflight(PreflightContext const& ctx) return temMALFORMED; } - auto const asset = ctx.tx[sfAsset].get(); - auto const asset2 = ctx.tx[sfAsset2].get(); + auto const asset = ctx.tx[sfAsset]; + auto const asset2 = ctx.tx[sfAsset2]; if (auto const res = invalidAMMAssetPair(asset, asset2)) { JLOG(ctx.j.debug()) << "AMM Withdraw: Invalid asset pair."; 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(); @@ -216,16 +226,19 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) << *amount; return tecAMM_BALANCE; } - if (auto const ter = - requireAuth(ctx.view, amount->issue(), accountID)) + if (auto const ter = requireAuth( + ctx.view, + amount->asset(), + accountID, + MPTAuthType::WeakAuth)) { 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 +246,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 +301,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 +361,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}; @@ -418,12 +441,7 @@ AMMWithdraw::applyGuts(Sandbox& sb) return {result, false}; auto const res = deleteAMMAccountIfEmpty( - sb, - ammSle, - newLPTokenBalance, - ctx_.tx[sfAsset].get(), - ctx_.tx[sfAsset2].get(), - j_); + sb, ammSle, newLPTokenBalance, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], j_); // LCOV_EXCL_START if (!res.second) return {res.first, false}; @@ -506,6 +524,7 @@ AMMWithdraw::withdraw( amountWithdraw.issue(), std::nullopt, freezeHandling, + AuthHandling::ahIGNORE_AUTH, // ??? journal); // LCOV_EXCL_START if (!expected) @@ -623,6 +642,61 @@ AMMWithdraw::withdraw( if (auto const err = sufficientReserve(amountWithdrawActual.issue())) return {err, STAmount{}, STAmount{}, STAmount{}}; + // Create MPToken if doesn't exist + // TODO make a library, AMMCreate, AMMAuthorize use almost identical code + auto createMPToken = [&](Asset const& asset) -> TER { + if (asset.holds()) + { + auto const& mptIssue = asset.get(); + auto const issuanceKey = keylet::mptIssuance(mptIssue.getMptID()); + auto const mptokenKey = keylet::mptoken(issuanceKey.key, account); + if (!view.exists(mptokenKey)) + { + if (auto err = requireAuth( + view, mptIssue, account, MPTAuthType::WeakAuth); + err != tesSUCCESS) + return err; + } + + auto const sleAcct = view.peek(keylet::account(account)); + if (!sleAcct) + return tefINTERNAL; + + std::uint32_t const uOwnerCount = + sleAcct->getFieldU32(sfOwnerCount); + XRPAmount const reserveCreate( + (uOwnerCount < 2) + ? XRPAmount(beast::zero) + : view.fees().accountReserve(uOwnerCount + 1)); + + if (priorBalance < reserveCreate) + return tecINSUFFICIENT_RESERVE; + + auto const ownerNode = view.dirInsert( + keylet::ownerDir(account), + mptokenKey, + describeOwnerDir(account)); + + if (!ownerNode) + return tecDIR_FULL; + + auto mptoken = std::make_shared(mptokenKey); + (*mptoken)[sfAccount] = account; + (*mptoken)[sfMPTokenIssuanceID] = mptIssue.getMptID(); + (*mptoken)[sfFlags] = 0; + (*mptoken)[sfOwnerNode] = *ownerNode; + view.insert(mptoken); + + // Update owner count. + adjustOwnerCount(view, sleAcct, 1, journal); + } + return tesSUCCESS; + }; + + if (auto const res = createMPToken(amountWithdrawActual.asset()); + res != tesSUCCESS) + return {res, STAmount{}, STAmount{}, STAmount{}}; + // Withdraw amountWithdraw auto res = accountSend( view, @@ -647,6 +721,10 @@ AMMWithdraw::withdraw( err != tesSUCCESS) return {err, STAmount{}, STAmount{}, STAmount{}}; + if (auto const res = createMPToken(amountWithdrawActual.asset()); + res != tesSUCCESS) + return {res, STAmount{}, STAmount{}, STAmount{}}; + res = accountSend( view, ammAccount, @@ -724,8 +802,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; @@ -791,9 +869,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 @@ -874,7 +952,7 @@ AMMWithdraw::equalWithdrawLimit( ammAccount, amountBalance, amount, - toSTAmount(amount2.issue(), amount2Withdraw), + toSTAmount(amount2.asset(), amount2Withdraw), lptAMMBalance, toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), tfee); @@ -890,7 +968,7 @@ AMMWithdraw::equalWithdrawLimit( ammSle, ammAccount, amountBalance, - toSTAmount(amount.issue(), amountWithdraw), + toSTAmount(amount.asset(), amountWithdraw), amount2, lptAMMBalance, toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), @@ -1014,7 +1092,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 52ca602b956..d0ba6303828 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 XRPL_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?! - XRPL_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(); - - XRPL_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. - XRPL_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! - XRPL_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); } } @@ -755,13 +368,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 +410,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 +444,7 @@ CreateOffer::flowCross( nonGatewayAmountIn = divideRound( result.actualAmountIn, gatewayXferRate, - takerAmount.in.issue(), + takerAmount.in.asset(), true); afterCross.in -= nonGatewayAmountIn; @@ -846,11 +466,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 +485,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 +503,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 +516,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 +617,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 +642,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 +679,10 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) } XRPL_ASSERT( - saTakerGets.issue() == place_offer.in.issue(), + saTakerGets.asset() == place_offer.in.asset(), "ripple::CreateOffer::applyGuts : taker gets issue match"); XRPL_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 +794,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 +805,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 def7914a49b..6f971286f8d 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 ea18306234b..06c22e013a8 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 1ed3bacbbd8..2df669d98d2 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/app/tx/detail/Taker.cpp b/src/xrpld/app/tx/detail/Taker.cpp index e98d65fd114..9f7b660c726 100644 --- a/src/xrpld/app/tx/detail/Taker.cpp +++ b/src/xrpld/app/tx/detail/Taker.cpp @@ -26,10 +26,11 @@ namespace ripple { static std::string format_amount(STAmount const& amount) { - std::string txt = amount.getText(); - txt += "/"; - txt += to_string(amount.issue().currency); - return txt; + if (amount.holds()) + return std::format( + "{}/{}", amount.getText(), to_string(amount.get().currency)); + return std::format( + "{}/{}", amount.getText(), to_string(amount.get())); } BasicTaker::BasicTaker( diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 74027752486..4d0fc7798b5 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 @@ -524,17 +542,44 @@ transferXRP( STAmount const& amount, beast::Journal j); +/* Check if MPToken exists: + * - StrongAuth - before checking lsfMPTRequireAuth is set + * - WeakAuth - after checking if lsfMPTRequireAuth is set + */ +enum class MPTAuthType : bool { StrongAuth = true, WeakAuth = false }; + /** Check if the account lacks required authorization. * Return tecNO_AUTH or tecNO_LINE if it does * and tesSUCCESS otherwise. */ [[nodiscard]] TER requireAuth(ReadView const& view, Issue const& issue, AccountID const& account); +/* If StrongAuth then return tecNO_AUTH if MPToken doesn't exist or + * lsfMPTRequireAuth is set and MPToken is not authorized. If WeakAuth then + * return tecNO_AUTH if lsfMPTRequireAuth is set and MPToken doesn't exist or is + * not authorized. + */ [[nodiscard]] TER requireAuth( ReadView const& view, MPTIssue const& mptIssue, - AccountID const& account); + AccountID const& account, + MPTAuthType authType = MPTAuthType::StrongAuth); +[[nodiscard]] TER inline requireAuth( + ReadView const& view, + Asset const& asset, + AccountID const& account, + MPTAuthType authType = MPTAuthType::StrongAuth) +{ + return std::visit( + [&](TIss const& issue_) { + if constexpr (std::is_same_v) + return requireAuth(view, issue_, account); + else + return requireAuth(view, issue_, account, authType); + }, + 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 ebf307f1535..054c6b0bd15 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -362,6 +362,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, @@ -382,6 +402,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. @@ -1835,7 +1871,8 @@ TER requireAuth( ReadView const& view, MPTIssue const& mptIssue, - AccountID const& account) + AccountID const& account, + MPTAuthType authType) { auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); auto const sleIssuance = view.read(mptID); @@ -1853,12 +1890,12 @@ requireAuth( auto const sleToken = view.read(mptokenID); // if account has no MPToken, fail - if (!sleToken) + if (!sleToken && authType == MPTAuthType::StrongAuth) return tecNO_AUTH; // mptoken must be authorized if issuance enabled requireAuth if (sleIssuance->getFieldU32(sfFlags) & lsfMPTRequireAuth && - !(sleToken->getFlags() & lsfMPTAuthorized)) + (!sleToken || (!(sleToken->getFlags() & lsfMPTAuthorized)))) return tecNO_AUTH; return tesSUCCESS; 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 f7c04e356c3..4e3412159a1 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,12 +256,12 @@ 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, - sendMax.issue().account, + sendMax.asset(), + sendMax.getIssuer(), amount, std::nullopt, app); @@ -270,7 +272,7 @@ checkPayment( STPath fullLiquidityPath; STPathSet paths; result = pf.getBestPaths( - 4, fullLiquidityPath, paths, sendMax.issue().account); + 4, fullLiquidityPath, paths, sendMax.getIssuer()); } } diff --git a/src/xrpld/rpc/handlers/AMMInfo.cpp b/src/xrpld/rpc/handlers/AMMInfo.cpp index 1990cdafd3e..384aad4616a 100644 --- a/src/xrpld/rpc/handlers/AMMInfo.cpp +++ b/src/xrpld/rpc/handlers/AMMInfo.cpp @@ -45,16 +45,16 @@ getAccount(Json::Value const& v, Json::Value& result) return std::optional(accountID); } -Expected -getIssue(Json::Value const& v, beast::Journal j) +Expected +getAsset(Json::Value const& v, beast::Journal j) { try { - return issueFromJson(v); + return assetFromJson(v); } catch (std::runtime_error const& ex) { - JLOG(j.debug()) << "getIssue " << ex.what(); + JLOG(j.debug()) << "getAsset " << ex.what(); } return Unexpected(rpcISSUE_MALFORMED); } @@ -84,16 +84,16 @@ doAMMInfo(RPC::JsonContext& context) struct ValuesFromContextParams { std::optional accountID; - Issue issue1; - Issue issue2; + Asset asset1; + Asset asset2; std::shared_ptr amm; }; auto getValuesFromContextParams = [&]() -> Expected { std::optional accountID; - std::optional issue1; - std::optional issue2; + std::optional asset1; + std::optional asset2; std::optional ammID; constexpr auto invalid = [](Json::Value const& params) -> bool { @@ -109,16 +109,16 @@ doAMMInfo(RPC::JsonContext& context) if (params.isMember(jss::asset)) { - if (auto const i = getIssue(params[jss::asset], context.j)) - issue1 = *i; + if (auto const i = getAsset(params[jss::asset], context.j)) + asset1 = *i; else return Unexpected(i.error()); } if (params.isMember(jss::asset2)) { - if (auto const i = getIssue(params[jss::asset2], context.j)) - issue2 = *i; + if (auto const i = getAsset(params[jss::asset2], context.j)) + asset2 = *i; else return Unexpected(i.error()); } @@ -153,22 +153,22 @@ doAMMInfo(RPC::JsonContext& context) "ripple::doAMMInfo : issue1 and issue2 do match"); auto const ammKeylet = [&]() { - if (issue1 && issue2) - return keylet::amm(*issue1, *issue2); + if (asset1 && asset2) + return keylet::amm(*asset1, *asset2); XRPL_ASSERT(ammID, "ripple::doAMMInfo::ammKeylet : ammID is set"); return keylet::amm(*ammID); }(); auto const amm = ledger->read(ammKeylet); if (!amm) return Unexpected(rpcACT_NOT_FOUND); - if (!issue1 && !issue2) + if (!asset1 && !asset2) { - issue1 = (*amm)[sfAsset].get(); - issue2 = (*amm)[sfAsset2].get(); + asset1 = (*amm)[sfAsset]; + asset2 = (*amm)[sfAsset2]; } return ValuesFromContextParams{ - accountID, *issue1, *issue2, std::move(amm)}; + accountID, *asset1, *asset2, std::move(amm)}; }; auto const r = getValuesFromContextParams(); @@ -178,7 +178,7 @@ doAMMInfo(RPC::JsonContext& context) return result; } - auto const& [accountID, issue1, issue2, amm] = *r; + auto const& [accountID, asset1, asset2, amm] = *r; auto const ammAccountID = amm->getAccountID(sfAccount); @@ -186,9 +186,10 @@ doAMMInfo(RPC::JsonContext& context) auto const [asset1Balance, asset2Balance] = ammPoolHolds( *ledger, ammAccountID, - issue1, - issue2, + asset1, + asset2, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, context.j); auto const lptAMMBalance = accountID ? ammLPHolds(*ledger, *amm, *accountID, context.j) @@ -253,11 +254,9 @@ doAMMInfo(RPC::JsonContext& context) } if (!isXRP(asset1Balance)) - ammResult[jss::asset_frozen] = - isFrozen(*ledger, ammAccountID, issue1.currency, issue1.account); + ammResult[jss::asset_frozen] = isFrozen(*ledger, ammAccountID, asset1); if (!isXRP(asset2Balance)) - ammResult[jss::asset2_frozen] = - isFrozen(*ledger, ammAccountID, issue2.currency, issue2.account); + ammResult[jss::asset2_frozen] = isFrozen(*ledger, ammAccountID, asset2); result[jss::amm] = std::move(ammResult); if (!result.isMember(jss::ledger_index) && diff --git a/src/xrpld/rpc/handlers/AccountLines.cpp b/src/xrpld/rpc/handlers/AccountLines.cpp index e2e6ce19ded..7222788d6cd 100644 --- a/src/xrpld/rpc/handlers/AccountLines.cpp +++ b/src/xrpld/rpc/handlers/AccountLines.cpp @@ -45,7 +45,7 @@ addLine(Json::Value& jsonLines, RPCTrustLine const& line) // Amount reported is negative if other account holds current // account's IOUs. jPeer[jss::balance] = saBalance.getText(); - jPeer[jss::currency] = to_string(saBalance.issue().currency); + jPeer[jss::currency] = to_string(saBalance.get().currency); jPeer[jss::limit] = saLimit.getText(); jPeer[jss::limit_peer] = saLimitPeer.getText(); jPeer[jss::quality_in] = line.getQualityIn().value; 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) {