From 96c8f3cca0b123c904ae73b897c6970d7e951717 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Wed, 25 Sep 2024 10:58:40 -0400 Subject: [PATCH] Next significant iteration of MPT DEX support (payment,offer,check not supported), not unit-tested --- include/xrpl/basics/MPTAmount.h | 6 + include/xrpl/protocol/AmountConversions.h | 84 +- include/xrpl/protocol/Asset.h | 58 +- include/xrpl/protocol/Book.h | 41 +- include/xrpl/protocol/Concepts.h | 72 ++ include/xrpl/protocol/MPTIssue.h | 27 + include/xrpl/protocol/PathAsset.h | 150 +++ include/xrpl/protocol/SField.h | 2 + include/xrpl/protocol/STPathSet.h | 143 ++- include/xrpl/protocol/TER.h | 4 +- src/libxrpl/protocol/Indexes.cpp | 41 +- src/libxrpl/protocol/MPTIssue.cpp | 5 + src/libxrpl/protocol/PathAsset.cpp | 85 ++ src/libxrpl/protocol/SField.cpp | 2 + src/libxrpl/protocol/STAmount.cpp | 4 +- src/libxrpl/protocol/STIssue.cpp | 2 +- src/libxrpl/protocol/STPathSet.cpp | 47 +- src/libxrpl/protocol/TER.cpp | 1 + src/libxrpl/protocol/TxFormats.cpp | 26 +- src/test/app/AMM_test.cpp | 2 +- src/test/app/PayStrand_test.cpp | 6 +- src/test/app/TheoreticalQuality_test.cpp | 9 +- src/test/jtx/PathSet.h | 2 +- src/test/jtx/amount.h | 8 + src/test/jtx/impl/AMM.cpp | 4 +- src/test/jtx/impl/paths.cpp | 4 +- src/xrpld/app/ledger/OrderBookDB.cpp | 54 +- src/xrpld/app/ledger/OrderBookDB.h | 10 +- src/xrpld/app/misc/AMMHelpers.h | 24 +- src/xrpld/app/misc/NetworkOPs.cpp | 16 +- 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 | 146 ++- src/xrpld/app/paths/PathRequest.cpp | 216 +++-- src/xrpld/app/paths/PathRequest.h | 23 +- src/xrpld/app/paths/PathRequests.cpp | 38 +- src/xrpld/app/paths/PathRequests.h | 10 +- src/xrpld/app/paths/Pathfinder.cpp | 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 | 205 ++-- src/xrpld/app/paths/detail/BookStep.cpp | 201 ++-- src/xrpld/app/paths/detail/DirectStep.cpp | 11 +- src/xrpld/app/paths/detail/FlowDebugInfo.h | 4 +- .../app/paths/detail/MPTEndpointStep.cpp | 884 ++++++++++++++++++ src/xrpld/app/paths/detail/PathfinderUtils.h | 4 +- src/xrpld/app/paths/detail/PaySteps.cpp | 304 ++++-- src/xrpld/app/paths/detail/Steps.h | 58 +- src/xrpld/app/paths/detail/StrandFlow.h | 15 +- .../app/paths/detail/XRPEndpointStep.cpp | 9 +- src/xrpld/app/tx/detail/AMMBid.cpp | 8 +- src/xrpld/app/tx/detail/AMMCreate.cpp | 139 ++- src/xrpld/app/tx/detail/AMMDelete.cpp | 9 +- src/xrpld/app/tx/detail/AMMDeposit.cpp | 57 +- src/xrpld/app/tx/detail/AMMVote.cpp | 8 +- src/xrpld/app/tx/detail/AMMWithdraw.cpp | 63 +- src/xrpld/app/tx/detail/CreateOffer.cpp | 6 +- src/xrpld/app/tx/detail/CreateOffer.h | 3 +- src/xrpld/app/tx/detail/MPTokenAuthorize.h | 2 +- src/xrpld/app/tx/detail/Offer.h | 94 +- src/xrpld/app/tx/detail/OfferStream.cpp | 139 +-- src/xrpld/app/tx/detail/OfferStream.h | 2 +- src/xrpld/ledger/View.h | 43 +- src/xrpld/ledger/detail/View.cpp | 32 +- src/xrpld/rpc/MPTokenIssuanceID.h | 2 +- src/xrpld/rpc/detail/MPTokenIssuanceID.cpp | 4 +- src/xrpld/rpc/detail/RPCHelpers.cpp | 147 +++ src/xrpld/rpc/detail/RPCHelpers.h | 46 + src/xrpld/rpc/detail/TransactionSign.cpp | 4 +- src/xrpld/rpc/handlers/BookOffers.cpp | 133 +-- src/xrpld/rpc/handlers/LedgerEntry.cpp | 4 +- src/xrpld/rpc/handlers/Subscribe.cpp | 77 +- src/xrpld/rpc/handlers/Unsubscribe.cpp | 76 +- 79 files changed, 3458 insertions(+), 1287 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 rename src/xrpld/app/paths/{RippleLineCache.cpp => AssetCache.cpp} (83%) rename src/xrpld/app/paths/{RippleLineCache.h => AssetCache.h} (93%) create mode 100644 src/xrpld/app/paths/detail/MPTEndpointStep.cpp diff --git a/include/xrpl/basics/MPTAmount.h b/include/xrpl/basics/MPTAmount.h index 5f8f3dbbe76..6bb8aa467a2 100644 --- a/include/xrpl/basics/MPTAmount.h +++ b/include/xrpl/basics/MPTAmount.h @@ -49,6 +49,7 @@ class MPTAmount : private boost::totally_ordered, public: MPTAmount() = default; constexpr MPTAmount(MPTAmount const& other) = default; + constexpr MPTAmount(beast::Zero); constexpr MPTAmount& operator=(MPTAmount const& other) = default; @@ -102,6 +103,11 @@ constexpr MPTAmount::MPTAmount(value_type value) : value_(value) { } +constexpr MPTAmount::MPTAmount(beast::Zero) +{ + *this = beast::zero; +} + constexpr MPTAmount& MPTAmount::operator=(beast::Zero) { value_ = 0; diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h index 270d009b916..3ec3073c06c 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -29,8 +30,9 @@ namespace ripple { inline STAmount -toSTAmount(IOUAmount const& iou, Issue const& iss) +toSTAmount(IOUAmount const& iou, Asset const& iss) { + assert(iss.holds()); 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,12 +53,25 @@ toSTAmount(XRPAmount const& xrp) } inline STAmount -toSTAmount(XRPAmount const& xrp, Issue const& iss) +toSTAmount(XRPAmount const& xrp, Asset const& iss) { - assert(isXRP(iss.account) && isXRP(iss.currency)); + assert(isXRP(iss)); return toSTAmount(xrp); } +inline STAmount +toSTAmount(MPTAmount const& mpt) +{ + return STAmount(mpt, noMPT()); +} + +inline STAmount +toSTAmount(MPTAmount const& mpt, Asset const& iss) +{ + assert(iss.holds()); + return STAmount(mpt, iss.get()); +} + template T toAmount(STAmount const& amt) = delete; @@ -94,6 +109,19 @@ toAmount(STAmount const& amt) return XRPAmount(sMant); } +template <> +inline MPTAmount +toAmount(STAmount const& amt) +{ + assert(amt.mantissa() < std::numeric_limits::max()); + assert(amt.holds()); + 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; @@ -116,10 +144,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()) { @@ -131,6 +170,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)) @@ -146,18 +187,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 { @@ -168,7 +222,7 @@ toMaxAmount(Issue const& issue) inline STAmount toSTAmount( - Issue const& issue, + Asset const& issue, Number const& n, Number::rounding_mode mode = Number::getround()) { @@ -176,15 +230,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; @@ -200,6 +256,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 1252489952f..b8e28d0ae28 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -21,21 +21,12 @@ #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_same_v || - std::is_convertible_v || std::is_convertible_v; - class Asset { private: @@ -51,11 +42,9 @@ class Asset Asset(MPTID const& mpt); - explicit - operator Issue() const; + explicit operator Issue() const; - explicit - operator MPTIssue() const; + explicit operator MPTIssue() const; AccountID const& getIssuer() const; @@ -185,12 +174,45 @@ isXRP(Asset const& asset) return asset.holds() && isXRP(asset.get()); } -template -decltype(auto) -invokeForAsset(Asset const& asset, Cb&& f) +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( - [&](auto const& issue) { return f(issue); }, asset.value()); + [&](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.currency, issue.account); + else + hash_append(h, issue.getMptID()); + }, + r.value()); } std::string diff --git a/include/xrpl/protocol/Book.h b/include/xrpl/protocol/Book.h index e4c28b07e92..117ef0743be 100644 --- a/include/xrpl/protocol/Book.h +++ b/include/xrpl/protocol/Book.h @@ -141,22 +141,27 @@ struct hash 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_.currency)); - if (!isXRP(issue_.currency)) - boost::hash_combine( - result, issuer_hash_type::member(issue_.account)); - return result; - } - else if constexpr (std::is_same_v) - { - value_type result(mpt_hash_type::member(issue_.getMptID())); - return result; - } - }, issue.value()); + 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()); } }; @@ -166,7 +171,7 @@ template <> struct hash { private: - using hasher = std::hash; + using hasher = std::hash; hasher m_hasher; @@ -207,8 +212,6 @@ struct hash : std::hash explicit hash() = default; using Base = std::hash; - // VFALCO NOTE broken in vs2012 - // using Base::Base; // inherit ctors }; template <> diff --git a/include/xrpl/protocol/Concepts.h b/include/xrpl/protocol/Concepts.h new file mode 100644 index 00000000000..f4a918c9d9c --- /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-foormat 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-foormat on + +} // namespace ripple + +#endif // RIPPLE_PROTOCOL_CONCEPTS_H_INCLUDED diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index 5892e2b91c8..545f1fd2614 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -35,6 +35,8 @@ class MPTIssue MPTIssue(MPTID const& id); + MPTIssue(AccountID const& account, std::uint32_t sequence); + AccountID const& getIssuer() const; @@ -66,6 +68,21 @@ 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{noAccount(), 0}; + return mpt.getMptID(); +} + Json::Value to_json(MPTIssue const& issue); @@ -74,4 +91,14 @@ to_string(MPTIssue const& mpt); } // namespace ripple +namespace std { + +template <> +struct hash : ripple::MPTID::hasher +{ + explicit hash() = default; +}; + +} // namespace std + #endif // RIPPLE_PROTOCOL_MPTISSUE_H_INCLUDED diff --git a/include/xrpl/protocol/PathAsset.h b/include/xrpl/protocol/PathAsset.h new file mode 100644 index 00000000000..9a148ad2b17 --- /dev/null +++ b/include/xrpl/protocol/PathAsset.h @@ -0,0 +1,150 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_PATHASSET_H_INCLUDED +#define RIPPLE_APP_PATHASSET_H_INCLUDED + +#include +#include + +namespace ripple { + +class PathAsset +{ +private: + std::variant easset_; + +public: + PathAsset() = default; + PathAsset(Asset const& asset); + PathAsset(Currency const& currency) : easset_(currency) + { + } + PathAsset(MPTID const& mpt) : easset_(mpt) + { + } + + template + constexpr bool + holds() const; + + constexpr bool + isXRP() const; + + template + T const& + get() const; + + constexpr std::variant const& + value() const; + + static PathAsset + toPathAsset(Asset const& asset); + + static std::optional + toPathAsset(std::optional const& asset); + + friend constexpr bool + operator==(PathAsset const& lhs, PathAsset const& rhs); +}; + +inline PathAsset::PathAsset(Asset const& asset) +{ + std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + easset_ = issue.currency; + else + easset_ = issue.getMptID(); + }, + asset.value()); +} + +template +constexpr bool +PathAsset::holds() const +{ + return std::holds_alternative(easset_); +} + +template +T const& +PathAsset::get() const +{ + if (!holds()) + Throw("PathAsset doesn't hold requested asset."); + return std::get(easset_); +} + +constexpr std::variant const& +PathAsset::value() const +{ + return easset_; +} + +constexpr bool +PathAsset::isXRP() const +{ + return holds() && get() == xrpCurrency(); +} + +constexpr bool +operator==(PathAsset const& lhs, PathAsset const& rhs) +{ + return std::visit( + []( + TLhs const& lhs_, TRhs const& rhs_) { + if constexpr (std::is_same_v) + return lhs_ == rhs_; + else + return false; + }, + lhs.value(), + rhs.value()); +} + +template +void +hash_append(Hasher& h, PathAsset const& pathAsset) +{ + std::visit( + [&](T const& e) { hash_append(h, e); }, pathAsset.value()); +} + +inline bool +isXRP(PathAsset const& asset) +{ + return asset.isXRP(); +} + +std::string +to_string(PathAsset const& asset); + +std::ostream& +operator<<(std::ostream& os, PathAsset const& x); + +bool +equalAssets(PathAsset const& asset1, Asset const& asset2); + +bool +equalAssets(Asset const& asset1, PathAsset const& asset2); + +} // namespace ripple + +#endif // RIPPLE_APP_PATHASSET_H_INCLUDED diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index b7391d2971b..c385bdca957 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -484,6 +484,8 @@ extern SF_UINT160 const sfTakerGetsIssuer; // 192-bit (common) extern SF_UINT192 const sfMPTokenIssuanceID; +extern SF_UINT192 const sfTakerGetsMPT; +extern SF_UINT192 const sfTakerPaysMPT; // 256-bit (common) extern SF_UINT256 const sfLedgerHash; diff --git a/include/xrpl/protocol/STPathSet.h b/include/xrpl/protocol/STPathSet.h index 473086368fb..ffa452264b3 100644 --- a/include/xrpl/protocol/STPathSet.h +++ b/include/xrpl/protocol/STPathSet.h @@ -22,6 +22,8 @@ #include #include +#include +#include #include #include #include @@ -35,21 +37,26 @@ class STPathElement final : public CountedObject { unsigned int mType; AccountID mAccountID; - Currency mCurrencyID; + PathAsset mAssetID; AccountID mIssuerID; bool is_offer_; std::size_t hash_value_; public: + struct PathAssetTag + { + }; enum Type { typeNone = 0x00, typeAccount = 0x01, // Rippling through an account (vs taking an offer). typeCurrency = 0x10, // Currency follows. typeIssuer = 0x20, // Issuer follows. + typeMPT = 0x40, // MPT follows. typeBoundary = 0xFF, // Boundary between alternate paths. - typeAll = typeAccount | typeCurrency | typeIssuer, + typeAsset = typeCurrency | typeMPT, + typeAll = typeAccount | typeCurrency | typeIssuer | typeMPT, // Combination of all types. }; @@ -60,19 +67,37 @@ class STPathElement final : public CountedObject STPathElement( std::optional const& account, - std::optional const& currency, + std::optional const& asset, std::optional const& issuer); + STPathElement( + std::optional const& account, + std::optional const& asset, + std::optional const& issuer, + PathAssetTag); + STPathElement( AccountID const& account, - Currency const& currency, + Asset const& asset, AccountID const& issuer, bool forceCurrency = false); + STPathElement( + AccountID const& account, + PathAsset const& asset, + AccountID const& issuer, + bool forceCurrency = false); + + STPathElement( + unsigned int uType, + AccountID const& account, + Asset const& asset, + AccountID const& issuer); + STPathElement( unsigned int uType, AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer); auto @@ -90,6 +115,12 @@ class STPathElement final : public CountedObject bool hasCurrency() const; + bool + hasMPT() const; + + bool + hasAsset() const; + bool isNone() const; @@ -98,9 +129,15 @@ class STPathElement final : public CountedObject AccountID const& getAccountID() const; + PathAsset const& + getPathAsset() const; + Currency const& getCurrency() const; + MPTID const& + getMPTID() const; + AccountID const& getIssuerID() const; @@ -140,7 +177,13 @@ class STPath final : public CountedObject bool hasSeen( AccountID const& account, - Currency const& currency, + Asset const& asset, + AccountID const& issuer) const; + + bool + hasSeen( + AccountID const& account, + PathAsset const& asset, AccountID const& issuer) const; Json::Value getJson(JsonOptions) const; @@ -244,8 +287,21 @@ inline STPathElement::STPathElement() : mType(typeNone), is_offer_(true) inline STPathElement::STPathElement( std::optional const& account, - std::optional const& currency, + std::optional const& asset, std::optional const& issuer) + : STPathElement( + account, + PathAsset::toPathAsset(asset), + issuer, + PathAssetTag{}) +{ +} + +inline STPathElement::STPathElement( + std::optional const& account, + std::optional const& asset, + std::optional const& issuer, + PathAssetTag) : mType(typeNone) { if (!account) @@ -260,10 +316,10 @@ inline STPathElement::STPathElement( assert(mAccountID != noAccount()); } - if (currency) + if (asset) { - mCurrencyID = *currency; - mType |= typeCurrency; + mAssetID = *asset; + mType |= mAssetID.holds() ? typeCurrency : typeMPT; } if (issuer) @@ -275,41 +331,70 @@ inline STPathElement::STPathElement( hash_value_ = get_hash(*this); } +inline STPathElement::STPathElement( + AccountID const& account, + Asset const& asset, + AccountID const& issuer, + bool forceCurrency) + : STPathElement( + account, + PathAsset::toPathAsset(asset), + issuer, + forceCurrency) +{ +} inline STPathElement::STPathElement( AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer, bool forceCurrency) : mType(typeNone) , mAccountID(account) - , mCurrencyID(currency) + , mAssetID(asset) , mIssuerID(issuer) , is_offer_(isXRP(mAccountID)) { if (!is_offer_) mType |= typeAccount; - if (forceCurrency || !isXRP(currency)) + if (!asset.holds() && + (forceCurrency || !isXRP(mAssetID.get()))) mType |= typeCurrency; if (!isXRP(issuer)) mType |= typeIssuer; + if (asset.holds()) + mType |= typeMPT; + hash_value_ = get_hash(*this); } inline STPathElement::STPathElement( unsigned int uType, AccountID const& account, - Currency const& currency, + Asset const& asset, + AccountID const& issuer) + : STPathElement(uType, account, PathAsset::toPathAsset(asset), issuer) +{ +} + +inline STPathElement::STPathElement( + unsigned int uType, + AccountID const& account, + PathAsset const& asset, AccountID const& issuer) : mType(uType) , mAccountID(account) - , mCurrencyID(currency) + , mAssetID(asset) , mIssuerID(issuer) , is_offer_(isXRP(mAccountID)) { + if (!asset.holds()) + mType = mType & (~Type::typeMPT); + else if (mAssetID.holds() && isXRP(mAssetID.get())) + mType = mType & (~Type::typeCurrency); hash_value_ = get_hash(*this); } @@ -343,6 +428,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 { @@ -357,10 +454,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& @@ -374,7 +483,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 d6225cbdc97..f53f17792cf 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -225,6 +225,7 @@ enum TERcodes : TERUnderlyingType { terQUEUED, // Transaction is being held in TxQ until fee drops terPRE_TICKET, // Ticket is not yet in ledger but might be on its way terNO_AMM, // AMM doesn't exist for the asset pair + terNO_MPT, // MPT doesn't exist for the asset pair }; //------------------------------------------------------------------------------ @@ -441,8 +442,7 @@ class TERSubset } // Conversion to bool. - explicit - operator bool() const + explicit operator bool() const { return code_ != tesSUCCESS; } diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index e98f8d61e1a..f17e99f1feb 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -96,12 +96,37 @@ getBookBase(Book const& book) { assert(isConsistent(book)); - 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); @@ -430,8 +455,8 @@ amm(Asset const& issue1, Asset const& issue2) noexcept issue1_.getMptID(), issue2_.getMptID())); }, - issue1.value(), - issue2.value()); + minA.value(), + maxA.value()); } Keylet diff --git a/src/libxrpl/protocol/MPTIssue.cpp b/src/libxrpl/protocol/MPTIssue.cpp index f46e6d9b2d8..addd4cd3afe 100644 --- a/src/libxrpl/protocol/MPTIssue.cpp +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -27,6 +27,11 @@ MPTIssue::MPTIssue(MPTID const& id) : mptID_(id) { } +MPTIssue::MPTIssue(AccountID const& account, std::uint32_t sequence) + : MPTIssue(ripple::getMptID(account, sequence)) +{ +} + AccountID const& MPTIssue::getIssuer() const { diff --git a/src/libxrpl/protocol/PathAsset.cpp b/src/libxrpl/protocol/PathAsset.cpp new file mode 100644 index 00000000000..47966a1a3e5 --- /dev/null +++ b/src/libxrpl/protocol/PathAsset.cpp @@ -0,0 +1,85 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +namespace ripple { + +PathAsset +PathAsset::toPathAsset(const Asset& asset) +{ + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + return PathAsset{issue.currency}; + else + return PathAsset{issue.getMptID()}; + }, + asset.value()); +} + +std::optional +PathAsset::toPathAsset(std::optional const& asset) +{ + if (asset) + return toPathAsset(*asset); + return std::nullopt; +} + +std::string +to_string(PathAsset const& asset) +{ + return std::visit( + [&](auto const& issue) { return to_string(issue); }, asset.value()); +} + +std::ostream& +operator<<(std::ostream& os, PathAsset const& x) +{ + os << to_string(x); + return os; +} + +bool +equalAssets(PathAsset const& asset1, Asset const& asset2) +{ + return std::visit( + [&]( + TPa const& element, TIss const& issue) { + if constexpr ( + std::is_same_v && std::is_same_v) + return element == issue.currency; + else if constexpr ( + std::is_same_v && std::is_same_v) + return element == issue.getMptID(); + else + return false; + }, + asset1.value(), + asset2.value()); +} + +bool +equalAssets(Asset const& asset1, PathAsset const& asset2) +{ + return equalAssets(asset2, asset1); +} + +} // namespace ripple diff --git a/src/libxrpl/protocol/SField.cpp b/src/libxrpl/protocol/SField.cpp index dc4bfb69a21..f675e1517d5 100644 --- a/src/libxrpl/protocol/SField.cpp +++ b/src/libxrpl/protocol/SField.cpp @@ -210,6 +210,8 @@ CONSTRUCT_TYPED_SFIELD(sfTakerGetsIssuer, "TakerGetsIssuer", UINT160, // 192-bit (common) CONSTRUCT_TYPED_SFIELD(sfMPTokenIssuanceID, "MPTokenIssuanceID", UINT192, 1); +CONSTRUCT_TYPED_SFIELD(sfTakerGetsMPT, "TakerGetsMPT", UINT192, 2); +CONSTRUCT_TYPED_SFIELD(sfTakerPaysMPT, "TakerPaysMPT", UINT192, 3); // 256-bit (common) CONSTRUCT_TYPED_SFIELD(sfLedgerHash, "LedgerHash", UINT256, 1); diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 735e3853c9d..1c8fc337a8c 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -119,7 +119,7 @@ STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name) mOffset = 0; mIsNegative = (value & cPositive) == 0; mValue = (value << 8) | sit.get8(); - mAsset = sit.get192(); + mAsset = static_cast(sit.get192()); return; } // else is XRP @@ -958,7 +958,7 @@ amountFromJson(SField const& name, Json::Value const& v) if (isMPT) { // sequence (32 bits) + account (160 bits) - uint192 u; + MPTID u; if (!u.parseHex(currencyOrMPTID.asString())) Throw("invalid MPTokenIssuanceID"); asset = u; diff --git a/src/libxrpl/protocol/STIssue.cpp b/src/libxrpl/protocol/STIssue.cpp index 03aaa54fb93..d9e446faf6d 100644 --- a/src/libxrpl/protocol/STIssue.cpp +++ b/src/libxrpl/protocol/STIssue.cpp @@ -57,7 +57,7 @@ STIssue::STIssue(SerialIter& sit, SField const& name) : STBase{name} // MPT if (noAccount() == account) { - uint192 mptID; + MPTID mptID; std::uint32_t sequence = sit.get32(); memcpy(mptID.data(), &sequence, sizeof(sequence)); memcpy( diff --git a/src/libxrpl/protocol/STPathSet.cpp b/src/libxrpl/protocol/STPathSet.cpp index b658465272e..5a093e207bd 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,26 @@ STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name) auto hasAccount = iType & STPathElement::typeAccount; auto hasCurrency = iType & STPathElement::typeCurrency; auto hasIssuer = iType & STPathElement::typeIssuer; + auto hasMPT = iType & STPathElement::typeMPT; AccountID account; - Currency currency; + PathAsset asset{}; AccountID issuer; if (hasAccount) account = sit.get160(); + assert(!(hasCurrency && hasMPT)); 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 +165,21 @@ STPathSet::isDefault() const bool STPath::hasSeen( AccountID const& account, - Currency const& currency, + Asset const& asset, + AccountID const& issuer) const +{ + return hasSeen(account, PathAsset::toPathAsset(asset), issuer); +} + +bool +STPath::hasSeen( + AccountID const& account, + PathAsset const& asset, AccountID const& issuer) const { for (auto& p : mPath) { - if (p.getAccountID() == account && p.getCurrency() == currency && + if (p.getAccountID() == account && p.getPathAsset() == asset && p.getIssuerID() == issuer) return true; } @@ -177,9 +201,15 @@ Json::Value STPath::getJson(JsonOptions) const if (iType & STPathElement::typeAccount) elem[jss::account] = to_string(it.getAccountID()); + assert( + !(iType & STPathElement::typeCurrency && + iType & STPathElement::typeMPT)); 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()); @@ -226,6 +256,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 6ebac882cae..9a612d63ca1 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -226,6 +226,7 @@ transResults() MAKE_ERROR(terQUEUED, "Held until escalated fee drops."), MAKE_ERROR(terPRE_TICKET, "Ticket is not yet in ledger."), MAKE_ERROR(terNO_AMM, "AMM doesn't exist for the asset pair."), + MAKE_ERROR(terNO_MPT, "MPT doesn't exist for the asset pair."), MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."), }; diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index 92e8ff3b690..4b83877a249 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -75,8 +75,8 @@ TxFormats::TxFormats() add(jss::OfferCreate, ttOFFER_CREATE, { - {sfTakerPays, soeREQUIRED}, - {sfTakerGets, soeREQUIRED}, + {sfTakerPays, soeREQUIRED, soeMPTSupported}, + {sfTakerGets, soeREQUIRED, soeMPTSupported}, {sfExpiration, soeOPTIONAL}, {sfOfferSequence, soeOPTIONAL}, }, @@ -85,8 +85,8 @@ TxFormats::TxFormats() add(jss::AMMCreate, ttAMM_CREATE, { - {sfAmount, soeREQUIRED}, - {sfAmount2, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, + {sfAmount2, soeREQUIRED, soeMPTSupported}, {sfTradingFee, soeREQUIRED}, }, commonFields); @@ -96,8 +96,8 @@ TxFormats::TxFormats() { {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, - {sfAmount2, soeOPTIONAL}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, + {sfAmount2, soeOPTIONAL, soeMPTSupported}, {sfEPrice, soeOPTIONAL}, {sfLPTokenOut, soeOPTIONAL}, {sfTradingFee, soeOPTIONAL}, @@ -109,8 +109,8 @@ TxFormats::TxFormats() { {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, - {sfAmount2, soeOPTIONAL}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, + {sfAmount2, soeOPTIONAL, soeMPTSupported}, {sfEPrice, soeOPTIONAL}, {sfLPTokenIn, soeOPTIONAL}, }, @@ -163,11 +163,11 @@ TxFormats::TxFormats() { {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, - {sfSendMax, soeOPTIONAL}, + {sfSendMax, soeOPTIONAL, soeMPTSupported}, {sfPaths, soeDEFAULT}, {sfInvoiceID, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, - {sfDeliverMin, soeOPTIONAL}, + {sfDeliverMin, soeOPTIONAL, soeMPTSupported}, }, commonFields); @@ -287,7 +287,7 @@ TxFormats::TxFormats() ttCHECK_CREATE, { {sfDestination, soeREQUIRED}, - {sfSendMax, soeREQUIRED}, + {sfSendMax, soeREQUIRED, soeMPTSupported}, {sfExpiration, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, {sfInvoiceID, soeOPTIONAL}, @@ -298,8 +298,8 @@ TxFormats::TxFormats() ttCHECK_CASH, { {sfCheckID, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, - {sfDeliverMin, soeOPTIONAL}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, + {sfDeliverMin, soeOPTIONAL, soeMPTSupported}, }, commonFields); diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index ceddc019504..3c2c181a6d7 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -6161,7 +6161,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/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp index f00a7361292..50ad8d58925 100644 --- a/src/test/app/PayStrand_test.cpp +++ b/src/test/app/PayStrand_test.cpp @@ -1006,7 +1006,11 @@ struct PayStrand_test : public beast::unit_test::suite // alice -> USD/XRP -> bob STPath path; - path.emplace_back(std::nullopt, xrpCurrency(), std::nullopt); + path.emplace_back( + std::nullopt, + xrpCurrency(), + std::nullopt, + STPathElement::PathAssetTag{}); auto [ter, strand] = toStrand( *env.current(), diff --git a/src/test/app/TheoreticalQuality_test.cpp b/src/test/app/TheoreticalQuality_test.cpp index 917d23377bf..eaa6eb1a67f 100644 --- a/src/test/app/TheoreticalQuality_test.cpp +++ b/src/test/app/TheoreticalQuality_test.cpp @@ -71,7 +71,8 @@ struct RippleCalcTestParams *parseBase58( pe[jss::account].asString()), std::nullopt, - std::nullopt); + std::nullopt, + STPathElement::PathAssetTag{}); } else if ( pe.isMember(jss::currency) && pe.isMember(jss::issuer)) @@ -85,7 +86,11 @@ struct RippleCalcTestParams else assert(isXRP(*parseBase58( pe[jss::issuer].asString()))); - p.emplace_back(std::nullopt, currency, issuer); + p.emplace_back( + std::nullopt, + currency, + issuer, + STPathElement::PathAssetTag{}); } else { diff --git a/src/test/jtx/PathSet.h b/src/test/jtx/PathSet.h index 0f4c4ddd3dd..9c54ffdcffe 100644 --- a/src/test/jtx/PathSet.h +++ b/src/test/jtx/PathSet.h @@ -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/amount.h b/src/test/jtx/amount.h index 122e21726f5..44bfc0f25a6 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -176,6 +176,10 @@ struct XRP_t { return xrpIssue(); } + operator Asset() const + { + return xrpIssue(); + } /** Returns an amount of XRP as PrettyAmount, which is trivially convertable to STAmount @@ -388,6 +392,10 @@ class MPT { return mpt(); } + operator ripple::Asset() const + { + return mpt(); + } template requires(sizeof(T) >= sizeof(int) && std::is_arithmetic_v) PrettyAmount diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 6f7bfb10b34..0c702ecfb8f 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -42,8 +42,8 @@ static IOUAmount initialTokens(STAmount const& asset1, STAmount const& asset2) { auto const product = number(asset1) * number(asset2); - return (IOUAmount)(product.mantissa() >= 0 ? root2(product) - : root2(-product)); + return (IOUAmount)( + product.mantissa() >= 0 ? root2(product) : root2(-product)); } AMM::AMM( diff --git a/src/test/jtx/impl/paths.cpp b/src/test/jtx/impl/paths.cpp index 393e36e9d61..7ad135553e4 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, diff --git a/src/xrpld/app/ledger/OrderBookDB.cpp b/src/xrpld/app/ledger/OrderBookDB.cpp index 802d1507f31..42acbf5f0ad 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 @@ -274,8 +292,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 8bf5a5c5f12..fe61f848799 100644 --- a/src/xrpld/app/misc/AMMHelpers.h +++ b/src/xrpld/app/misc/AMMHelpers.h @@ -226,7 +226,7 @@ getAMMOfferStartWithTakerGets( // Round downward to minimize the offer and to maximize the quality. // This has the most impact when takerGets is XRP. auto const takerGets = toAmount( - getIssue(pool.out), nTakerGetsProposed, Number::downward); + getAsset(pool.out), nTakerGetsProposed, Number::downward); return TAmounts{ swapAssetOut(pool, takerGets, tfee), takerGets}; }; @@ -297,7 +297,7 @@ getAMMOfferStartWithTakerPays( // Round downward to minimize the offer and to maximize the quality. // This has the most impact when takerPays is XRP. auto const takerPays = toAmount( - getIssue(pool.in), nTakerPaysProposed, Number::downward); + getAsset(pool.in), nTakerPaysProposed, Number::downward); return TAmounts{ takerPays, swapAssetIn(pool, takerPays, tfee)}; }; @@ -374,7 +374,7 @@ changeSpotPriceQuality( return std::nullopt; } auto const takerPays = - toAmount(getIssue(pool.in), nTakerPays, Number::upward); + toAmount(getAsset(pool.in), nTakerPays, Number::upward); // should not fail if (auto const amounts = TAmounts{ @@ -409,7 +409,7 @@ changeSpotPriceQuality( // Generate the offer starting with XRP side. Return seated offer amounts // if the offer can be generated, otherwise nullopt. auto const amounts = [&]() { - if (isXRP(getIssue(pool.out))) + if (isXRP(getAsset(pool.out))) return getAMMOfferStartWithTakerGets(pool, quality, tfee); return getAMMOfferStartWithTakerPays(pool, quality, tfee); }(); @@ -501,7 +501,7 @@ swapAssetIn( auto const denom = pool.in + assetIn * (1 - fee); if (denom.signum() <= 0) - return toAmount(getIssue(pool.out), 0); + return toAmount(getAsset(pool.out), 0); Number::setround(Number::upward); auto const ratio = numerator / denom; @@ -510,14 +510,14 @@ swapAssetIn( auto const swapOut = pool.out - ratio; if (swapOut.signum() < 0) - return toAmount(getIssue(pool.out), 0); + return toAmount(getAsset(pool.out), 0); - return toAmount(getIssue(pool.out), swapOut, Number::downward); + return toAmount(getAsset(pool.out), swapOut, Number::downward); } else { return toAmount( - getIssue(pool.out), + getAsset(pool.out), pool.out - (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)), Number::downward); @@ -569,7 +569,7 @@ swapAssetOut( auto const denom = pool.out - assetOut; if (denom.signum() <= 0) { - return toMaxAmount(getIssue(pool.in)); + return toMaxAmount(getAsset(pool.in)); } Number::setround(Number::upward); @@ -583,14 +583,14 @@ swapAssetOut( Number::setround(Number::upward); auto const swapIn = numerator2 / feeMult; if (swapIn.signum() < 0) - return toAmount(getIssue(pool.in), 0); + return toAmount(getAsset(pool.in), 0); - return toAmount(getIssue(pool.in), swapIn, Number::upward); + return toAmount(getAsset(pool.in), swapIn, Number::upward); } else { return toAmount( - getIssue(pool.in), + getAsset(pool.in), ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) / feeMult(tfee), Number::upward); diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 7868807c52a..9648671cd28 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -4104,8 +4104,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; @@ -4115,7 +4115,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) @@ -4163,7 +4163,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. @@ -4192,9 +4192,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) @@ -4214,9 +4214,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. 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 83% rename from src/xrpld/app/paths/RippleLineCache.cpp rename to src/xrpld/app/paths/AssetCache.cpp index 0ff967c0821..5211fb021f4 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); @@ -125,4 +123,32 @@ RippleLineCache::getRippleLines( return it->second; } +std::shared_ptr> const& +AssetCache::getMPTs(const ripple::AccountID& account) +{ + std::lock_guard sl(mLock); + + if (auto it = mpts_.find(account); it != mpts_.end()) + return it->second; + + std::vector mpts; + // Get issued/authorized tokens + forEachItem(*ledger_, account, [&](std::shared_ptr const& sle) { + if (sle->getType() == ltMPTOKEN_ISSUANCE) + mpts.push_back(getMptID(account, sle->getFieldU32(sfSequence))); + 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 c21d40c33b5..e0b3e41f062 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 sendMax->asset(); + if (deliver.holds()) return Issue(deliver.issue().currency, src); + if (deliver.holds()) + return deliver.asset(); return xrpIssue(); }(); - 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,45 @@ 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( - sb, - strands, - asDeliver.iou, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); - } - - if (!srcIsXRP && dstIsXRP) - { - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( + using Var = + std::variant; + auto getTypedAmt = [&](Asset const& iss) -> Var { + static auto xrp = XRPAmount{}; + static auto cft = MPTAmount{}; + static auto iou = IOUAmount{}; + if (isXRP(iss)) + return &xrp; + if (iss.holds()) + return &cft; + return &iou; + }; + + // The src account may send either xrp,iou,mpt. The dst account may receive + // either xrp,iou,mpt. Since XRP, IOU, and MPT amounts are represented by + // different types, use templates to tell `flow` about the amount types. + path::RippleCalc::Output result; + std::visit( + [&, &strands_ = strands]( + TIn const*&&, TOut const*&&) { + result = finishFlow( sb, - strands, - asDeliver.xrp, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); - } - - assert(!srcIsXRP && !dstIsXRP); - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( - sb, - strands, - asDeliver.iou, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); + srcAsset, + dstAsset, + flow( + sb, + strands_, + get(deliver), + partialPayment, + offerCrossing, + limitQuality, + sendMax, + j, + ammContext, + flowDebugInfo)); + }, + getTypedAmt(srcAsset), + getTypedAmt(dstAsset)); + return result; } } // namespace ripple diff --git a/src/xrpld/app/paths/PathRequest.cpp b/src/xrpld/app/paths/PathRequest.cpp index bb6a104bca2..063f46fa238 100644 --- a/src/xrpld/app/paths/PathRequest.cpp +++ b/src/xrpld/app/paths/PathRequest.cpp @@ -169,7 +169,7 @@ PathRequest::updateComplete() } bool -PathRequest::isValid(std::shared_ptr const& crCache) +PathRequest::isValid(std::shared_ptr const& crCache) { if (!raSrcAccount || !raDstAccount) return false; @@ -222,6 +222,13 @@ PathRequest::isValid(std::shared_ptr const& crCache) for (auto const& currency : usDestCurrID) jvDestCur.append(to_string(currency)); + + if (auto mpts = crCache->getMPTs(*raDstAccount)) + { + for (auto const& mpt : *mpts) + jvDestCur.append(to_string(mpt)); + } + jvStatus[jss::destination_tag] = (sleDest->getFlags() & lsfRequireDestTag); } @@ -242,7 +249,7 @@ PathRequest::isValid(std::shared_ptr const& crCache) */ std::pair PathRequest::doCreate( - std::shared_ptr const& cache, + std::shared_ptr const& cache, Json::Value const& value) { bool valid = false; @@ -313,11 +320,9 @@ PathRequest::parseJson(Json::Value const& jvParams) return PFR_PJ_INVALID; } - convert_all_ = saDstAmount == STAmount(saDstAmount.issue(), 1u, 0, true); + convert_all_ = saDstAmount == STAmount(saDstAmount.asset(), 1u, 0, true); - if ((saDstAmount.getCurrency().isZero() && - saDstAmount.getIssuer().isNonZero()) || - (saDstAmount.getCurrency() == badCurrency()) || + if (!validAsset(saDstAmount.asset()) || (!convert_all_ && saDstAmount <= beast::zero)) { jvStatus = rpcError(rpcDST_AMT_MALFORMED); @@ -335,11 +340,9 @@ PathRequest::parseJson(Json::Value const& jvParams) saSendMax.emplace(); if (!amountFromJsonNoThrow(*saSendMax, jvParams[jss::send_max]) || - (saSendMax->getCurrency().isZero() && - saSendMax->getIssuer().isNonZero()) || - (saSendMax->getCurrency() == badCurrency()) || + !validAsset(saSendMax->asset()) || (*saSendMax <= beast::zero && - *saSendMax != STAmount(saSendMax->issue(), 1u, 0, true))) + *saSendMax != STAmount(saSendMax->asset(), 1u, 0, true))) { jvStatus = rpcError(rpcSENDMAX_MALFORMED); return PFR_PJ_INVALID; @@ -356,47 +359,72 @@ PathRequest::parseJson(Json::Value const& jvParams) return PFR_PJ_INVALID; } - sciSourceCurrencies.clear(); + sciSourceAssets.clear(); for (auto const& c : jvSrcCurrencies) { - // Mandatory currency - Currency srcCurrencyID; - if (!c.isObject() || !c.isMember(jss::currency) || - !c[jss::currency].isString() || - !to_currency(srcCurrencyID, c[jss::currency].asString())) + // Mandatory currency or MPT + if (!validJSONAsset(c) || !c.isObject()) { jvStatus = rpcError(rpcSRC_CUR_MALFORMED); return PFR_PJ_INVALID; } + PathAsset srcPathAsset; + if (c.isMember(jss::currency)) + { + Currency currency; + if (!c[jss::currency].isString() || + !to_currency(currency, c[jss::currency].asString())) + { + jvStatus = rpcError(rpcSRC_CUR_MALFORMED); + return PFR_PJ_INVALID; + } + srcPathAsset = currency; + } + else + { + uint192 u; + if (!c[jss::mpt_issuance_id].isString() || + !u.parseHex(c[jss::mpt_issuance_id].asString())) + { + jvStatus = rpcError(rpcSRC_CUR_MALFORMED); + return PFR_PJ_INVALID; + } + srcPathAsset = u; + } + // Optional issuer AccountID srcIssuerID; if (c.isMember(jss::issuer) && - (!c[jss::issuer].isString() || + (c.isMember(jss::mpt_issuance_id) || + !c[jss::issuer].isString() || !to_issuer(srcIssuerID, c[jss::issuer].asString()))) { jvStatus = rpcError(rpcSRC_ISR_MALFORMED); return PFR_PJ_INVALID; } - if (srcCurrencyID.isZero()) + if (srcPathAsset.holds()) { - if (srcIssuerID.isNonZero()) + if (srcPathAsset.get().isZero()) { - jvStatus = rpcError(rpcSRC_CUR_MALFORMED); - return PFR_PJ_INVALID; + if (srcIssuerID.isNonZero()) + { + jvStatus = rpcError(rpcSRC_CUR_MALFORMED); + return PFR_PJ_INVALID; + } + } + else if (srcIssuerID.isZero()) + { + srcIssuerID = *raSrcAccount; } - } - else if (srcIssuerID.isZero()) - { - srcIssuerID = *raSrcAccount; } if (saSendMax) { - // If the currencies don't match, ignore the source currency. - if (srcCurrencyID == saSendMax->getCurrency()) + // If the assets don't match, ignore the source asset. + if (equalAssets(srcPathAsset, saSendMax->asset())) { // If neither is the source and they are not equal, then the // source issuer is illegal. @@ -410,26 +438,37 @@ PathRequest::parseJson(Json::Value const& jvParams) // If both are the source, use the source. // Otherwise, use the one that's not the source. - if (srcIssuerID != *raSrcAccount) + if (srcPathAsset.holds()) { - sciSourceCurrencies.insert( - {srcCurrencyID, srcIssuerID}); - } - else if (saSendMax->getIssuer() != *raSrcAccount) - { - sciSourceCurrencies.insert( - {srcCurrencyID, saSendMax->getIssuer()}); + if (srcIssuerID != *raSrcAccount) + { + sciSourceAssets.insert(Issue{ + srcPathAsset.get(), srcIssuerID}); + } + else if (saSendMax->getIssuer() != *raSrcAccount) + { + sciSourceAssets.insert(Issue{ + srcPathAsset.get(), + saSendMax->getIssuer()}); + } + else + { + sciSourceAssets.insert(Issue{ + srcPathAsset.get(), *raSrcAccount}); + } } else - { - sciSourceCurrencies.insert( - {srcCurrencyID, *raSrcAccount}); - } + sciSourceAssets.insert(srcPathAsset.get()); } } + else if (srcPathAsset.holds()) + { + sciSourceAssets.insert( + Issue{srcPathAsset.get(), srcIssuerID}); + } else { - sciSourceCurrencies.insert({srcCurrencyID, srcIssuerID}); + sciSourceAssets.insert(MPTIssue{srcPathAsset.get()}); } } } @@ -465,21 +504,21 @@ PathRequest::doAborting() const std::unique_ptr const& PathRequest::getPathFinder( - std::shared_ptr const& cache, - hash_map>& currency_map, - Currency const& currency, + std::shared_ptr const& cache, + hash_map>& pathasset_map, + PathAsset const& asset, STAmount const& dst_amount, int const level, std::function const& continueCallback) { - auto i = currency_map.find(currency); - if (i != currency_map.end()) + auto i = pathasset_map.find(asset); + if (i != pathasset_map.end()) return i->second; auto pathfinder = std::make_unique( cache, *raSrcAccount, *raDstAccount, - currency, + asset, std::nullopt, dst_amount, saSendMax, @@ -488,51 +527,60 @@ PathRequest::getPathFinder( pathfinder->computePathRanks(max_paths_, continueCallback); else pathfinder.reset(); // It's a bad request - clear it. - return currency_map[currency] = std::move(pathfinder); + return pathasset_map[asset] = std::move(pathfinder); } bool PathRequest::findPaths( - std::shared_ptr const& cache, + std::shared_ptr const& cache, int const level, Json::Value& jvArray, std::function const& continueCallback) { - auto sourceCurrencies = sciSourceCurrencies; - if (sourceCurrencies.empty() && saSendMax) + auto sourceAssets = sciSourceAssets; + if (sourceAssets.empty() && saSendMax) { - sourceCurrencies.insert(saSendMax->issue()); + sourceAssets.insert(saSendMax->asset()); } - if (sourceCurrencies.empty()) + if (sourceAssets.empty()) { auto currencies = accountSourceCurrencies(*raSrcAccount, cache, true); bool const sameAccount = *raSrcAccount == *raDstAccount; for (auto const& c : currencies) { - if (!sameAccount || c != saDstAmount.getCurrency()) + if (!sameAccount || + (saDstAmount.holds() && + c != saDstAmount.get().currency)) { - if (sourceCurrencies.size() >= RPC::Tuning::max_auto_src_cur) + if (sourceAssets.size() >= RPC::Tuning::max_auto_src_cur) return false; - sourceCurrencies.insert( - {c, c.isZero() ? xrpAccount() : *raSrcAccount}); + sourceAssets.insert( + Issue{c, c.isZero() ? xrpAccount() : *raSrcAccount}); } } + if (auto mpts = cache->getMPTs(*raSrcAccount)) + { + if (sourceAssets.size() >= RPC::Tuning::max_auto_src_cur) + return false; + for (auto const& mpt : *mpts) + sourceAssets.insert(mpt); + } } auto const dst_amount = convertAmount(saDstAmount, convert_all_); - hash_map> currency_map; - for (auto const& issue : sourceCurrencies) + hash_map> pathasset_map; + for (auto const& asset : sourceAssets) { if (continueCallback && !continueCallback()) break; JLOG(m_journal.debug()) << iIdentifier - << " Trying to find paths: " << STAmount(issue, 1).getFullText(); + << " Trying to find paths: " << STAmount(asset, 1).getFullText(); auto& pathfinder = getPathFinder( cache, - currency_map, - issue.currency, + pathasset_map, + PathAsset::toPathAsset(asset), dst_amount, level, continueCallback); @@ -546,23 +594,32 @@ PathRequest::findPaths( auto ps = pathfinder->getBestPaths( max_paths_, fullLiquidityPath, - mContext[issue], - issue.account, + mContext[asset], + asset.getIssuer(), continueCallback); - mContext[issue] = ps; + mContext[asset] = ps; auto const& sourceAccount = [&] { - if (!isXRP(issue.account)) - return issue.account; + if (!isXRP(asset.getIssuer())) + return asset.getIssuer(); - if (isXRP(issue.currency)) + if (isXRP(asset)) return xrpAccount(); return *raSrcAccount; }(); - STAmount saMaxAmount = saSendMax.value_or( - STAmount(Issue{issue.currency, sourceAccount}, 1u, 0, true)); + STAmount saMaxAmount = [&]() { + if (saSendMax) + return *saSendMax; + if (asset.holds()) + return STAmount( + Issue{asset.get().currency, sourceAccount}, + 1u, + 0, + true); + return STAmount(asset.get(), 1u, 0, true); + }(); JLOG(m_journal.debug()) << iIdentifier << " Paths found, calling rippleCalc"; @@ -619,7 +676,9 @@ PathRequest::findPaths( if (rc.result() == tesSUCCESS) { Json::Value jvEntry(Json::objectValue); - rc.actualAmountIn.setIssuer(sourceAccount); + // TODO MPT + if (rc.actualAmountIn.holds()) + rc.actualAmountIn.setIssuer(sourceAccount); jvEntry[jss::source_amount] = rc.actualAmountIn.getJson(JsonOptions::none); jvEntry[jss::paths_computed] = ps.getJson(JsonOptions::none); @@ -647,14 +706,14 @@ PathRequest::findPaths( The minimum cost is 50 and the maximum is 400. The cost increases after four source currencies, 50 - (4 * 4) = 34. */ - int const size = sourceCurrencies.size(); + int const size = sourceAssets.size(); consumer_.charge({std::clamp(size * size + 34, 50, 400), "path update"}); return true; } Json::Value PathRequest::doUpdate( - std::shared_ptr const& cache, + std::shared_ptr const& cache, bool fast, std::function const& continueCallback) { @@ -674,11 +733,16 @@ PathRequest::doUpdate( if (hasCompletion()) { // Old ripple_path_find API gives destination_currencies - auto& destCurrencies = + auto& destAssets = (newStatus[jss::destination_currencies] = Json::arrayValue); - auto usCurrencies = accountDestCurrencies(*raDstAccount, cache, true); - for (auto const& c : usCurrencies) - destCurrencies.append(to_string(c)); + auto usAssets = accountDestCurrencies(*raDstAccount, cache, true); + for (auto const& c : usAssets) + destAssets.append(to_string(c)); + if (auto mpts = cache->getMPTs(*raDstAccount)) + { + for (auto const& mpt : *mpts) + destAssets.append(to_string(mpt)); + } } newStatus[jss::source_account] = toBase58(*raSrcAccount); diff --git a/src/xrpld/app/paths/PathRequest.h b/src/xrpld/app/paths/PathRequest.h index 21f10d066ba..534b1392946 100644 --- a/src/xrpld/app/paths/PathRequest.h +++ b/src/xrpld/app/paths/PathRequest.h @@ -21,10 +21,11 @@ #define RIPPLE_APP_PATHS_PATHREQUEST_H_INCLUDED #include +#include #include -#include #include #include +#include #include #include #include @@ -37,7 +38,7 @@ namespace ripple { // A pathfinding request submitted by a client // The request issuer must maintain a strong pointer -class RippleLineCache; +class AssetCache; class PathRequests; // Return values from parseJson <0 = invalid, >0 = valid @@ -86,7 +87,7 @@ class PathRequest final : public InfoSubRequest, updateComplete(); std::pair - doCreate(std::shared_ptr const&, Json::Value const&); + doCreate(std::shared_ptr const&, Json::Value const&); Json::Value doClose() override; @@ -98,7 +99,7 @@ class PathRequest final : public InfoSubRequest, // update jvStatus Json::Value doUpdate( - std::shared_ptr const&, + std::shared_ptr const&, bool fast, std::function const& continueCallback = {}); InfoSub::pointer @@ -108,13 +109,13 @@ class PathRequest final : public InfoSubRequest, private: bool - isValid(std::shared_ptr const& crCache); + isValid(std::shared_ptr const& crCache); std::unique_ptr const& getPathFinder( - std::shared_ptr const&, - hash_map>&, - Currency const&, + std::shared_ptr const&, + hash_map>&, + PathAsset const&, STAmount const&, int const, std::function const&); @@ -124,7 +125,7 @@ class PathRequest final : public InfoSubRequest, */ bool findPaths( - std::shared_ptr const&, + std::shared_ptr const&, int const, Json::Value&, std::function const&); @@ -152,8 +153,8 @@ class PathRequest final : public InfoSubRequest, STAmount saDstAmount; std::optional saSendMax; - std::set sciSourceCurrencies; - std::map mContext; + std::set sciSourceAssets; + std::map mContext; bool convert_all_; diff --git a/src/xrpld/app/paths/PathRequests.cpp b/src/xrpld/app/paths/PathRequests.cpp index 86560445ec7..8888096ff04 100644 --- a/src/xrpld/app/paths/PathRequests.cpp +++ b/src/xrpld/app/paths/PathRequests.cpp @@ -30,21 +30,22 @@ namespace ripple { -/** Get the current RippleLineCache, updating it if necessary. +/** Get the current AssetCache, updating it if necessary. Get the correct ledger to use. */ -std::shared_ptr -PathRequests::getLineCache( +std::shared_ptr +PathRequests::getAssetCache( std::shared_ptr const& ledger, bool authoritative) { std::lock_guard sl(mLock); - auto lineCache = lineCache_.lock(); + auto assetCache = assetCache_.lock(); - std::uint32_t const lineSeq = lineCache ? lineCache->getLedger()->seq() : 0; + std::uint32_t const lineSeq = + assetCache ? assetCache->getLedger()->seq() : 0; std::uint32_t const lgrSeq = ledger->seq(); - JLOG(mJournal.debug()) << "getLineCache has cache for " << lineSeq + JLOG(mJournal.debug()) << "getAssetCache has cache for " << lineSeq << ", considering " << lgrSeq; if ((lineSeq == 0) || // no ledger @@ -54,14 +55,14 @@ PathRequests::getLineCache( (lgrSeq > (lineSeq + 8))) // we jumped way forward for some reason { JLOG(mJournal.debug()) - << "getLineCache creating new cache for " << lgrSeq; + << "getAssetCache creating new cache for " << lgrSeq; // Assign to the local before the member, because the member is a // weak_ptr, and will immediately discard it if there are no other // references. - lineCache_ = lineCache = std::make_shared( - ledger, app_.journal("RippleLineCache")); + assetCache_ = assetCache = + std::make_shared(ledger, app_.journal("AssetCache")); } - return lineCache; + return assetCache; } void @@ -71,13 +72,13 @@ PathRequests::updateAll(std::shared_ptr const& inLedger) app_.getJobQueue().makeLoadEvent(jtPATH_FIND, "PathRequest::updateAll"); std::vector requests; - std::shared_ptr cache; + std::shared_ptr cache; // Get the ledger and cache we should be using { std::lock_guard sl(mLock); requests = requests_; - cache = getLineCache(inLedger, true); + cache = getAssetCache(inLedger, true); } bool newRequests = app_.getLedgerMaster().isNewPathRequest(); @@ -202,7 +203,7 @@ PathRequests::updateAll(std::shared_ptr const& inLedger) // Hold on to the line cache until after the lock is released, so it can // be destroyed outside of the lock - std::shared_ptr lastCache; + std::shared_ptr lastCache; { // Get the latest requests, cache, and ledger for next pass std::lock_guard sl(mLock); @@ -211,7 +212,7 @@ PathRequests::updateAll(std::shared_ptr const& inLedger) break; requests = requests_; lastCache = cache; - cache = getLineCache(cache->getLedger(), false); + cache = getAssetCache(cache->getLedger(), false); } } while (!app_.getJobQueue().isStopping()); @@ -255,7 +256,7 @@ PathRequests::makePathRequest( app_, subscriber, ++mLastIdentifier, *this, mJournal); auto [valid, jvRes] = - req->doCreate(getLineCache(inLedger, false), requestJson); + req->doCreate(getAssetCache(inLedger, false), requestJson); if (valid) { @@ -280,7 +281,8 @@ PathRequests::makeLegacyPathRequest( req = std::make_shared( app_, completion, consumer, ++mLastIdentifier, *this, mJournal); - auto [valid, jvRes] = req->doCreate(getLineCache(inLedger, false), request); + auto [valid, jvRes] = + req->doCreate(getAssetCache(inLedger, false), request); if (!valid) { @@ -306,8 +308,8 @@ PathRequests::doLegacyPathRequest( std::shared_ptr const& inLedger, Json::Value const& request) { - auto cache = std::make_shared( - inLedger, app_.journal("RippleLineCache")); + auto cache = + std::make_shared(inLedger, app_.journal("AssetCache")); auto req = std::make_shared( app_, [] {}, consumer, ++mLastIdentifier, *this, mJournal); diff --git a/src/xrpld/app/paths/PathRequests.h b/src/xrpld/app/paths/PathRequests.h index 670790518a1..e5760d64ea2 100644 --- a/src/xrpld/app/paths/PathRequests.h +++ b/src/xrpld/app/paths/PathRequests.h @@ -21,8 +21,8 @@ #define RIPPLE_APP_PATHS_PATHREQUESTS_H_INCLUDED #include +#include #include -#include #include #include #include @@ -54,8 +54,8 @@ class PathRequests bool requestsPending() const; - std::shared_ptr - getLineCache( + std::shared_ptr + getAssetCache( std::shared_ptr const& ledger, bool authoritative); @@ -111,8 +111,8 @@ class PathRequests // Track all requests std::vector requests_; - // Use a RippleLineCache - std::weak_ptr lineCache_; + // Use a AssetCache + std::weak_ptr assetCache_; std::atomic mLastIdentifier; diff --git a/src/xrpld/app/paths/Pathfinder.cpp b/src/xrpld/app/paths/Pathfinder.cpp index a5fe2afe949..52a30c849cb 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,23 +207,16 @@ Pathfinder::Pathfinder( isXRP(saDstAmount.getIssuer()) ? uDstAccount : saDstAmount.getIssuer()) , mDstAmount(saDstAmount) - , mSrcCurrency(uSrcCurrency) + , mSrcPathAsset(uSrcPathAsset) , mSrcIssuer(uSrcIssuer) - , mSrcAmount(srcAmount.value_or(STAmount( - Issue{ - uSrcCurrency, - uSrcIssuer.value_or( - isXRP(uSrcCurrency) ? xrpAccount() : uSrcAccount)}, - 1u, - 0, - true))) + , mSrcAmount(amountFromPathAsset(uSrcPathAsset, uSrcIssuer, uSrcAccount)) , convert_all_(convertAllCheck(mDstAmount)) , mLedger(cache->getLedger()) - , mRLCache(cache) + , mAssetCache(cache) , app_(app) , j_(app.journal("Pathfinder")) { - assert(!uSrcIssuer || isXRP(uSrcCurrency) == isXRP(uSrcIssuer.value())); + assert(!uSrcIssuer || isXRP(uSrcPathAsset) == isXRP(uSrcIssuer.value())); } bool @@ -210,7 +237,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"; @@ -218,27 +245,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 << " mDstAccount=" << mDstAccount << " mDstAmount=" << mDstAmount.getFullText() - << " mSrcCurrency=" << mSrcCurrency + << " mSrcPathAsset=" << mSrcPathAsset << " mSrcIssuer=" << issuerString; if (!mLedger) @@ -247,8 +273,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))) { @@ -305,7 +331,7 @@ Pathfinder::findPaths( JLOG(j_.debug()) << "non-XRP to XRP payment"; paymentType = pt_nonXRP_to_XRP; } - else if (mSrcCurrency == mDstAmount.getCurrency()) + else if (equalAssets(mSrcPathAsset, mDstAmount.asset())) { // non-XRP -> non-XRP - Same currency JLOG(j_.debug()) << "non-XRP to non-XRP - same currency"; @@ -580,7 +606,7 @@ Pathfinder::getBestPaths( assert(fullLiquidityPath.empty()); const bool issuerIsSender = - isXRP(mSrcCurrency) || (srcIssuer == mSrcAccount); + isXRP(mSrcPathAsset) || (srcIssuer == mSrcAccount); std::vector extraPathRanks; rankPaths(maxPaths, extraPaths, extraPathRanks, continueCallback); @@ -695,28 +721,28 @@ Pathfinder::getBestPaths( } bool -Pathfinder::issueMatchesOrigin(Issue const& issue) +Pathfinder::issueMatchesOrigin(Asset const& asset) { - bool matchingCurrency = (issue.currency == mSrcCurrency); - bool matchingAccount = isXRP(issue.currency) || - (mSrcIssuer && issue.account == mSrcIssuer) || - issue.account == mSrcAccount; + bool matchingAsset = equalAssets(asset, mSrcPathAsset); + bool matchingAccount = isXRP(asset) || + (mSrcIssuer && asset.getIssuer() == mSrcIssuer) || + asset.getIssuer() == mSrcAccount; - return matchingCurrency && matchingAccount; + return matchingAsset && matchingAccount; } int Pathfinder::getPathsOut( - Currency const& currency, + PathAsset const& pathAsset, AccountID const& account, - LineDirection direction, - bool isDstCurrency, + std::optional direction, + bool isDstAsset, AccountID const& dstAccount, std::function const& continueCallback) { - Issue const issue(currency, account); + Asset const asset = assetFromPathAsset(pathAsset, account); - auto [it, inserted] = mPathsOutCountMap.emplace(issue, 0); + auto [it, inserted] = mPathsOutCountMap.emplace(asset, 0); // If it was already present, return the stored number of paths if (!inserted) @@ -728,41 +754,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 { @@ -918,7 +983,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 @@ -942,10 +1008,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) @@ -977,27 +1043,40 @@ Pathfinder::addLink( { bool const bRequireAuth( sleEnd->getFieldU32(sfFlags) & lsfRequireAuth); - bool const bIsEndCurrency( - uEndCurrency == mDstAmount.getCurrency()); + bool const bIsEndAsset( + equalAssets(uEndPathAsset, mDstAmount.asset())); bool const bIsNoRippleOut(isNoRippleOut(currentPath)); bool const bDestOnly(addFlags & afAC_LAST); - if (auto const lines = mRLCache->getRippleLines( - uEndAccount, - bIsNoRippleOut ? LineDirection::incoming - : LineDirection::outgoing)) - { - auto& rippleLines = *lines; + AccountCandidates candidates; - AccountCandidates candidates; - candidates.reserve(rippleLines.size()); + auto forAssets = [&]( + AssetType const& assets) { + candidates.reserve(assets.size()); - for (auto const& rs : rippleLines) + static bool constexpr isLine = std:: + is_same_v>; + static bool constexpr isMPT = + std::is_same_v>; + + for (auto const& asset : assets) { if (continueCallback && !continueCallback()) return; - auto const& acct = rs.getAccountIDPeer(); - LineDirection const direction = rs.getDirectionPeer(); + // clang-format off + 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; + }(); + // clang-format on if (hasEffectiveDestination && (acct == mDstAccount)) { @@ -1012,26 +1091,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()) @@ -1059,10 +1153,10 @@ Pathfinder::addLink( { // save this candidate int out = getPathsOut( - uEndCurrency, + uEndPathAsset, acct, direction, - bIsEndCurrency, + bIsEndAsset, mEffectiveDst, continueCallback); if (out) @@ -1070,40 +1164,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; } } } @@ -1120,7 +1228,8 @@ Pathfinder::addLink( { // to XRP only if (!bOnXRP && - app_.getOrderBookDB().isBookToXRP({uEndCurrency, uEndIssuer})) + app_.getOrderBookDB().isBookToXRP( + assetFromPathAsset(uEndPathAsset, uEndIssuer))) { STPathElement pathElement( STPathElement::typeCurrency, @@ -1134,7 +1243,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"; @@ -1143,14 +1252,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 || book.out == mDstAmount.asset())) { STPath newPath(currentPath); - if (book.out.currency.isZero()) + if (isXRP(book.out)) { // to XRP // add the order book itself @@ -1160,7 +1268,7 @@ Pathfinder::addLink( xrpCurrency(), xrpAccount()); - if (mDstAmount.getCurrency().isZero()) + if (isXRP(mDstAmount.asset())) { // destination is XRP, add account and path is // complete @@ -1173,10 +1281,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) && @@ -1185,32 +1296,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 && + book.out == mDstAmount.asset()) { // We skipped a required issuer } else if ( - book.out.account == mEffectiveDst && - book.out.currency == mDstAmount.getCurrency()) + book.out.getIssuer() == mEffectiveDst && + book.out == mDstAmount.asset()) { // with the destination account, this path is // complete JLOG(j_.trace()) @@ -1220,14 +1329,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 8215cdee593..d0e26ba7dab 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_); @@ -80,7 +80,7 @@ AMMLiquidity::generateFibSeqOffer( assert(!ammContext_.maxItersReached()); 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 @@ -104,11 +104,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(STAmount::cMaxNative); } 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); @@ -132,7 +134,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( @@ -234,8 +236,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; } @@ -250,9 +252,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 8a1117f9920..17cc5a84565 100644 --- a/src/xrpld/app/paths/detail/AmountSpec.h +++ b/src/xrpld/app/paths/detail/AmountSpec.h @@ -32,22 +32,56 @@ struct AmountSpec { explicit AmountSpec() = default; - bool native; - union - { - XRPAmount xrp; - IOUAmount iou = {}; - }; + std::variant amount; std::optional issuer; std::optional currency; + std::optional mptid; + + bool + native() const + { + return std::holds_alternative(amount); + } + bool + isIOU() const + { + return std::holds_alternative(amount); + } + template + void + check() const + { + if (!std::holds_alternative(amount)) + Throw("AmountSpec doesn't hold requested amount"); + } + XRPAmount const& + xrp() const + { + check(); + return std::get(amount); + } + IOUAmount const& + iou() const + { + check(); + return std::get(amount); + } + MPTAmount const& + mpt() const + { + check(); + return std::get(amount); + } friend std::ostream& operator<<(std::ostream& stream, AmountSpec const& amt) { - if (amt.native) - stream << to_string(amt.xrp); + if (std::holds_alternative(amt.amount)) + stream << to_string(*amt.mptid); + else if (amt.native()) + stream << to_string(amt.xrp()); else - stream << to_string(amt.iou); + stream << to_string(amt.iou()); if (amt.currency) stream << "/(" << *amt.currency << ")"; if (amt.issuer) @@ -58,63 +92,86 @@ struct AmountSpec struct EitherAmount { -#ifndef NDEBUG - bool native = false; -#endif - - union - { - IOUAmount iou = {}; - XRPAmount xrp; - }; + std::variant amount; EitherAmount() = default; - explicit EitherAmount(IOUAmount const& a) : iou(a) + explicit EitherAmount(IOUAmount const& a) : amount(a) { } -#if defined(__GNUC__) && !defined(__clang__) -#pragma GCC diagnostic push - // ignore warning about half of iou amount being uninitialized -#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" -#endif - explicit EitherAmount(XRPAmount const& a) : xrp(a) + explicit EitherAmount(XRPAmount const& a) : amount(a) + { + } + + explicit EitherAmount(MPTAmount const& a) : amount(a) { -#ifndef NDEBUG - native = true; -#endif } -#if defined(__GNUC__) && !defined(__clang__) -#pragma GCC diagnostic pop -#endif explicit EitherAmount(AmountSpec const& a) { -#ifndef NDEBUG - native = a.native; -#endif - if (a.native) - xrp = a.xrp; - else - iou = a.iou; + amount = a.amount; + } + + bool + native() const + { + return std::holds_alternative(amount); + } + bool + isIOU() const + { + return std::holds_alternative(amount); + } + bool + isMPT() const + { + return std::holds_alternative(amount); + } + template + void + check() const + { + if (!std::holds_alternative(amount)) + Throw( + "EitherAmount doesn't hold requested amount"); + } + XRPAmount const& + xrp() const + { + check(); + return std::get(amount); + } + IOUAmount const& + iou() const + { + check(); + return std::get(amount); + } + MPTAmount const& + mpt() const + { + check(); + return std::get(amount); } #ifndef NDEBUG friend std::ostream& operator<<(std::ostream& stream, EitherAmount const& amt) { - if (amt.native) - stream << to_string(amt.xrp); + if (amt.native()) + stream << to_string(amt.xrp()); + else if (amt.isIOU()) + stream << to_string(amt.iou()); else - stream << to_string(amt.iou); + stream << to_string(amt.mpt()); return stream; } #endif }; template -T& +T const& get(EitherAmount& amt) { static_assert(sizeof(T) == -1, "Must used specialized function"); @@ -122,19 +179,27 @@ get(EitherAmount& amt) } template <> -inline IOUAmount& +inline IOUAmount const& get(EitherAmount& amt) { - assert(!amt.native); - return amt.iou; + assert(amt.isIOU()); + return amt.iou(); } template <> -inline XRPAmount& +inline XRPAmount const& get(EitherAmount& amt) { - assert(amt.native); - return amt.xrp; + assert(amt.native()); + return amt.xrp(); +} + +template <> +inline MPTAmount const& +get(EitherAmount& amt) +{ + assert(amt.isMPT()); + return amt.mpt(); } template @@ -149,16 +214,24 @@ template <> inline IOUAmount const& get(EitherAmount const& amt) { - assert(!amt.native); - return amt.iou; + assert(!amt.native()); + return amt.iou(); } template <> inline XRPAmount const& get(EitherAmount const& amt) { - assert(amt.native); - return amt.xrp; + assert(amt.native()); + return amt.xrp(); +} + +template <> +inline MPTAmount const& +get(EitherAmount const& amt) +{ + assert(amt.isMPT()); + return amt.mpt(); } inline AmountSpec @@ -170,14 +243,18 @@ toAmountSpec(STAmount const& amt) isNeg ? -std::int64_t(amt.mantissa()) : amt.mantissa(); AmountSpec result; - result.native = isXRP(amt); - if (result.native) + if (isXRP(amt)) { - result.xrp = XRPAmount(sMant); + result.amount = XRPAmount(sMant); + } + else if (amt.holds()) + { + result.mptid = amt.get().getMptID(); + result.amount = amt.mpt(); } else { - result.iou = IOUAmount(sMant, amt.exponent()); + result.amount = IOUAmount(sMant, amt.exponent()); result.issuer = amt.issue().account; result.currency = amt.issue().currency; } @@ -190,24 +267,18 @@ toEitherAmount(STAmount const& amt) { if (isXRP(amt)) return EitherAmount{amt.xrp()}; - return EitherAmount{amt.iou()}; + else if (amt.holds()) + return EitherAmount{amt.iou()}; + return EitherAmount(amt.mpt()); } inline AmountSpec toAmountSpec(EitherAmount const& ea, std::optional const& c) { AmountSpec r; - r.native = (!c || isXRP(*c)); r.currency = c; - assert(ea.native == r.native); - if (r.native) - { - r.xrp = ea.xrp; - } - else - { - r.iou = ea.iou; - } + assert(ea.native() == r.native()); + 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 96971a516fc..e7146716a55 100644 --- a/src/xrpld/app/paths/detail/BookStep.cpp +++ b/src/xrpld/app/paths/detail/BookStep.cpp @@ -89,7 +89,7 @@ class BookStep : public StepImp> } public: - BookStep(StrandContext const& ctx, Issue const& in, Issue const& out) + BookStep(StrandContext const& ctx, Asset const& in, Asset const& out) : maxOffersToConsume_(getMaxOffersToConsume(ctx)) , book_(in, out) , strandSrc_(ctx.strandSrc) @@ -189,10 +189,10 @@ class BookStep : public StepImp> { std::ostringstream ostr; ostr << name << ": " - << "\ninIss: " << book_.in.account - << "\noutIss: " << book_.out.account - << "\ninCur: " << book_.in.currency - << "\noutCur: " << book_.out.currency; + << "\ninIss: " << book_.in.getIssuer() + << "\noutIss: " << book_.out.getIssuer() + << "\ninCur: " << to_string(book_.in) + << "\noutCur: " << to_string(book_.out); return ostr.str(); } @@ -337,19 +337,21 @@ class BookPaymentStep : public BookStep> // (the old code does not charge a fee) // Calculate amount that goes to the taker and the amount charged the // offer owner - auto rate = [&](AccountID const& id) { - if (isXRP(id) || id == this->strandDst_) + auto rate = [&](Asset const& asset) { + if (isXRP(asset) || asset.getIssuer() == this->strandDst_) return parityRate; - return transferRate(v, id); + if (asset.holds()) + return transferRate(v, asset.getIssuer()); + return transferRate(v, asset.get().getMptID()); }; auto const trIn = - redeems(prevStepDir) ? rate(this->book_.in.account) : parityRate; + redeems(prevStepDir) ? rate(this->book_.in) : parityRate; // Always charge the transfer fee, even if the owner is the issuer, // unless the fee is waived auto const trOut = (this->ownerPaysTransferFee_ && waiveFee == WaiveTransferFee::No) - ? rate(this->book_.out.account) + ? rate(this->book_.out) : parityRate; Quality const q1{getRate(STAmount(trOut.value), STAmount(trIn.value))}; @@ -389,8 +391,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)) @@ -544,8 +546,9 @@ class BookOfferCrossingStep return transferRate(v, id); }; - auto const trIn = - redeems(prevStepDir) ? rate(this->book_.in.account) : parityRate; + auto const trIn = redeems(prevStepDir) + ? rate(this->book_.in.getIssuer()) + : parityRate; // AMM doesn't pay the transfer fee on the out amount auto const trOut = parityRate; @@ -721,17 +724,19 @@ BookStep::forEachOffer( // (the old code does not charge a fee) // Calculate amount that goes to the taker and the amount charged the offer // owner - auto rate = [this, &sb](AccountID const& id) -> std::uint32_t { - if (isXRP(id) || id == this->strandDst_) + auto rate = [this, &sb](Asset const& asset) -> std::uint32_t { + if (isXRP(asset) || asset.getIssuer() == this->strandDst_) return QUALITY_ONE; - return transferRate(sb, id).value; + if (asset.holds()) + return transferRate(sb, asset.getIssuer()).value; + return transferRate(sb, asset.get().getMptID()).value; }; std::uint32_t const trIn = - redeems(prevStepDir) ? rate(book_.in.account) : QUALITY_ONE; + redeems(prevStepDir) ? rate(book_.in) : QUALITY_ONE; // Always charge the transfer fee, even if the owner is the issuer std::uint32_t const trOut = - ownerPaysTransferFee_ ? rate(book_.out.account) : QUALITY_ONE; + ownerPaysTransferFee_ ? rate(book_.out) : QUALITY_ONE; typename FlowOfferStream::StepCounter counter( maxOffersToConsume_, j_); @@ -756,34 +761,18 @@ BookStep::forEachOffer( // Make sure offer owner has authorization to own IOUs from issuer. // An account can always own XRP or their own IOUs. - if (flowCross && (!isXRP(offer.issueIn().currency)) && - (offer.owner() != offer.issueIn().account)) + if (flowCross && + 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( @@ -891,7 +880,7 @@ BookStep::consumeOffer( { auto const dr = offer.send( sb, - book_.in.account, + book_.in.getIssuer(), offer.owner(), toSTAmount(ofrAmt.in, book_.in), j_); @@ -905,7 +894,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) @@ -1351,20 +1340,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)) @@ -1378,14 +1368,25 @@ BookStep::check(StrandContext const& ctx) const if (auto const prev = ctx.prevStep->directStepSrcAcct()) { auto const& view = ctx.view; - auto const& cur = book_.in.account; - - auto sle = view.read(keylet::line(*prev, cur, book_.in.currency)); - if (!sle) - return terNO_LINE; - if ((*sle)[sfFlags] & - ((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple)) - return terNO_RIPPLE; + auto const& cur = book_.in.getIssuer(); + + if (book_.in.holds()) + { + auto sle = view.read( + keylet::line(*prev, cur, book_.in.get().currency)); + if (!sle) + return terNO_LINE; + if ((*sle)[sfFlags] & + ((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple)) + return terNO_RIPPLE; + } + else + { + auto const mptID = + keylet::mptIssuance(book_.in.get().getMptID()); + if (auto const sle = view.read(mptID); !sle) + return terNO_MPT; + } } } @@ -1406,32 +1407,38 @@ equalHelper(Step const& step, ripple::Book const& book) return false; } +static std::variant +getTypedAmt(Asset const& asset) +{ + static auto xrp = XRPAmount{}; + static auto mpt = MPTAmount{}; + static auto iou = IOUAmount{}; + if (asset.holds()) + { + if (isXRP(asset.get().currency)) + return &xrp; + return &iou; + } + return &mpt; +} + bool bookStepEqual(Step const& step, ripple::Book const& book) { - bool const inXRP = isXRP(book.in.currency); - bool const outXRP = isXRP(book.out.currency); - if (inXRP && outXRP) + if (isXRP(book.in) && isXRP(book.out)) { assert(0); return false; // no such thing as xrp/xrp book step } - if (inXRP && !outXRP) - return equalHelper< - XRPAmount, - IOUAmount, - BookPaymentStep>(step, book); - if (!inXRP && outXRP) - return equalHelper< - IOUAmount, - XRPAmount, - BookPaymentStep>(step, book); - if (!inXRP && !outXRP) - return equalHelper< - IOUAmount, - IOUAmount, - BookPaymentStep>(step, book); - return false; + bool ret = false; + std::visit( + [&](TIn*&&, TOut*&&) { + ret = + equalHelper>(step, book); + }, + getTypedAmt(book.in), + getTypedAmt(book.out)); + return ret; } } // namespace test @@ -1439,7 +1446,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; @@ -1481,4 +1488,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 25e78e9c47e..563b4dd40b0 100644 --- a/src/xrpld/app/paths/detail/DirectStep.cpp +++ b/src/xrpld/app/paths/detail/DirectStep.cpp @@ -720,7 +720,7 @@ DirectStepI::validFwd( auto const savCache = *cache_; - assert(!in.native); + assert(!in.native()); auto const [maxSrcToDst, srcDebtDir] = static_cast(this)->maxFlow(sb, cache_->srcToDst); @@ -729,7 +729,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&) { @@ -932,13 +932,14 @@ DirectStepI::check(StrandContext const& ctx) const // issue if (auto book = ctx.prevStep->bookStepBook()) { - if (book->out != srcIssue) + if (book->out.holds() && + book->out.get() != srcIssue) return temBAD_PATH_LOOP; } } - if (!ctx.seenDirectIssues[0].insert(srcIssue).second || - !ctx.seenDirectIssues[1].insert(dstIssue).second) + if (!ctx.seenDirectAssets[0].insert(srcIssue).second || + !ctx.seenDirectAssets[1].insert(dstIssue).second) { JLOG(j_.debug()) << "DirectStepI: loop detected: Index: " << ctx.strandSize diff --git a/src/xrpld/app/paths/detail/FlowDebugInfo.h b/src/xrpld/app/paths/detail/FlowDebugInfo.h index 000db4e5714..bebf698f92f 100644 --- a/src/xrpld/app/paths/detail/FlowDebugInfo.h +++ b/src/xrpld/app/paths/detail/FlowDebugInfo.h @@ -233,7 +233,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); }; @@ -241,7 +241,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..7b38a7e6a08 --- /dev/null +++ b/src/xrpld/app/paths/detail/MPTEndpointStep.cpp @@ -0,0 +1,884 @@ +//------------------------------------------------------------------------------ +/* + 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 mptIssue_; + + // Charge transfer fees when the prev step redeems + Step const* const prevStep_ = nullptr; + bool const isLast_; + 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) + , 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 whether or not 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 +{ + // Since this is a payment a MPToken must be present. Perform all + // MPToken related checks. + if (!ctx.view.exists(keylet::mptIssuance(mptIssue_.getMptID()))) + return tecOBJECT_NOT_FOUND; + if (src_ != mptIssue_.getIssuer()) + { + auto const mptokenID = keylet::mptoken(mptIssue_.getMptID(), src_); + if (!ctx.view.exists(mptokenID)) + return tecOBJECT_NOT_FOUND; + + if (auto const ter = requireAuth(ctx.view, mptIssue_, src_); + ter != tesSUCCESS) + return ter; + } + + if (dst_ != mptIssue_.getIssuer()) + { + auto const mptokenID = keylet::mptoken(mptIssue_.getMptID(), dst_); + if (!ctx.view.exists(mptokenID)) + return tecOBJECT_NOT_FOUND; + + if (auto const ter = requireAuth(ctx.view, mptIssue_, dst_); + ter != tesSUCCESS) + return ter; + } + + // Direct MPT payment between holders + if (ctx.isFirst && ctx.strandDeliver.holds() && + mptIssue_ == ctx.strandDeliver.get() && dst_ != ctx.strandDst) + { + if (isFrozen(ctx.view, src_, mptIssue_) || + isFrozen(ctx.view, ctx.strandDst, mptIssue_)) + return tecMPT_LOCKED; + } + + return tesSUCCESS; +} + +TER +DirectMPTOfferCrossingStep::check( + StrandContext const&, + std::shared_ptr const&) const +{ + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +template +std::pair +MPTEndpointStep::maxPaymentFlow(ReadView const& sb) const +{ + if (src_ != mptIssue_.getIssuer()) + { + auto const srcOwed = toAmount(accountHolds( + sb, src_, mptIssue_, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_)); + + return {srcOwed, DebtDirection::redeems}; + } + + if (auto const sle = sb.read(keylet::mptIssuance(mptIssue_.getMptID()))) + { + std::int64_t const max = + [&]() { + auto const max = sle->getFieldU64(sfMaximumAmount); + return max > 0 ? max : STAmount::cMaxNativeN; // TODO MPT + }() - + sle->getFieldU64(sfOutstandingAmount); + return {MPTAmount{max}, DebtDirection::issues}; + } + + return {MPTAmount{0}, DebtDirection::issues}; // TODO MPT +} + +template +DebtDirection +MPTEndpointStep::debtDirection( + ReadView const& sb, + StrandDirection dir) const +{ + if (dir == StrandDirection::forward && cache_) + return cache_->srcDebtDir; + + if (src_ != mptIssue_.getIssuer()) + return DebtDirection::redeems; + return DebtDirection::issues; +} + +template +std::pair +MPTEndpointStep::revImp( + PaymentSandbox& sb, + ApplyView& /*afView*/, + boost::container::flat_set& /*ofrsToRm*/, + MPTAmount const& out) +{ + cache_.reset(); + + auto const [maxSrcToDst, srcDebtDir] = + static_cast(this)->maxFlow(sb, out); + + auto const [srcQOut, dstQIn] = + qualities(sb, srcDebtDir, StrandDirection::reverse); + assert(static_cast(this)->verifyDstQualityIn(dstQIn)); + + 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 = accountSendMPT( + sb, src_, dst_, toSTAmount(srcToDst, srcToDstIss), 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 = rippleMPTCredit( + sb, src_, dst_, toSTAmount(maxSrcToDst, srcToDstIss), j_); + (void)ter; + JLOG(j_.trace()) << "MPTEndpointStep::rev: Limiting" + << " srcRedeems: " << redeems(srcDebtDir) + << " in: " << to_string(in) + << " srcToDst: " << to_string(maxSrcToDst) + << " out: " << to_string(out); + return {in, actualOut}; +} + +// The forward pass should never have more liquidity than the reverse +// pass. But sometimes rounding differences cause the forward pass to +// deliver more liquidity. Use the cached values from the reverse pass +// to prevent this. +template +void +MPTEndpointStep::setCacheLimiting( + MPTAmount const& fwdIn, + MPTAmount const& fwdSrcToDst, + MPTAmount const& fwdOut, + DebtDirection srcDebtDir) +{ + if (cache_->in < fwdIn) + { + MPTAmount const smallDiff(1); + auto const diff = fwdIn - cache_->in; + if (diff > smallDiff) + { + if (!cache_->in.value() || + (double(fwdIn.value()) / double(cache_->in.value())) > 1.01) + { + // Detect large diffs on forward pass so they may be + // investigated + JLOG(j_.warn()) + << "MPTEndpointStep::fwd: setCacheLimiting" + << " fwdIn: " << to_string(fwdIn) + << " cacheIn: " << to_string(cache_->in) + << " fwdSrcToDst: " << to_string(fwdSrcToDst) + << " cacheSrcToDst: " << to_string(cache_->srcToDst) + << " fwdOut: " << to_string(fwdOut) + << " cacheOut: " << to_string(cache_->out); + cache_.emplace(fwdIn, fwdSrcToDst, fwdOut, srcDebtDir); + return; + } + } + } + cache_->in = fwdIn; + if (fwdSrcToDst < cache_->srcToDst) + cache_->srcToDst = fwdSrcToDst; + if (fwdOut < cache_->out) + cache_->out = fwdOut; + cache_->srcDebtDir = srcDebtDir; +}; + +template +std::pair +MPTEndpointStep::fwdImp( + PaymentSandbox& sb, + ApplyView& /*afView*/, + boost::container::flat_set& /*ofrsToRm*/, + MPTAmount const& in) +{ + assert(cache_); + + 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 = rippleMPTCredit( + sb, src_, dst_, toSTAmount(cache_->srcToDst, srcToDstIss), 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 = rippleMPTCredit( + sb, src_, dst_, toSTAmount(cache_->srcToDst, srcToDstIss), j_); + (void)ter; + JLOG(j_.trace()) << "MPTEndpointStep::rev: Limiting" + << " srcRedeems: " << redeems(srcDebtDir) + << " in: " << to_string(actualIn) + << " srcToDst: " << to_string(srcToDst) + << " out: " << to_string(out); + } + return {cache_->in, cache_->out}; +} + +template +std::pair +MPTEndpointStep::validFwd( + PaymentSandbox& sb, + ApplyView& afView, + EitherAmount const& in) +{ + if (!cache_) + { + JLOG(j_.trace()) << "Expected valid cache in validFwd"; + return {false, EitherAmount(MPTAmount(beast::zero))}; + } + + auto const savCache = *cache_; + + assert(!in.native() && !in.isIOU()); + + auto const [maxSrcToDst, srcDebtDir] = + static_cast(this)->maxFlow(sb, cache_->srcToDst); + (void)srcDebtDir; + + try + { + boost::container::flat_set dummy; + fwdImp(sb, afView, dummy, in.mpt()); // changes cache + } + catch (FlowException const&) + { + return {false, EitherAmount(MPTAmount(beast::zero))}; + } + + if (maxSrcToDst < cache_->srcToDst) + { + JLOG(j_.warn()) << "MPTEndpointStep: Strand re-execute check failed." + << " Exceeded max src->dst limit" + << " max src->dst: " << to_string(maxSrcToDst) + << " actual src->dst: " << to_string(cache_->srcToDst); + return {false, EitherAmount(cache_->out)}; + } + + if (!(checkNear(savCache.in, cache_->in) && + checkNear(savCache.out, cache_->out))) + { + JLOG(j_.warn()) << "MPTEndpointStep: Strand re-execute check failed." + << " ExpectedIn: " << to_string(savCache.in) + << " CachedIn: " << to_string(cache_->in) + << " ExpectedOut: " << to_string(savCache.out) + << " CachedOut: " << to_string(cache_->out); + return {false, EitherAmount(cache_->out)}; + } + return {true, EitherAmount(cache_->out)}; +} + +// Returns srcQOut, dstQIn +template +std::pair +MPTEndpointStep::qualitiesSrcRedeems(ReadView const& sb) const +{ + if (!prevStep_) + return {QUALITY_ONE, QUALITY_ONE}; + + auto const prevStepQIn = prevStep_->lineQualityIn(sb); + auto srcQOut = + static_cast(this)->quality(sb, QualityDirection::out); + + if (prevStepQIn > srcQOut) + srcQOut = prevStepQIn; + return {srcQOut, QUALITY_ONE}; +} + +// Returns srcQOut, dstQIn +template +std::pair +MPTEndpointStep::qualitiesSrcIssues( + ReadView const& sb, + DebtDirection prevStepDebtDirection) const +{ + // Charge a transfer rate when issuing and previous step redeems + + assert(static_cast(this)->verifyPrevStepDebtDirection( + prevStepDebtDirection)); + + 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 + if (!(ctx.isLast && ctx.isFirst)) + { + if (isFrozen(ctx.view, src_, mptIssue_) || + isFrozen(ctx.view, dst_, mptIssue_)) + return tecMPT_LOCKED; + } + + // 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 f28c1b96a7c..8ac4295d73c 100644 --- a/src/xrpld/app/paths/detail/PaySteps.cpp +++ b/src/xrpld/app/paths/detail/PaySteps.cpp @@ -62,6 +62,12 @@ checkNear(XRPAmount const& expected, XRPAmount const& actual) return expected == actual; }; +bool +checkNear(MPTAmount const& expected, MPTAmount const& actual) +{ + return expected == actual; +}; + static bool isXRPAccount(STPathElement const& pe) { @@ -75,13 +81,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()); } @@ -91,8 +97,17 @@ toStep( if (e1->isAccount() && e2->isAccount()) { + if (curAsset.holds()) + return make_MPTEndpointStep( + ctx, + e1->getAccountID(), + e2->getAccountID(), + curAsset.get().getMptID()); return make_DirectStepI( - ctx, e1->getAccountID(), e2->getAccountID(), curIssue.currency); + ctx, + e1->getAccountID(), + e2->getAccountID(), + curAsset.get().currency); } if (e1->isOffer() && e2->isAccount()) @@ -105,16 +120,16 @@ toStep( } assert( - (e2->getNodeType() & STPathElement::typeCurrency) || + (e2->getNodeType() & STPathElement::typeAsset) || (e2->getNodeType() & STPathElement::typeIssuer)); - 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{}}; @@ -122,13 +137,34 @@ toStep( assert(e2->isOffer()); - 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 @@ -136,9 +172,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, @@ -146,12 +182,17 @@ toStrand( beast::Journal j) { if (isXRP(src) || isXRP(dst) || !isConsistent(deliver) || - (sendMaxIssue && !isConsistent(*sendMaxIssue))) + (sendMaxAsset && !isConsistent(*sendMaxAsset))) return {temBAD_PATH, Strand{}}; - if ((sendMaxIssue && sendMaxIssue->account == noAccount()) || + if ((sendMaxAsset && sendMaxAsset->getIssuer() == noAccount()) || (src == noAccount()) || (dst == noAccount()) || - (deliver.account == noAccount())) + (deliver.getIssuer() == noAccount())) + return {temBAD_PATH, Strand{}}; + + if ((deliver.holds() && deliver.getIssuer() == beast::zero) || + (sendMaxAsset && sendMaxAsset->holds() && + sendMaxAsset->getIssuer() == beast::zero)) return {temBAD_PATH, Strand{}}; for (auto const& pe : path) @@ -164,6 +205,7 @@ toStrand( bool const hasAccount = t & STPathElement::typeAccount; bool const hasIssuer = t & STPathElement::typeIssuer; bool const hasCurrency = t & STPathElement::typeCurrency; + bool const hasMPT = t & STPathElement::typeMPT; if (hasAccount && (hasIssuer || hasCurrency)) return {temBAD_PATH, Strand{}}; @@ -183,18 +225,27 @@ toStrand( if (hasAccount && (pe.getAccountID() == noAccount())) return {temBAD_PATH, Strand{}}; + + if (hasMPT && (hasCurrency || hasAccount)) + return {temBAD_PATH, Strand{}}; + + if (hasMPT && hasIssuer && + (pe.getIssuerID() != getMPTIssuer(pe.getMPTID()))) + return {temBAD_PATH, Strand{}}; } - Issue curIssue = [&] { - auto const& currency = - sendMaxIssue ? sendMaxIssue->currency : deliver.currency; - if (isXRP(currency)) + Asset curAsset = [&]() -> Asset { + auto const& asset = sendMaxAsset ? *sendMaxAsset : deliver; + if (isXRP(asset)) return xrpIssue(); - return Issue{currency, src}; + if (asset.holds()) + return asset; + return Issue{asset.get().currency, src}; }(); - auto hasCurrency = [](STPathElement const pe) { - return pe.getNodeType() & STPathElement::typeCurrency; + // Currency or MPT + auto hasAsset = [](STPathElement const pe) { + return pe.getNodeType() & STPathElement::typeAsset; }; std::vector normPath; @@ -202,15 +253,26 @@ toStrand( // sendmax and deliver. normPath.reserve(4 + path.size()); { - normPath.emplace_back( - STPathElement::typeAll, src, curIssue.currency, curIssue.account); + // Implied step: sender of the transaction and either sendmax or deliver + // asset + auto const t = [&]() { + auto const t = + STPathElement::typeAccount | STPathElement::typeIssuer; + if (curAsset.holds()) + return t | STPathElement::typeMPT; + return t | STPathElement::typeCurrency; + }(); + normPath.emplace_back(t, src, curAsset, curAsset.getIssuer()); - if (sendMaxIssue && sendMaxIssue->account != src && + // If transaction includes sendmax with the issuer, which is not + // the sender then the issuer is the second implied step, unless + // the path starts at address, which is the issuer of sendmax + if (sendMaxAsset && sendMaxAsset->getIssuer() != src && (path.empty() || !path[0].isAccount() || - path[0].getAccountID() != sendMaxIssue->account)) + path[0].getAccountID() != sendMaxAsset->getIssuer())) { normPath.emplace_back( - sendMaxIssue->account, std::nullopt, std::nullopt); + sendMaxAsset->getIssuer(), std::nullopt, std::nullopt); } for (auto const& i : path) @@ -218,29 +280,34 @@ toStrand( { // Note that for offer crossing (only) we do use an offer book - // even if all that is changing is the Issue.account. - STPathElement const& lastCurrency = - *std::find_if(normPath.rbegin(), normPath.rend(), hasCurrency); - if ((lastCurrency.getCurrency() != deliver.currency) || + // even if all that is changing is the Issue/MPTIssue.account. + STPathElement const& lastAsset = + *std::find_if(normPath.rbegin(), normPath.rend(), hasAsset); + if (lastAsset.getPathAsset() != deliver || (offerCrossing && - lastCurrency.getIssuerID() != deliver.account)) + lastAsset.getIssuerID() != deliver.getIssuer())) { normPath.emplace_back( - std::nullopt, deliver.currency, deliver.account); + std::nullopt, deliver, deliver.getIssuer()); } } if (!((normPath.back().isAccount() && - normPath.back().getAccountID() == deliver.account) || - (dst == deliver.account))) + normPath.back().getAccountID() == deliver.getIssuer()) || + (dst == deliver.getIssuer()))) { - normPath.emplace_back(deliver.account, std::nullopt, std::nullopt); + normPath.emplace_back( + deliver.getIssuer(), + std::nullopt, + std::nullopt, + STPathElement::PathAssetTag{}); } if (!normPath.back().isAccount() || normPath.back().getAccountID() != dst) { - normPath.emplace_back(dst, std::nullopt, std::nullopt); + normPath.emplace_back( + dst, std::nullopt, std::nullopt, STPathElement::PathAssetTag{}); } } @@ -259,11 +326,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{ @@ -277,7 +344,7 @@ toStrand( ownerPaysTransferFee, offerCrossing, isDefaultPath, - seenDirectIssues, + seenDirectAssets, seenBookOuts, ammContext, j}; @@ -296,36 +363,61 @@ toStrand( auto cur = &normPath[i]; auto const next = &normPath[i + 1]; - if (cur->isAccount()) - curIssue.account = cur->getAccountID(); - else if (cur->hasIssuer()) - curIssue.account = cur->getIssuerID(); + // Switch over from MPT to Currency. + if (curAsset.holds() && cur->hasCurrency()) + curAsset = Issue{}; + + // Can only update the account for Issue since MPTIssue's account + // is immutable as it is part of MPTID + if (curAsset.holds()) + { + if (cur->isAccount()) + curAsset.get().account = cur->getAccountID(); + else if (cur->hasIssuer()) + curAsset.get().account = cur->getIssuerID(); + } if (cur->hasCurrency()) { - curIssue.currency = cur->getCurrency(); - if (isXRP(curIssue.currency)) - curIssue.account = xrpAccount(); + curAsset = Issue{cur->getCurrency(), curAsset.getIssuer()}; + if (isXRP(curAsset)) + curAsset.get().account = xrpAccount(); } + else if (cur->hasMPT()) + curAsset = cur->getPathAsset().get(); + + auto getImpliedStep = [&](AccountID const& src_, + AccountID const& dst_, + Asset const& asset_) { + if (asset_.holds()) + return make_MPTEndpointStep( + ctx(), src_, dst_, asset_.get().getMptID()); + return make_DirectStepI( + ctx(), src_, dst_, asset_.get().currency); + }; if (cur->isAccount() && next->isAccount()) { - if (!isXRP(curIssue.currency) && - curIssue.account != cur->getAccountID() && - curIssue.account != next->getAccountID()) + // TODO MPT This code never executes if curAsset is Currency + // since curAsset's account is set to cur's account above. + // It should not execute for MPT either because MPT rippling + // is invalid. Should this block be removed? + if (!isXRP(curAsset) && + curAsset.getIssuer() != cur->getAccountID() && + curAsset.getIssuer() != next->getAccountID()) { + if (curAsset.holds()) + Throw( + tefEXCEPTION, "MPT is invalid with rippling"); JLOG(j.trace()) << "Inserting implied account"; - auto msr = make_DirectStepI( - ctx(), - cur->getAccountID(), - curIssue.account, - curIssue.currency); + auto msr = getImpliedStep( + cur->getAccountID(), curAsset.getIssuer(), curAsset); if (msr.first != tesSUCCESS) return {msr.first, Strand{}}; result.push_back(std::move(msr.second)); impliedPE.emplace( STPathElement::typeAccount, - curIssue.account, + curAsset.getIssuer(), xrpCurrency(), xrpAccount()); cur = &*impliedPE; @@ -333,20 +425,21 @@ toStrand( } else if (cur->isAccount() && next->isOffer()) { - if (curIssue.account != cur->getAccountID()) + // TODO MPT Same as above. + if (curAsset.getIssuer() != cur->getAccountID()) { + if (curAsset.holds()) + Throw( + tefEXCEPTION, "MPT is invalid with rippling"); JLOG(j.trace()) << "Inserting implied account before offer"; - auto msr = make_DirectStepI( - ctx(), - cur->getAccountID(), - curIssue.account, - curIssue.currency); + auto msr = getImpliedStep( + cur->getAccountID(), curAsset.getIssuer(), curAsset); if (msr.first != tesSUCCESS) return {msr.first, Strand{}}; result.push_back(std::move(msr.second)); impliedPE.emplace( STPathElement::typeAccount, - curIssue.account, + curAsset.getIssuer(), xrpCurrency(), xrpAccount()); cur = &*impliedPE; @@ -354,10 +447,10 @@ toStrand( } else if (cur->isOffer() && next->isAccount()) { - if (curIssue.account != next->getAccountID() && + if (curAsset.getIssuer() != next->getAccountID() && !isXRP(next->getAccountID())) { - if (isXRP(curIssue)) + if (isXRP(curAsset)) { if (i != normPath.size() - 2) return {temBAD_PATH, Strand{}}; @@ -374,11 +467,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)); @@ -387,8 +477,8 @@ toStrand( continue; } - if (!next->isOffer() && next->hasCurrency() && - next->getCurrency() != curIssue.currency) + if (!next->isOffer() && next->hasAsset() && + next->getPathAsset() != curAsset) { // Should never happen assert(0); @@ -396,7 +486,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 @@ -411,19 +501,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) @@ -434,22 +525,31 @@ toStrand( if (auto const b = s->bookStepBook()) { - if (curIss != b->in) + if (curAsset != b->in) return false; - curIss = b->out; + curAsset = b->out; } - else + else if (curAsset.holds()) { - curIss.account = accts.second; + curAsset.get().account = accts.second; } curAcc = accts.second; } if (curAcc != dst) return false; - if (curIss.currency != deliver.currency) + if (curAsset.holds() != deliver.holds() || + (curAsset.holds() && + curAsset.get().currency != deliver.get().currency) || + (curAsset.holds() && + curAsset.get() != deliver.get())) + { + std::cout << to_string(curAsset) << std::endl; + std::cout << to_string(deliver) << std::endl; return false; - if (curIss.account != deliver.account && curIss.account != dst) + } + if (curAsset.getIssuer() != deliver.getIssuer() && + curAsset.getIssuer() != dst) return false; return true; }; @@ -469,9 +569,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, @@ -584,14 +684,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_) @@ -606,7 +706,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_) @@ -633,5 +733,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..3d893691585 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 @@ -362,8 +363,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 +379,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 +395,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 +434,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 +445,7 @@ toStrands( beast::Journal j); /// @cond INTERNAL -template +template struct StepImp : public Step { explicit StepImp() = default; @@ -517,6 +518,8 @@ bool checkNear(IOUAmount const& expected, IOUAmount const& actual); bool checkNear(XRPAmount const& expected, XRPAmount const& actual); +bool +checkNear(MPTAmount const& expected, MPTAmount const& actual); /// @endcond /** @@ -527,7 +530,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 +548,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 +564,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 +602,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 +621,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 329a4cc643f..4bdb7607eba 100644 --- a/src/xrpld/app/paths/detail/StrandFlow.h +++ b/src/xrpld/app/paths/detail/StrandFlow.h @@ -360,6 +360,15 @@ qualityUpperBound(ReadView const& v, Strand const& strand) * increases quality of AMM steps, increasing the strand's composite * quality as the result. */ +inline MPTAmount +limitOut( + ReadView const& v, + Strand const& strand, + MPTAmount const& remainingOut, + Quality const& limitQuality) +{ + return remainingOut; +} template inline TOutAmt limitOut( @@ -396,9 +405,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))) @@ -550,7 +561,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 ac88152c9dd..9ae58037206 100644 --- a/src/xrpld/app/paths/detail/XRPEndpointStep.cpp +++ b/src/xrpld/app/paths/detail/XRPEndpointStep.cpp @@ -202,7 +202,8 @@ class XRPEndpointOfferCrossingStep static std::int32_t computeReserveReduction(StrandContext const& ctx, AccountID const& acc) { - if (ctx.isFirst && !ctx.view.read(keylet::line(acc, ctx.strandDeliver))) + if (ctx.isFirst && ctx.strandDeliver.holds() && + !ctx.view.read(keylet::line(acc, ctx.strandDeliver.get()))) return -1; return 0; } @@ -310,9 +311,9 @@ XRPEndpointStep::validFwd( return {false, EitherAmount(XRPAmount(beast::zero))}; } - assert(in.native); + assert(in.native()); - auto const& xrpIn = in.xrp; + auto const& xrpIn = in.xrp(); auto const balance = static_cast(this)->xrpLiquid(sb); if (!isLast_ && balance < xrpIn) @@ -365,7 +366,7 @@ XRPEndpointStep::check(StrandContext const& ctx) const if (ctx.view.rules().enabled(fix1781)) { auto const issuesIndex = isLast_ ? 0 : 1; - if (!ctx.seenDirectIssues[issuesIndex].insert(xrpIssue()).second) + if (!ctx.seenDirectAssets[issuesIndex].insert(xrpIssue()).second) { JLOG(j_.debug()) << "XRPEndpointStep: loop detected: Index: " << ctx.strandSize diff --git a/src/xrpld/app/tx/detail/AMMBid.cpp b/src/xrpld/app/tx/detail/AMMBid.cpp index 7b5a7e002e2..e745654da59 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/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index ec71604858d..370d3373dff 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -38,6 +38,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 +55,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 +98,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 +156,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 +178,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; }; @@ -185,20 +191,45 @@ AMMCreate::preclaim(PreclaimContext const& ctx) } // Disallow AMM if the issuer has clawback 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; - return clawbackDisabled(amount2.issue()); + if (auto const ter = clawbackDisabled(amount2.asset()); ter != tesSUCCESS) + return ter; + + auto checkMPT = [&](Asset const& asset) { + if (asset.holds()) + return ctx.view.read(keylet::mptIssuance( + asset.get().getMptID())) != nullptr; + return true; + }; + if (!checkMPT(amount.asset()) || !checkMPT(amount2.asset())) + return tecOBJECT_NOT_FOUND; + + return tesSUCCESS; } static std::pair @@ -211,7 +242,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 { @@ -273,9 +304,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]); @@ -303,7 +334,29 @@ applyCreate( return {res, false}; } - auto sendAndTrustSet = [&](STAmount const& amount) -> TER { + auto sendAndInitTrustOrMPT = [&](STAmount const& amount) -> TER { + // Authorize MPT + if (amount.holds()) + { + auto const mptokenKey = + keylet::mptoken(amount.get().getMptID(), *ammAccount); + + auto const ownerNode = sb.dirInsert( + keylet::ownerDir(*ammAccount), + mptokenKey, + describeOwnerDir(*ammAccount)); + + if (!ownerNode) + return tecDIR_FULL; + + auto mptoken = std::make_shared(mptokenKey); + (*mptoken)[sfAccount] = *ammAccount; + (*mptoken)[sfMPTokenIssuanceID] = amount.get().getMptID(); + (*mptoken)[sfFlags] = 0; + (*mptoken)[sfOwnerNode] = *ownerNode; + sb.insert(mptoken); + } + if (auto const res = accountSend( sb, account_, @@ -312,8 +365,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())); @@ -326,11 +380,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; @@ -338,7 +393,7 @@ applyCreate( } // Send asset2. - res = sendAndTrustSet(amount2); + res = sendAndInitTrustOrMPT(amount2); if (res != tesSUCCESS) { JLOG(j_.debug()) << "AMM Instance: failed to send " << amount2; @@ -349,15 +404,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 2d4f9e72ffa..9d692381869 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -38,6 +38,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 +107,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 +156,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; @@ -162,6 +169,12 @@ AMMDeposit::preflight(PreflightContext const& ctx) return temBAD_FEE; } + if (lpTokens && lpTokens->holds()) + { + JLOG(ctx.j.debug()) << "AMM Deposit: invalid MPT LPTokens."; + return temMALFORMED; + } + return preflight2(ctx); } @@ -234,12 +247,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; @@ -257,17 +271,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, " @@ -275,11 +289,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) @@ -352,8 +366,8 @@ 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); @@ -494,12 +508,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; @@ -620,8 +635,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, @@ -680,7 +695,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; @@ -690,7 +705,7 @@ AMMDeposit::equalDepositLimit( ammAccount, amountBalance, amount, - toSTAmount(amount2Balance.issue(), amount2Deposit), + toSTAmount(amount2Balance.asset(), amount2Deposit), lptAMMBalance, tokens, std::nullopt, @@ -870,7 +885,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 31b3e8a04e7..f8c83b607fd 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 105f51af56c..d977153f57d 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -38,6 +38,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; @@ -101,18 +108,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; } @@ -152,6 +159,17 @@ AMMWithdraw::preflight(PreflightContext const& ctx) JLOG(ctx.j.debug()) << "AMM Withdraw: invalid EPrice"; return res; } + if (ePrice && ePrice->holds()) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: invalid MPT EPrice"; + return temMALFORMED; + } + } + + if (lpTokens && lpTokens->holds()) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: invalid MPT LPTokens"; + return temMALFORMED; } return preflight2(ctx); @@ -187,8 +205,8 @@ 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); @@ -219,15 +237,15 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) return tecAMM_BALANCE; } if (auto const ter = - requireAuth(ctx.view, amount->issue(), accountID)) + requireAuth(ctx.view, amount->asset(), accountID)) { JLOG(ctx.j.debug()) << "AMM Withdraw: account is not authorized, " - << amount->issue(); + << amount->asset(); return ter; } // AMM account or currency frozen - if (isFrozen(ctx.view, ammAccountID, amount->issue())) + if (isFrozen(ctx.view, ammAccountID, amount->asset())) { JLOG(ctx.j.debug()) << "AMM Withdraw: AMM account or currency is frozen, " @@ -235,11 +253,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; } } @@ -341,8 +359,8 @@ 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); @@ -411,11 +429,8 @@ AMMWithdraw::applyGuts(Sandbox& sb) bool updateBalance = true; if (newLPTokenBalance == beast::zero) { - if (auto const ter = deleteAMMAccount( - sb, - ctx_.tx[sfAsset].get(), - ctx_.tx[sfAsset2].get(), - j_); + if (auto const ter = + deleteAMMAccount(sb, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], j_); ter != tesSUCCESS && ter != tecINCOMPLETE) return {ter, false}; else @@ -468,7 +483,7 @@ AMMWithdraw::withdraw( auto const expected = ammHolds( view, *ammSle, - amountWithdraw.issue(), + amountWithdraw.asset(), std::nullopt, FreezeHandling::fhZERO_IF_FROZEN, AuthHandling::ahIGNORE_AUTH, @@ -637,9 +652,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 @@ -711,7 +726,7 @@ AMMWithdraw::equalWithdrawLimit( ammAccount, amountBalance, amount, - toSTAmount(amount2.issue(), amount2Withdraw), + toSTAmount(amount2.asset(), amount2Withdraw), lptAMMBalance, toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), tfee); @@ -722,7 +737,7 @@ AMMWithdraw::equalWithdrawLimit( view, ammAccount, amountBalance, - toSTAmount(amount.issue(), amountWithdraw), + toSTAmount(amount.asset(), amountWithdraw), amount2, lptAMMBalance, toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), @@ -837,7 +852,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( view, diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index c1e8e19ba9d..a29c401ca6d 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -323,7 +323,11 @@ CreateOffer::flowCross( if (!takerAmount.in.native() && !takerAmount.out.native()) { STPath path; - path.emplace_back(std::nullopt, xrpCurrency(), std::nullopt); + path.emplace_back( + std::nullopt, + xrpCurrency(), + std::nullopt, + STPathElement::PathAssetTag{}); paths.emplace_back(std::move(path)); } // Special handling for the tfSell flag. diff --git a/src/xrpld/app/tx/detail/CreateOffer.h b/src/xrpld/app/tx/detail/CreateOffer.h index 187fa3c8659..f5f26781f3a 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.h +++ b/src/xrpld/app/tx/detail/CreateOffer.h @@ -37,8 +37,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) + explicit CreateOffer(ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index 94451a61c88..fd92a5fe11e 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 a6f707ba561..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 - assert(0); -#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 b963195259a..328d8d85740 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); } @@ -96,63 +97,56 @@ 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 +// clang-format off +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)); } +// clang-format on static XRPAmount 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)); } +// clang-format off 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; @@ -177,7 +171,7 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const if constexpr (!inIsXRP && !outIsXRP) { - if (ofrAmts.in >= ofrAmts.out) + if (Number(ofrAmts.in) >= Number(ofrAmts.out)) return false; } @@ -185,7 +179,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. @@ -215,6 +209,7 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const Quality const effectiveQuality{effectiveAmounts}; return effectiveQuality < offer_.quality(); } +// clang-format on template bool @@ -277,8 +272,9 @@ TOfferStreamBase::step() view_, offer_.owner(), amount.out, - offer_.issueOut(), + offer_.assetOut(), fhZERO_IF_FROZEN, + ahZERO_IF_UNAUTHORIZED, j_); // Check for unfunded offer @@ -291,8 +287,9 @@ TOfferStreamBase::step() cancelView_, offer_.owner(), amount.out, - offer_.issueOut(), + offer_.assetOut(), fhZERO_IF_FROZEN, + ahZERO_IF_UNAUTHORIZED, j_); if (original_funds == *ownerFunds_) @@ -310,37 +307,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; } - assert(0); // xrp/xrp offer!?! should never happen - return false; + assert(0); + return ret; }(); if (rmSmallIncreasedQOffer) @@ -349,8 +357,9 @@ TOfferStreamBase::step() cancelView_, offer_.owner(), amount.out, - offer_.issueOut(), + offer_.assetOut(), fhZERO_IF_FROZEN, + ahZERO_IF_UNAUTHORIZED, j_); if (original_funds == *ownerFunds_) @@ -394,9 +403,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..9c3538c6abd 100644 --- a/src/xrpld/app/tx/detail/OfferStream.h +++ b/src/xrpld/app/tx/detail/OfferStream.h @@ -86,7 +86,7 @@ class TOfferStreamBase permRmOffer(uint256 const& offerIndex) = 0; template - bool + requires ValidTaker bool shouldRmSmallIncreasedQOffer() const; public: diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index ab0755572ef..e66c1cb07e1 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -104,12 +104,25 @@ isIndividualFrozen( return isIndividualFrozen(view, account, issue.currency, issue.account); } -[[nodiscard]] inline bool +[[nodiscard]] bool isIndividualFrozen( ReadView const& view, AccountID const& account, MPTIssue const& mpt); +[[nodiscard]] inline bool +isIndividualFrozen( + ReadView const& view, + AccountID const& account, + Asset const& asset) +{ + return std::visit( + [&](auto const& issue) { + return isIndividualFrozen(view, account, issue); + }, + asset.value()); +} + [[nodiscard]] bool isFrozen( ReadView const& view, @@ -126,6 +139,14 @@ isFrozen(ReadView const& view, AccountID const& account, Issue const& issue) [[nodiscard]] bool isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mpt); +[[nodiscard]] inline bool +isFrozen(ReadView const& view, AccountID const& account, Asset const& asset) +{ + return std::visit( + [&](auto const& issue) { return isFrozen(view, account, issue); }, + asset.value()); +} + // Returns the amount an account can spend without going into debt. // // <-- saAmount: amount of currency held by account. May be negative. @@ -177,6 +198,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 @@ -517,6 +547,17 @@ requireAuth( ReadView const& view, MPTIssue const& mpt, AccountID const& account); +[[nodiscard]] TER inline requireAuth( + ReadView const& view, + Asset const& asset, + AccountID const& account) +{ + return std::visit( + [&](TIss const& issue_) { + return requireAuth(view, issue_, account); + }, + asset.value()); +} /** Check if the destination account is allowed * to receive MPT. Return tecNO_AUTH if it doesn't diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 44679dd1e15..c4c6d180f9f 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -355,13 +355,15 @@ accountHolds( AuthHandling zeroIfUnauthorized, beast::Journal j) { - return invokeForAsset(issue, [&](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); - }); + 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 @@ -384,6 +386,22 @@ accountFunds( j); } +STAmount +accountFunds( + ReadView const& view, + AccountID const& id, + STAmount const& saDefault, + FreezeHandling freezeHandling, + AuthHandling authHandling, + beast::Journal j) +{ + if (!saDefault.native() && saDefault.getIssuer() == id) + return saDefault; + + return accountHolds( + view, id, saDefault.asset(), freezeHandling, authHandling, j); +} + // Prevent ownerCount from wrapping under error conditions. // // adjustment allows the ownerCount to be adjusted up or down in multiple steps. diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h b/src/xrpld/rpc/MPTokenIssuanceID.h index f7f45fded3b..4f94e61fa86 100644 --- a/src/xrpld/rpc/MPTokenIssuanceID.h +++ b/src/xrpld/rpc/MPTokenIssuanceID.h @@ -37,7 +37,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 8de1e8f3344..c2aa3eea02a 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.has_value()) response[jss::mpt_issuance_id] = to_string(result.value()); } diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index 87ec9ff03f4..874ac0925ac 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -33,7 +33,10 @@ #include #include #include + #include + +#include #include namespace ripple { @@ -1129,5 +1132,149 @@ getLedgerByContext(RPC::JsonContext& context) return RPC::make_error( rpcNOT_READY, "findCreate failed to return an inbound ledger"); } + +Expected +parseJSONBookAsset(Json::Value const& jv, std::string const& name) +{ + if (!jv.isMember(name)) + return Unexpected(JSONAssetError::MissingField); + auto const field = jv[name]; + if (field.isObjectOrNull()) + return Unexpected(JSONAssetError::NullObject); + if (field.isMember(jss::mpt_issuance_id) && + (field.isMember(jss::currency) || field.isMember(jss::issuer))) + return Unexpected(JSONAssetError::Malformed); + if (!field.isMember(jss::currency)) + return Unexpected(JSONAssetError::MissingFieldCurr); + + if (field.isMember(jss::currency)) + { + Issue issue = xrpIssue(); + // Parse mandatory currency. + if (!field[jss::currency].isString()) + return Unexpected(JSONAssetError::ExpectedFieldCurr); + if (!to_currency(issue.currency, field[jss::currency].asString())) + return Unexpected(JSONAssetError::MalformedCurr); + + // Parse optional issuer. + if (field.isMember(jss::issuer)) + { + if (!field[jss::issuer].isString()) + return Unexpected(JSONAssetError::ExpectedFieldIssr); + if (!to_issuer(issue.account, field[jss::issuer].asString())) + return Unexpected(JSONAssetError::MalformedIssr); + if (issue.account == noAccount()) + return Unexpected(JSONAssetError::NoIssuer); + if (isXRP(issue.currency) && !isXRP(issue.account)) + return Unexpected(JSONAssetError::UnneededIssuer); + if (!isXRP(issue.currency) && isXRP(issue.account)) + return Unexpected(JSONAssetError::BadIssuer); + } + + return issue; + } + else + { + MPTID u; + if (!u.parseHex(field.asString())) + return Unexpected(JSONAssetError::BadMPT); + return u; + } +} + +Json::Value +getSubscribeParseError( + RPC::JSONAssetError err, + std::string const& field, + error_code_i currError, + error_code_i issrError, + beast::Journal j) +{ + switch (err) + { + case RPC::JSONAssetError::MissingField: + case RPC::JSONAssetError::NullObject: + return rpcError(rpcINVALID_PARAMS); + case RPC::JSONAssetError::ExpectedFieldCurr: + case RPC::JSONAssetError::MissingFieldCurr: + case RPC::JSONAssetError::MalformedCurr: + case RPC::JSONAssetError::Malformed: { + JLOG(j.info()) << std::format("Bad {} currency.", field); + return rpcError(currError); + } + case RPC::JSONAssetError::ExpectedFieldIssr: + case RPC::JSONAssetError::MalformedIssr: + case RPC::JSONAssetError::NoIssuer: + case RPC::JSONAssetError::UnneededIssuer: + case RPC::JSONAssetError::BadIssuer: { + JLOG(j.info()) << std::format("Bad {} issuer.", field); + return rpcError(issrError); + case RPC::JSONAssetError::BadMPT: + return rpcError(currError); + } + } +} + +Json::Value +getBookOffersParseError( + RPC::JSONAssetError err, + std::string const& field, + error_code_i currError, + error_code_i issrError, + beast::Journal j) +{ + switch (err) + { + case RPC::JSONAssetError::MissingField: + return RPC::missing_field_error(field); + case RPC::JSONAssetError::NullObject: + return RPC::object_field_error(field); + case RPC::JSONAssetError::MissingFieldCurr: + case RPC::JSONAssetError::Malformed: + return RPC::missing_field_error(std::format("{}.currency", field)); + case RPC::JSONAssetError::ExpectedFieldCurr: + return RPC::expected_field_error( + std::format("{}.currency", field), "string"); + case RPC::JSONAssetError::MalformedCurr: { + JLOG(j.info()) << std::format("Bad {} currency.", field); + return RPC::make_error( + currError, + std::format( + "Invalid field '{}.currency', bad currency.", field)); + } + case RPC::JSONAssetError::ExpectedFieldIssr: + return RPC::expected_field_error( + std::format("{}.issuer", field), "string"); + case RPC::JSONAssetError::MalformedIssr: + return RPC::make_error( + issrError, + std::format("Invalid field '{}.issuer', bad issuer.", field)); + case RPC::JSONAssetError::NoIssuer: + return RPC::make_error( + issrError, + std::format( + "Invalid field '{}.issuer', bad issuer account one.", + field)); + case RPC::JSONAssetError::UnneededIssuer: + return RPC::make_error( + issrError, + std::format( + "Unneeded field '{}.issuer' for " + "XRP currency specification.", + field)); + case RPC::JSONAssetError::BadIssuer: + return RPC::make_error( + issrError, + std::format( + "Invalid field '{}.issuer', expected non-XRP issuer.", + field)); + case RPC::JSONAssetError::BadMPT: + return RPC::make_error( + issrError, + std::format( + "Invalid field '{}.mpt_issuance_id', bad MPTID.", field)); + } +} + } // namespace RPC } // namespace ripple diff --git a/src/xrpld/rpc/detail/RPCHelpers.h b/src/xrpld/rpc/detail/RPCHelpers.h index 54c426b17c3..349898c50f3 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.h +++ b/src/xrpld/rpc/detail/RPCHelpers.h @@ -268,6 +268,52 @@ keypairForSignature( Json::Value const& params, Json::Value& error, unsigned int apiVersion = apiVersionIfUnspecified); + +enum class JSONAssetError { + MissingField, + NullObject, + Malformed, + MissingFieldCurr, + ExpectedFieldCurr, + ExpectedFieldIssr, + MalformedCurr, + MalformedIssr, + NoIssuer, + UnneededIssuer, + BadIssuer, + BadMPT +}; + +Expected +parseJSONBookAsset(Json::Value const& field, std::string const& name); + +Json::Value +getSubscribeParseError( + RPC::JSONAssetError err, + std::string const& field, + error_code_i currError, + error_code_i issrError, + beast::Journal j); + +inline Json::Value +getUnsubscribeParseError( + RPC::JSONAssetError err, + std::string const& field, + error_code_i currError, + error_code_i issrError, + beast::Journal j) +{ + return getSubscribeParseError(err, field, currError, issrError, j); +} + +Json::Value +getBookOffersParseError( + RPC::JSONAssetError err, + std::string const& field, + error_code_i currError, + error_code_i issrError, + beast::Journal j); + } // namespace RPC } // namespace ripple diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 401f808f56a..95258a4d4b3 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -254,8 +254,8 @@ checkPayment( if (auto ledger = app.openLedger().current()) { Pathfinder pf( - std::make_shared( - ledger, app.journal("RippleLineCache")), + std::make_shared( + ledger, app.journal("AssetCache")), srcAddressID, *dstAccountID, sendMax.issue().currency, diff --git a/src/xrpld/rpc/handlers/BookOffers.cpp b/src/xrpld/rpc/handlers/BookOffers.cpp index 6126913a5b6..12e29b06dbe 100644 --- a/src/xrpld/rpc/handlers/BookOffers.cpp +++ b/src/xrpld/rpc/handlers/BookOffers.cpp @@ -47,118 +47,29 @@ doBookOffers(RPC::JsonContext& context) if (!lpLedger) return jvResult; - if (!context.params.isMember(jss::taker_pays)) - return RPC::missing_field_error(jss::taker_pays); - - if (!context.params.isMember(jss::taker_gets)) - return RPC::missing_field_error(jss::taker_gets); - - Json::Value const& taker_pays = context.params[jss::taker_pays]; - Json::Value const& taker_gets = context.params[jss::taker_gets]; - - if (!taker_pays.isObjectOrNull()) - return RPC::object_field_error(jss::taker_pays); - - if (!taker_gets.isObjectOrNull()) - return RPC::object_field_error(jss::taker_gets); - - if (!taker_pays.isMember(jss::currency)) - return RPC::missing_field_error("taker_pays.currency"); - - if (!taker_pays[jss::currency].isString()) - return RPC::expected_field_error("taker_pays.currency", "string"); - - if (!taker_gets.isMember(jss::currency)) - return RPC::missing_field_error("taker_gets.currency"); - - if (!taker_gets[jss::currency].isString()) - return RPC::expected_field_error("taker_gets.currency", "string"); - - Currency pay_currency; - - if (!to_currency(pay_currency, taker_pays[jss::currency].asString())) - { - JLOG(context.j.info()) << "Bad taker_pays currency."; - return RPC::make_error( + Book book; + + auto const takerPaysRes = + RPC::parseJSONBookAsset(context.params, jss::taker_pays.c_str()); + if (!takerPaysRes) + return getBookOffersParseError( + takerPaysRes.error(), + jss::taker_pays.c_str(), rpcSRC_CUR_MALFORMED, - "Invalid field 'taker_pays.currency', bad currency."); - } - - Currency get_currency; - - 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."); - } - - 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(); - } - - 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[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(); - } - - 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( + context.j); + book.in = *takerPaysRes; + + auto const takerGetsRes = + RPC::parseJSONBookAsset(context.params, jss::taker_gets.c_str()); + if (!takerGetsRes) + return getBookOffersParseError( + takerGetsRes.error(), + jss::taker_gets.c_str(), + rpcDST_AMT_MALFORMED, rpcDST_ISR_MALFORMED, - "Invalid field 'taker_gets.issuer', expected non-XRP issuer."); + context.j); + book.out = *takerGetsRes; std::optional takerID; if (context.params.isMember(jss::taker)) @@ -171,7 +82,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 +100,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/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index 30b1113cd05..b8f5ffe104a 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -651,7 +651,7 @@ doLedgerEntry(RPC::JsonContext& context) context.params[jss::mpt_issuance]; if (unparsedMPTIssuanceID.isString()) { - uint192 mptIssuanceID; + MPTID mptIssuanceID; if (!mptIssuanceID.parseHex(unparsedMPTIssuanceID.asString())) { uNodeIndex = beast::zero; @@ -691,7 +691,7 @@ doLedgerEntry(RPC::JsonContext& context) context.params[jss::mptoken][jss::mpt_issuance_id] .asString(); - uint192 mptIssuanceID; + MPTID mptIssuanceID; if (!mptIssuanceID.parseHex(mptIssuanceIdStr)) Throw( "Cannot parse mpt_issuance_id"); diff --git a/src/xrpld/rpc/handlers/Subscribe.cpp b/src/xrpld/rpc/handlers/Subscribe.cpp index 66fe89dea04..f63c6b8b096 100644 --- a/src/xrpld/rpc/handlers/Subscribe.cpp +++ b/src/xrpld/rpc/handlers/Subscribe.cpp @@ -234,62 +234,31 @@ doSubscribe(RPC::JsonContext& context) for (auto& j : context.params[jss::books]) { - if (!j.isObject() || !j.isMember(jss::taker_pays) || - !j.isMember(jss::taker_gets) || - !j[jss::taker_pays].isObjectOrNull() || - !j[jss::taker_gets].isObjectOrNull()) - return rpcError(rpcINVALID_PARAMS); - Book book; - 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())) - { - 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) - { - JLOG(context.j.info()) << "Bad taker_pays issuer."; - return rpcError(rpcSRC_ISR_MALFORMED); - } - - // Parse mandatory currency. - if (!taker_gets.isMember(jss::currency) || - !to_currency( - book.out.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) - { - JLOG(context.j.info()) << "Bad taker_gets issuer."; - return rpcError(rpcDST_ISR_MALFORMED); - } - if (book.in.currency == book.out.currency && - book.in.account == book.out.account) + auto const takerPaysRes = + RPC::parseJSONBookAsset(j, jss::taker_pays.c_str()); + if (!takerPaysRes) + return getSubscribeParseError( + takerPaysRes.error(), + jss::taker_pays.c_str(), + rpcSRC_CUR_MALFORMED, + rpcSRC_ISR_MALFORMED, + context.j); + book.in = *takerPaysRes; + + auto const takerGetsRes = + RPC::parseJSONBookAsset(j, jss::taker_gets.c_str()); + if (!takerGetsRes) + return getSubscribeParseError( + takerGetsRes.error(), + jss::taker_gets.c_str(), + rpcDST_AMT_MALFORMED, + rpcDST_ISR_MALFORMED, + context.j); + book.out = *takerGetsRes; + + 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..b3685a334e4 100644 --- a/src/xrpld/rpc/handlers/Unsubscribe.cpp +++ b/src/xrpld/rpc/handlers/Unsubscribe.cpp @@ -165,63 +165,29 @@ doUnsubscribe(RPC::JsonContext& context) for (auto& jv : context.params[jss::books]) { - if (!jv.isObject() || !jv.isMember(jss::taker_pays) || - !jv.isMember(jss::taker_gets) || - !jv[jss::taker_pays].isObjectOrNull() || - !jv[jss::taker_gets].isObjectOrNull()) - { - return rpcError(rpcINVALID_PARAMS); - } - - Json::Value taker_pays = jv[jss::taker_pays]; - Json::Value taker_gets = jv[jss::taker_gets]; - Book book; - // Parse mandatory currency. - if (!taker_pays.isMember(jss::currency) || - !to_currency( - book.in.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( - book.in.account, taker_pays[jss::issuer].asString()))) - // Don't allow illegal issuers. - || !isConsistent(book.in) || noAccount() == book.in.account) - { - JLOG(context.j.info()) << "Bad taker_pays issuer."; - - return rpcError(rpcSRC_ISR_MALFORMED); - } - - // Parse mandatory currency. - if (!taker_gets.isMember(jss::currency) || - !to_currency( - book.out.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( - book.out.account, taker_gets[jss::issuer].asString()))) - // Don't allow illegal issuers. - || !isConsistent(book.out) || noAccount() == book.out.account) - { - JLOG(context.j.info()) << "Bad taker_gets issuer."; - - return rpcError(rpcDST_ISR_MALFORMED); - } + auto const takerPaysRes = RPC::parseJSONBookAsset( + context.params, jss::taker_pays.c_str()); + if (!takerPaysRes) + return getUnsubscribeParseError( + takerPaysRes.error(), + jss::taker_pays.c_str(), + rpcSRC_CUR_MALFORMED, + rpcSRC_ISR_MALFORMED, + context.j); + book.in = *takerPaysRes; + + auto const takerGetsRes = RPC::parseJSONBookAsset( + context.params, jss::taker_gets.c_str()); + if (!takerGetsRes) + return getUnsubscribeParseError( + takerGetsRes.error(), + jss::taker_gets.c_str(), + rpcDST_AMT_MALFORMED, + rpcDST_ISR_MALFORMED, + context.j); + book.out = *takerGetsRes; if (book.in == book.out) {