From bf10f6e36ffef3ca7fd35af5dfa2a536c75d960d Mon Sep 17 00:00:00 2001 From: gregtatcam Date: Tue, 11 Feb 2025 17:57:26 +0000 Subject: [PATCH] Add MPT support to DEX: * Payment * OfferCreate * CheckCash * All AMM transactions * Pathfinding Retire FlowCross feature --- include/xrpl/protocol/AMMCore.h | 20 +- include/xrpl/protocol/AmountConversions.h | 100 +- include/xrpl/protocol/Asset.h | 88 +- include/xrpl/protocol/Book.h | 90 +- include/xrpl/protocol/Concepts.h | 66 + include/xrpl/protocol/Feature.h | 11 +- include/xrpl/protocol/Indexes.h | 2 +- include/xrpl/protocol/LedgerFormats.h | 1 + include/xrpl/protocol/MPTAmount.h | 14 + include/xrpl/protocol/MPTIssue.h | 41 +- include/xrpl/protocol/PathAsset.h | 149 ++ include/xrpl/protocol/STAmount.h | 21 +- include/xrpl/protocol/STObject.h | 2 + include/xrpl/protocol/STPathSet.h | 86 +- include/xrpl/protocol/TER.h | 1 + include/xrpl/protocol/detail/features.macro | 4 +- .../xrpl/protocol/detail/ledger_entries.macro | 2 + include/xrpl/protocol/detail/sfields.macro | 2 + .../xrpl/protocol/detail/transactions.macro | 48 +- include/xrpl/protocol/jss.h | 2 + src/libxrpl/protocol/AMMCore.cpp | 52 +- src/libxrpl/protocol/Asset.cpp | 10 + src/libxrpl/protocol/Feature.cpp | 3 +- src/libxrpl/protocol/Indexes.cpp | 89 +- src/libxrpl/protocol/MPTIssue.cpp | 13 + src/libxrpl/protocol/PathAsset.cpp | 39 + src/libxrpl/protocol/STAmount.cpp | 2 +- src/libxrpl/protocol/STObject.cpp | 6 + src/libxrpl/protocol/STParsedJSON.cpp | 94 +- src/libxrpl/protocol/STPathSet.cpp | 41 +- src/libxrpl/protocol/TER.cpp | 1 + src/test/app/AMMCalc_test.cpp | 19 +- src/test/app/AMMExtended_test.cpp | 8 +- src/test/app/AMM_test.cpp | 12 +- src/test/app/Check_test.cpp | 19 +- src/test/app/CrossingLimits_test.cpp | 52 +- src/test/app/DeliverMin_test.cpp | 1 - src/test/app/Discrepancy_test.cpp | 1 - src/test/app/Flow_test.cpp | 55 +- src/test/app/Freeze_test.cpp | 26 +- src/test/app/MPToken_test.cpp | 2120 +++++++++++++++-- src/test/app/NFToken_test.cpp | 7 +- src/test/app/Offer_test.cpp | 148 +- src/test/app/Path_test.cpp | 17 +- src/test/app/PayStrand_test.cpp | 37 +- src/test/app/ReducedOffer_test.cpp | 29 +- src/test/app/SetAuth_test.cpp | 1 - src/test/app/Taker_test.cpp | 1395 ----------- src/test/app/TheoreticalQuality_test.cpp | 6 +- src/test/app/TrustAndBalance_test.cpp | 3 +- src/test/app/XChain_test.cpp | 24 +- src/test/jtx/AMM.h | 32 +- src/test/jtx/PathSet.h | 14 +- src/test/jtx/TestHelpers.h | 63 + src/test/jtx/amount.h | 22 +- src/test/jtx/impl/AMM.cpp | 93 +- src/test/jtx/impl/AMMTest.cpp | 6 +- src/test/jtx/impl/TestHelpers.cpp | 196 +- src/test/jtx/impl/amount.cpp | 36 +- src/test/jtx/impl/balance.cpp | 27 +- src/test/jtx/impl/mpt.cpp | 24 +- src/test/jtx/impl/paths.cpp | 22 +- src/test/jtx/impl/xchain_bridge.cpp | 14 +- src/test/jtx/mpt.h | 8 + src/test/jtx/paths.h | 3 + src/test/ledger/BookDirs_test.cpp | 15 +- src/test/ledger/Directory_test.cpp | 4 +- src/test/ledger/PaymentSandbox_test.cpp | 21 +- src/test/protocol/STAmount_test.cpp | 10 +- src/test/protocol/STNumber_test.cpp | 2 +- src/test/rpc/AMMInfo_test.cpp | 44 +- src/test/rpc/Book_test.cpp | 5 +- src/test/rpc/GatewayBalances_test.cpp | 10 +- src/test/rpc/NoRipple_test.cpp | 1 - src/xrpld/app/ledger/AcceptedLedgerTx.cpp | 3 +- src/xrpld/app/ledger/OrderBookDB.cpp | 54 +- src/xrpld/app/ledger/OrderBookDB.h | 10 +- src/xrpld/app/misc/AMMHelpers.h | 37 +- src/xrpld/app/misc/AMMUtils.h | 22 +- src/xrpld/app/misc/MPTUtils.h | 52 + src/xrpld/app/misc/NetworkOPs.cpp | 23 +- src/xrpld/app/misc/detail/AMMHelpers.cpp | 14 +- src/xrpld/app/misc/detail/AMMUtils.cpp | 206 +- src/xrpld/app/misc/detail/MPTUtils.cpp | 105 + src/xrpld/app/paths/AMMLiquidity.h | 23 +- src/xrpld/app/paths/AMMOffer.h | 10 +- ...ccountCurrencies.cpp => AccountAssets.cpp} | 54 +- .../{AccountCurrencies.h => AccountAssets.h} | 14 +- .../{RippleLineCache.cpp => AssetCache.cpp} | 59 +- .../paths/{RippleLineCache.h => AssetCache.h} | 11 +- src/xrpld/app/paths/Credit.cpp | 4 +- src/xrpld/app/paths/Flow.cpp | 138 +- src/xrpld/app/paths/MPT.h | 68 + src/xrpld/app/paths/PathRequest.cpp | 227 +- 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 | 400 ++-- src/xrpld/app/paths/Pathfinder.h | 19 +- src/xrpld/app/paths/RippleCalc.cpp | 2 +- src/xrpld/app/paths/detail/AMMLiquidity.cpp | 38 +- src/xrpld/app/paths/detail/AMMOffer.cpp | 37 +- src/xrpld/app/paths/detail/AmountSpec.h | 229 +- src/xrpld/app/paths/detail/BookStep.cpp | 229 +- src/xrpld/app/paths/detail/DirectStep.cpp | 11 +- src/xrpld/app/paths/detail/FlowDebugInfo.h | 4 +- .../app/paths/detail/MPTEndpointStep.cpp | 987 ++++++++ src/xrpld/app/paths/detail/PathfinderUtils.h | 4 +- src/xrpld/app/paths/detail/PaySteps.cpp | 333 ++- src/xrpld/app/paths/detail/StepChecks.h | 5 +- src/xrpld/app/paths/detail/Steps.h | 64 +- src/xrpld/app/paths/detail/StrandFlow.h | 6 +- .../app/paths/detail/XRPEndpointStep.cpp | 10 +- src/xrpld/app/tx/detail/AMMBid.cpp | 22 +- src/xrpld/app/tx/detail/AMMClawback.cpp | 39 +- src/xrpld/app/tx/detail/AMMCreate.cpp | 147 +- src/xrpld/app/tx/detail/AMMDelete.cpp | 9 +- src/xrpld/app/tx/detail/AMMDeposit.cpp | 104 +- src/xrpld/app/tx/detail/AMMDeposit.h | 2 +- src/xrpld/app/tx/detail/AMMVote.cpp | 8 +- src/xrpld/app/tx/detail/AMMWithdraw.cpp | 172 +- src/xrpld/app/tx/detail/AMMWithdraw.h | 6 +- src/xrpld/app/tx/detail/CashCheck.cpp | 305 ++- src/xrpld/app/tx/detail/Clawback.cpp | 8 +- src/xrpld/app/tx/detail/CreateCheck.cpp | 84 +- src/xrpld/app/tx/detail/CreateOffer.cpp | 661 ++--- src/xrpld/app/tx/detail/CreateOffer.h | 60 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 106 +- src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 47 +- src/xrpld/app/tx/detail/MPTokenAuthorize.h | 9 +- .../app/tx/detail/NFTokenAcceptOffer.cpp | 10 +- src/xrpld/app/tx/detail/NFTokenUtils.cpp | 15 +- src/xrpld/app/tx/detail/Offer.h | 94 +- src/xrpld/app/tx/detail/OfferStream.cpp | 178 +- src/xrpld/app/tx/detail/OfferStream.h | 28 +- src/xrpld/app/tx/detail/Payment.cpp | 31 +- src/xrpld/app/tx/detail/SetTrust.cpp | 10 +- src/xrpld/app/tx/detail/Taker.cpp | 862 ------- src/xrpld/app/tx/detail/Taker.h | 342 --- src/xrpld/app/tx/detail/Transactor.cpp | 45 +- src/xrpld/app/tx/detail/XChainBridge.cpp | 26 +- src/xrpld/ledger/View.h | 61 +- src/xrpld/ledger/detail/PaymentSandbox.cpp | 6 +- src/xrpld/ledger/detail/View.cpp | 86 +- src/xrpld/rpc/BookChanges.h | 38 +- src/xrpld/rpc/MPTokenIssuanceID.h | 2 +- src/xrpld/rpc/detail/MPTokenIssuanceID.cpp | 4 +- src/xrpld/rpc/detail/RPCHelpers.cpp | 64 + src/xrpld/rpc/detail/RPCHelpers.h | 10 + src/xrpld/rpc/detail/TransactionSign.cpp | 19 +- src/xrpld/rpc/handlers/AMMInfo.cpp | 55 +- .../rpc/handlers/AccountCurrenciesHandler.cpp | 4 +- src/xrpld/rpc/handlers/AccountLines.cpp | 2 +- src/xrpld/rpc/handlers/BookOffers.cpp | 237 +- src/xrpld/rpc/handlers/GatewayBalances.cpp | 6 +- src/xrpld/rpc/handlers/NoRippleCheck.cpp | 2 +- src/xrpld/rpc/handlers/Subscribe.cpp | 55 +- src/xrpld/rpc/handlers/Unsubscribe.cpp | 54 +- 158 files changed, 7669 insertions(+), 5801 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 delete mode 100644 src/test/app/Taker_test.cpp create mode 100644 src/xrpld/app/misc/MPTUtils.h create mode 100644 src/xrpld/app/misc/detail/MPTUtils.cpp rename src/xrpld/app/paths/{AccountCurrencies.cpp => AccountAssets.cpp} (66%) rename src/xrpld/app/paths/{AccountCurrencies.h => AccountAssets.h} (85%) rename src/xrpld/app/paths/{RippleLineCache.cpp => AssetCache.cpp} (74%) rename src/xrpld/app/paths/{RippleLineCache.h => AssetCache.h} (93%) create mode 100644 src/xrpld/app/paths/MPT.h create mode 100644 src/xrpld/app/paths/detail/MPTEndpointStep.cpp delete mode 100644 src/xrpld/app/tx/detail/Taker.cpp delete mode 100644 src/xrpld/app/tx/detail/Taker.h diff --git a/include/xrpl/protocol/AMMCore.h b/include/xrpl/protocol/AMMCore.h index 32988af5fc7..a82f18e9894 100644 --- a/include/xrpl/protocol/AMMCore.h +++ b/include/xrpl/protocol/AMMCore.h @@ -22,7 +22,7 @@ #include #include -#include +#include #include #include @@ -59,14 +59,14 @@ ammAccountID( /** Calculate Liquidity Provider Token (LPT) Currency. */ Currency -ammLPTCurrency(Currency const& cur1, Currency const& cur2); +ammLPTCurrency(Asset const& asset1, Asset const& asset2); /** Calculate LPT Issue from AMM asset pair. */ Issue ammLPTIssue( - Currency const& cur1, - Currency const& cur2, + Asset const& asset1, + Asset const& asset2, AccountID const& ammAccountID); /** Validate the amount. @@ -77,19 +77,19 @@ ammLPTIssue( NotTEC invalidAMMAmount( STAmount const& amount, - std::optional> const& pair = std::nullopt, + std::optional> const& pair = std::nullopt, bool validZero = false); NotTEC invalidAMMAsset( - Issue const& issue, - std::optional> const& pair = std::nullopt); + Asset const& asset, + std::optional> const& pair = std::nullopt); NotTEC invalidAMMAssetPair( - Issue const& issue1, - Issue const& issue2, - std::optional> const& pair = std::nullopt); + Asset const& asset1, + Asset const& asset2, + std::optional> const& pair = std::nullopt); /** Get time slot of the auction slot. */ diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h index a65f7fcad8b..91950772632 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -21,6 +21,7 @@ #define RIPPLE_PROTOCOL_AMOUNTCONVERSION_H_INCLUDED #include +#include #include #include @@ -29,11 +30,12 @@ namespace ripple { inline STAmount -toSTAmount(IOUAmount const& iou, Issue const& iss) +toSTAmount(IOUAmount const& iou, Asset const& asset) { + XRPL_ASSERT(asset.holds(), "ripple::toSTAmount : is Issue"); bool const isNeg = iou.signum() < 0; std::uint64_t const umant = isNeg ? -iou.mantissa() : iou.mantissa(); - return STAmount(iss, umant, iou.exponent(), isNeg, STAmount::unchecked()); + return STAmount(asset, umant, iou.exponent(), isNeg, STAmount::unchecked()); } inline STAmount @@ -51,14 +53,25 @@ toSTAmount(XRPAmount const& xrp) } inline STAmount -toSTAmount(XRPAmount const& xrp, Issue const& iss) +toSTAmount(XRPAmount const& xrp, Asset const& asset) { - XRPL_ASSERT( - isXRP(iss.account) && isXRP(iss.currency), - "ripple::toSTAmount : is XRP"); + XRPL_ASSERT(isXRP(asset), "ripple::toSTAmount : is XRP"); return toSTAmount(xrp); } +inline STAmount +toSTAmount(MPTAmount const& mpt) +{ + return STAmount(mpt, noMPT()); +} + +inline STAmount +toSTAmount(MPTAmount const& mpt, Asset const& asset) +{ + XRPL_ASSERT(asset.holds(), "ripple::toSTAmount : is MPT"); + return STAmount(mpt, asset.get()); +} + template T toAmount(STAmount const& amt) = delete; @@ -100,6 +113,21 @@ toAmount(STAmount const& amt) return XRPAmount(sMant); } +template <> +inline MPTAmount +toAmount(STAmount const& amt) +{ + XRPL_ASSERT( + amt.holds() && amt.mantissa() <= maxMPTokenAmount && + amt.exponent() == 0, + "ripple::toAmount : maximum mantissa"); + bool const isNeg = amt.negative(); + std::int64_t const sMant = + isNeg ? -std::int64_t(amt.mantissa()) : amt.mantissa(); + + return MPTAmount(sMant); +} + template T toAmount(IOUAmount const& amt) = delete; @@ -122,26 +150,39 @@ 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& asset, Number const& n, Number::rounding_mode mode = Number::getround()) { saveNumberRoundMode rm(Number::getround()); - if (isXRP(issue)) + if (isXRP(asset)) Number::setround(mode); if constexpr (std::is_same_v) 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)) - return STAmount(issue, static_cast(n)); - return STAmount(issue, n.mantissa(), n.exponent()); + if (isXRP(asset)) + return STAmount(asset, static_cast(n)); + return STAmount(asset, n.mantissa(), n.exponent()); } else { @@ -152,18 +193,31 @@ toAmount( template T -toMaxAmount(Issue const& issue) +toMaxAmount(Asset const& asset) { 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); + }, + asset.value()); } else { @@ -174,23 +228,25 @@ toMaxAmount(Issue const& issue) inline STAmount toSTAmount( - Issue const& issue, + Asset const& asset, Number const& n, Number::rounding_mode mode = Number::getround()) { - return toAmount(issue, n, mode); + return toAmount(asset, n, mode); } template -Issue -getIssue(T const& amt) +Asset +getAsset(T const& amt) { if constexpr (std::is_same_v) return noIssue(); else if constexpr (std::is_same_v) return xrpIssue(); + else if constexpr (std::is_same_v) + return noMPT(); else if constexpr (std::is_same_v) - return amt.issue(); + return amt.asset(); else { constexpr bool alwaysFalse = !std::is_same_v; @@ -206,6 +262,8 @@ get(STAmount const& a) return a.iou(); else if constexpr (std::is_same_v) return a.xrp(); + else if constexpr (std::is_same_v) + return a.mpt(); else if constexpr (std::is_same_v) return a; else diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index 0d12cd40580..bf606b75775 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -21,21 +21,20 @@ #define RIPPLE_PROTOCOL_ASSET_H_INCLUDED #include +#include #include #include namespace ripple { -class Asset; - -template -concept ValidIssueType = - std::is_same_v || std::is_same_v; - -template -concept AssetType = - std::is_convertible_v || std::is_convertible_v || - std::is_convertible_v || std::is_convertible_v; +template + requires( + std::is_same_v || std::is_same_v || + std::is_same_v) +struct AmountType +{ + using amount_type = T; +}; /* Asset is an abstraction of three different issue types: XRP, IOU, MPT. * For historical reasons, two issue types XRP and IOU are wrapped in Issue @@ -46,6 +45,10 @@ class Asset { public: using value_type = std::variant; + using AmtType = std::variant< + AmountType, + AmountType, + AmountType>; private: value_type issue_; @@ -92,12 +95,15 @@ class Asset void setJson(Json::Value& jv) const; - bool + constexpr bool native() const { return holds() && get().native(); } + constexpr AmtType + getAmountType() const; + friend constexpr bool operator==(Asset const& lhs, Asset const& rhs); @@ -145,6 +151,22 @@ Asset::value() const return issue_; } +constexpr Asset::AmtType +Asset::getAmountType() const +{ + return std::visit( + [&](TIss const& issue) -> AmtType { + constexpr AmountType xrp; + constexpr AmountType iou; + constexpr AmountType mpt; + if constexpr (std::is_same_v) + return native() ? AmtType(xrp) : AmtType(iou); + else + return AmtType(mpt); + }, + issue_); +} + constexpr bool operator==(Asset const& lhs, Asset const& rhs) { @@ -222,6 +244,50 @@ assetFromJson(Json::Value const& jv); Json::Value to_json(Asset const& asset); +inline bool +isConsistent(Asset const& issue) +{ + return std::visit( + [&](TIss const& issue_) { + if constexpr (std::is_same_v) + return isConsistent(issue_); + else + return true; + }, + issue.value()); +} + +inline bool +validAsset(Asset const& issue) +{ + return std::visit( + [&](TIss const& issue_) { + if constexpr (std::is_same_v) + return isConsistent(issue_) && issue_.currency != badCurrency(); + else + return true; + }, + issue.value()); +} + +template +void +hash_append(Hasher& h, Asset const& r) +{ + using beast::hash_append; + std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + hash_append(h, issue); + else + hash_append(h, issue); + }, + r.value()); +} + +std::ostream& +operator<<(std::ostream& os, Asset const& x); + } // namespace ripple #endif // RIPPLE_PROTOCOL_ASSET_H_INCLUDED diff --git a/include/xrpl/protocol/Book.h b/include/xrpl/protocol/Book.h index 164a5ccfa99..58a78b9925d 100644 --- a/include/xrpl/protocol/Book.h +++ b/include/xrpl/protocol/Book.h @@ -21,7 +21,7 @@ #define RIPPLE_PROTOCOL_BOOK_H_INCLUDED #include -#include +#include #include namespace ripple { @@ -33,14 +33,14 @@ namespace ripple { class Book final : public CountedObject { public: - Issue in; - Issue out; + Asset in; + Asset out; Book() { } - Book(Issue const& in_, Issue const& out_) : in(in_), out(out_) + Book(Asset const& in_, Asset const& out_) : in(in_), out(out_) { } }; @@ -119,13 +119,77 @@ struct hash } }; +template <> +struct hash + : private boost::base_from_member, 0> +{ +private: + using id_hash_type = boost::base_from_member, 0>; + +public: + explicit hash() = default; + + using value_type = std::size_t; + using argument_type = ripple::MPTIssue; + + value_type + operator()(argument_type const& value) const + { + value_type result(id_hash_type::member(value.getMptID())); + return result; + } +}; + +template <> +struct hash + : private boost::base_from_member, 0>, + private boost::base_from_member, 1>, + private boost::base_from_member, 2> +{ +private: + using currency_hash_type = + boost::base_from_member, 0>; + using issuer_hash_type = + boost::base_from_member, 1>; + using mpt_hash_type = boost::base_from_member, 2>; + +public: + explicit hash() = default; + + using value_type = std::size_t; + using argument_type = ripple::Asset; + + value_type + operator()(argument_type const& asset) 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; + } + }, + asset.value()); + } +}; + //------------------------------------------------------------------------------ template <> struct hash { private: - using hasher = std::hash; + using hasher = std::hash; hasher m_hasher; @@ -160,6 +224,22 @@ struct hash : std::hash // using Base::Base; // inherit ctors }; +template <> +struct hash : std::hash +{ + explicit hash() = default; + + using Base = std::hash; +}; + +template <> +struct hash : std::hash +{ + explicit hash() = default; + + using Base = std::hash; +}; + template <> struct hash : std::hash { diff --git a/include/xrpl/protocol/Concepts.h b/include/xrpl/protocol/Concepts.h new file mode 100644 index 00000000000..dba32e4ce2a --- /dev/null +++ b/include/xrpl/protocol/Concepts.h @@ -0,0 +1,66 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 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; + +template +concept StepAmount = std::is_same_v || + std::is_same_v || 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 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)); + +} // namespace ripple + +#endif // RIPPLE_PROTOCOL_CONCEPTS_H_INCLUDED diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 1c476df617f..d490c0466af 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -52,11 +52,12 @@ * When a feature has been enabled for several years, the conditional code * may be removed, and the feature "retired". To retire a feature: * 1) Remove the uint256 declaration from this file. - * 2) MOVE the uint256 definition in Feature.cpp to the "retired features" - * section at the end of the file. + * 2) MOVE the uint256 definition in features.macro to the "retired features" + * section at the end of Feature.cpp, which calls retireFeature(). + * retireFeature() registers the retired feature as Supported::yes with + * VoteBehavior::Obsolete. * 3) CHANGE the name of the variable to start with "retired". - * 4) CHANGE the parameters of the `registerFeature` call to `Supported::yes` - * and `VoteBehavior::DefaultNo`. + * * The feature must remain registered and supported indefinitely because it * still exists in the ledger, but there is no need to vote for it because * there's nothing to vote for. If it is removed completely from the code, any @@ -80,7 +81,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 88; +static constexpr std::size_t numFeatures = 90; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index bbed5395927..6796cf2268d 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -310,7 +310,7 @@ Keylet mptIssuance(std::uint32_t seq, AccountID const& issuer) noexcept; Keylet -mptIssuance(MPTID const& issuanceID) noexcept; +mptIssuance(MPTIssue const& mptIssue) noexcept; inline Keylet mptIssuance(uint256 const& issuanceKey) diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 5f3cca53ac8..af70ff66fc8 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -188,6 +188,7 @@ enum LedgerSpecificFlags { // ltMPTOKEN lsfMPTAuthorized = 0x00000002, + lsfMPTAMM = 0x00000004, // ltCREDENTIAL lsfAccepted = 0x00010000, diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index 244d6839156..a6bcc3bec8b 100644 --- a/include/xrpl/protocol/MPTAmount.h +++ b/include/xrpl/protocol/MPTAmount.h @@ -50,6 +50,7 @@ class MPTAmount : private boost::totally_ordered, public: MPTAmount() = default; constexpr MPTAmount(MPTAmount const& other) = default; + constexpr MPTAmount(beast::Zero); constexpr MPTAmount& operator=(MPTAmount const& other) = default; @@ -108,6 +109,11 @@ constexpr MPTAmount::MPTAmount(value_type value) : value_(value) { } +constexpr MPTAmount::MPTAmount(beast::Zero) +{ + *this = beast::zero; +} + constexpr MPTAmount& MPTAmount::operator=(beast::Zero) { @@ -138,6 +144,14 @@ MPTAmount::value() const return value_; } +// Output MPTAmount as just the value. +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const MPTAmount& q) +{ + return os << q.value(); +} + inline std::string to_string(MPTAmount const& amount) { diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index 028051ab1ae..637b401c62d 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -37,7 +37,9 @@ class MPTIssue public: MPTIssue() = default; - explicit MPTIssue(MPTID const& issuanceID); + MPTIssue(MPTID const& issuanceID); + + MPTIssue(std::uint32_t sequence, AccountID const& account); AccountID const& getIssuer() const; @@ -84,6 +86,30 @@ isXRP(MPTID const&) return false; } +inline AccountID const& +getMPTIssuer(MPTID const& mptid) +{ + static_assert(sizeof(MPTID) == (sizeof(std::uint32_t) + sizeof(AccountID))); + AccountID const* accountId = reinterpret_cast( + mptid.data() + sizeof(std::uint32_t)); + return *accountId; +} + +inline MPTID +noMPT() +{ + static MPTIssue mpt{0, noAccount()}; + return mpt.getMptID(); +} + +template +void +hash_append(Hasher& h, MPTIssue const& r) +{ + using beast::hash_append; + hash_append(h, r.getMptID()); +} + Json::Value to_json(MPTIssue const& mptIssue); @@ -93,6 +119,19 @@ to_string(MPTIssue const& mptIssue); MPTIssue mptIssueFromJson(Json::Value const& jv); +std::ostream& +operator<<(std::ostream& os, MPTIssue const& x); + } // namespace ripple +namespace std { + +template <> +struct hash : ripple::MPTID::hasher +{ + explicit hash() = default; +}; + +} // namespace std + #endif // RIPPLE_PROTOCOL_MPTISSUE_H_INCLUDED diff --git a/include/xrpl/protocol/PathAsset.h b/include/xrpl/protocol/PathAsset.h new file mode 100644 index 00000000000..50f32f23f2a --- /dev/null +++ b/include/xrpl/protocol/PathAsset.h @@ -0,0 +1,149 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_PATHASSET_H_INCLUDED +#define RIPPLE_APP_PATHASSET_H_INCLUDED + +#include +#include + +namespace ripple { + +/* Represent STPathElement's asset, which can be Currency or MPTID. + */ +class PathAsset +{ +private: + std::variant easset_; + +public: + PathAsset() = default; + // Enables comparing Asset and PathAsset + PathAsset(Asset const& asset); + PathAsset(Currency const& currency) : easset_(currency) + { + } + PathAsset(MPTID const& mpt) : easset_(mpt) + { + } + + template + constexpr bool + holds() const; + + constexpr bool + isXRP() const; + + template + T const& + get() const; + + constexpr std::variant const& + value() const; + + friend constexpr bool + operator==(PathAsset const& lhs, PathAsset const& rhs); +}; + +inline PathAsset::PathAsset(Asset const& asset) +{ + std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + easset_ = issue.currency; + else + easset_ = issue.getMptID(); + }, + asset.value()); +} + +template +constexpr bool +PathAsset::holds() const +{ + return std::holds_alternative(easset_); +} + +template +T const& +PathAsset::get() const +{ + if (!holds()) + Throw("PathAsset doesn't hold requested asset."); + return std::get(easset_); +} + +constexpr std::variant const& +PathAsset::value() const +{ + return easset_; +} + +constexpr bool +PathAsset::isXRP() const +{ + return std::visit( + [&](A const& a) { + if constexpr (std::is_same_v) + return ripple::isXRP(a); + else + return false; + }, + easset_); +} + +constexpr bool +operator==(PathAsset const& lhs, PathAsset const& rhs) +{ + return std::visit( + []( + TLhs const& lhs_, TRhs const& rhs_) { + if constexpr (std::is_same_v) + return lhs_ == rhs_; + else + return false; + }, + lhs.value(), + rhs.value()); +} + +template +void +hash_append(Hasher& h, PathAsset const& pathAsset) +{ + std::visit( + [&](T const& e) { hash_append(h, e); }, + pathAsset.value()); +} + +inline bool +isXRP(PathAsset const& asset) +{ + return asset.isXRP(); +} + +std::string +to_string(PathAsset const& asset); + +std::ostream& +operator<<(std::ostream& os, PathAsset const& x); + +} // namespace ripple + +#endif // RIPPLE_APP_PATHASSET_H_INCLUDED diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 23e4c5e5b59..3a1d7595dcd 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -188,13 +188,6 @@ class STAmount final : public STBase, public CountedObject constexpr TIss const& get() const; - Issue const& - issue() const; - - // These three are deprecated - Currency const& - getCurrency() const; - AccountID const& getIssuer() const; @@ -483,18 +476,6 @@ STAmount::get() const return mAsset.get(); } -inline Issue const& -STAmount::issue() const -{ - return get(); -} - -inline Currency const& -STAmount::getCurrency() const -{ - return mAsset.get().currency; -} - inline AccountID const& STAmount::getIssuer() const { @@ -568,6 +549,8 @@ STAmount::clear(Asset const& asset) inline void STAmount::setIssuer(AccountID const& uIssuer) { + if (!mAsset.holds()) + Throw("Can't set issuer for non-Issue"); mAsset.get().account = uIssuer; } diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index 4c8db2e01e4..4bcbe7dde33 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -362,6 +362,8 @@ class STObject : public STBase, public CountedObject void setFieldH128(SField const& field, uint128 const&); void + setFieldH192(SField const& field, uint192 const&); + void setFieldH256(SField const& field, uint256 const&); void setFieldVL(SField const& field, Blob const&); diff --git a/include/xrpl/protocol/STPathSet.h b/include/xrpl/protocol/STPathSet.h index 953a209c150..ecbeff62348 100644 --- a/include/xrpl/protocol/STPathSet.h +++ b/include/xrpl/protocol/STPathSet.h @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include #include #include @@ -35,7 +37,7 @@ class STPathElement final : public CountedObject { unsigned int mType; AccountID mAccountID; - Currency mCurrencyID; + PathAsset mAssetID; AccountID mIssuerID; bool is_offer_; @@ -48,8 +50,10 @@ class STPathElement final : public CountedObject 0x01, // Rippling through an account (vs taking an offer). typeCurrency = 0x10, // Currency follows. typeIssuer = 0x20, // Issuer follows. + typeMPT = 0x40, // MPT follows. typeBoundary = 0xFF, // Boundary between alternate paths. - typeAll = typeAccount | typeCurrency | typeIssuer, + typeAsset = typeCurrency | typeMPT, + typeAll = typeAccount | typeCurrency | typeIssuer | typeMPT, // Combination of all types. }; @@ -60,19 +64,19 @@ class STPathElement final : public CountedObject STPathElement( std::optional const& account, - std::optional const& currency, + std::optional const& asset, std::optional const& issuer); STPathElement( AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer, - bool forceCurrency = false); + bool forceAsset = false); STPathElement( unsigned int uType, AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer); auto @@ -90,6 +94,12 @@ class STPathElement final : public CountedObject bool hasCurrency() const; + bool + hasMPT() const; + + bool + hasAsset() const; + bool isNone() const; @@ -98,9 +108,15 @@ class STPathElement final : public CountedObject AccountID const& getAccountID() const; + PathAsset const& + getPathAsset() const; + Currency const& getCurrency() const; + MPTID const& + getMPTID() const; + AccountID const& getIssuerID() const; @@ -140,7 +156,7 @@ class STPath final : public CountedObject bool hasSeen( AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer) const; Json::Value getJson(JsonOptions) const; @@ -244,7 +260,7 @@ inline STPathElement::STPathElement() : mType(typeNone), is_offer_(true) inline STPathElement::STPathElement( std::optional const& account, - std::optional const& currency, + std::optional const& asset, std::optional const& issuer) : mType(typeNone) { @@ -262,10 +278,10 @@ inline STPathElement::STPathElement( "ripple::STPathElement::STPathElement : account is set"); } - if (currency) + if (asset) { - mCurrencyID = *currency; - mType |= typeCurrency; + mAssetID = *asset; + mType |= mAssetID.holds() ? typeCurrency : typeMPT; } if (issuer) @@ -282,20 +298,20 @@ inline STPathElement::STPathElement( inline STPathElement::STPathElement( AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer, - bool forceCurrency) + bool forceAsset) : mType(typeNone) , mAccountID(account) - , mCurrencyID(currency) + , mAssetID(asset) , mIssuerID(issuer) , is_offer_(isXRP(mAccountID)) { if (!is_offer_) mType |= typeAccount; - if (forceCurrency || !isXRP(currency)) - mType |= typeCurrency; + if (forceAsset || !isXRP(mAssetID)) + mType |= asset.holds() ? typeCurrency : typeMPT; if (!isXRP(issuer)) mType |= typeIssuer; @@ -306,14 +322,20 @@ inline STPathElement::STPathElement( inline STPathElement::STPathElement( unsigned int uType, AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer) : mType(uType) , mAccountID(account) - , mCurrencyID(currency) + , mAssetID(asset) , mIssuerID(issuer) , is_offer_(isXRP(mAccountID)) { + // uType could be assetType; i.e. either Currency or MPTID. + // Get the actual type. + if (!asset.holds()) + mType = mType & (~Type::typeMPT); + else if (mAssetID.holds() && isXRP(mAssetID.get())) + mType = mType & (~Type::typeCurrency); hash_value_ = get_hash(*this); } @@ -347,6 +369,18 @@ STPathElement::hasCurrency() const return getNodeType() & STPathElement::typeCurrency; } +inline bool +STPathElement::hasMPT() const +{ + return getNodeType() & STPathElement::typeMPT; +} + +inline bool +STPathElement::hasAsset() const +{ + return getNodeType() & STPathElement::typeAsset; +} + inline bool STPathElement::isNone() const { @@ -361,10 +395,22 @@ STPathElement::getAccountID() const return mAccountID; } +inline PathAsset const& +STPathElement::getPathAsset() const +{ + return mAssetID; +} + inline Currency const& STPathElement::getCurrency() const { - return mCurrencyID; + return mAssetID.get(); +} + +inline MPTID const& +STPathElement::getMPTID() const +{ + return mAssetID.get(); } inline AccountID const& @@ -378,7 +424,7 @@ STPathElement::operator==(const STPathElement& t) const { return (mType & typeAccount) == (t.mType & typeAccount) && hash_value_ == t.hash_value_ && mAccountID == t.mAccountID && - mCurrencyID == t.mCurrencyID && mIssuerID == t.mIssuerID; + mAssetID == t.mAssetID && mIssuerID == t.mIssuerID; } inline bool diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 317e9c2c978..ff7514b9aa9 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -92,6 +92,7 @@ enum TEMcodes : TERUnderlyingType { temBAD_FEE, temBAD_ISSUER, temBAD_LIMIT, + temBAD_MPT, temBAD_OFFER, temBAD_PATH, temBAD_PATH_LOOP, diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index aa0782b1378..b8896f2f7fc 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -29,6 +29,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(MPTokensV2, Supported::yes, VoteBehavior::DefaultNo) // Check flags in Credential transactions XRPL_FIX (InvalidTxFlags, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (FrozenLPTokenTransfer, Supported::yes, VoteBehavior::DefaultNo) @@ -83,7 +84,7 @@ XRPL_FEATURE(HardenedValidations, Supported::yes, VoteBehavior::DefaultYe // fix1781: XRPEndpointSteps should be included in the circular payment check XRPL_FIX (1781, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(RequireFullyCanonicalSig, Supported::yes, VoteBehavior::DefaultYes) -// fixQualityUpperBound should be activated before FlowCross +// fixQualityUpperBound should be activated before FlowCross(retired) XRPL_FIX (QualityUpperBound, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DeletableAccounts, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (PayChanRecipientOwnerDir, Supported::yes, VoteBehavior::DefaultYes) @@ -101,7 +102,6 @@ XRPL_FIX (1571, Supported::yes, VoteBehavior::DefaultYe XRPL_FEATURE(Checks, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DepositAuth, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (1513, Supported::yes, VoteBehavior::DefaultYes) -XRPL_FEATURE(FlowCross, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(Flow, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(OwnerPaysFee, Supported::no, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 5a652baf4f7..78939e2d4c9 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -177,8 +177,10 @@ LEDGER_ENTRY(ltDIR_NODE, 0x0064, DirectoryNode, directory, ({ {sfOwner, soeOPTIONAL}, // for owner directories {sfTakerPaysCurrency, soeOPTIONAL}, // order book directories {sfTakerPaysIssuer, soeOPTIONAL}, // order book directories + {sfTakerPaysMPT, soeOPTIONAL}, // order book directories {sfTakerGetsCurrency, soeOPTIONAL}, // order book directories {sfTakerGetsIssuer, soeOPTIONAL}, // order book directories + {sfTakerGetsMPT, soeOPTIONAL}, // order book directories {sfExchangeRate, soeOPTIONAL}, // order book directories {sfIndexes, soeREQUIRED}, {sfRootIndex, soeREQUIRED}, diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 3217bab9134..d01b92e25a0 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -154,6 +154,8 @@ TYPED_SFIELD(sfTakerGetsIssuer, UINT160, 4) // 192-bit (common) TYPED_SFIELD(sfMPTokenIssuanceID, UINT192, 1) +TYPED_SFIELD(sfTakerPaysMPT, UINT192, 2) +TYPED_SFIELD(sfTakerGetsMPT, UINT192, 3) // 256-bit (common) TYPED_SFIELD(sfLedgerHash, UINT256, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index dd3ac42325d..88559cc53e6 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -89,8 +89,8 @@ TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, ({ /** This transaction type creates an offer to trade one asset for another. */ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, ({ - {sfTakerPays, soeREQUIRED}, - {sfTakerGets, soeREQUIRED}, + {sfTakerPays, soeREQUIRED, soeMPTSupported}, + {sfTakerGets, soeREQUIRED, soeMPTSupported}, {sfExpiration, soeOPTIONAL}, {sfOfferSequence, soeOPTIONAL}, })) @@ -147,7 +147,7 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, ({ /** This transaction type creates a new check. */ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({ {sfDestination, soeREQUIRED}, - {sfSendMax, soeREQUIRED}, + {sfSendMax, soeREQUIRED, soeMPTSupported}, {sfExpiration, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, {sfInvoiceID, soeOPTIONAL}, @@ -156,8 +156,8 @@ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({ /** This transaction type cashes an existing check. */ TRANSACTION(ttCHECK_CASH, 17, CheckCash, ({ {sfCheckID, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, - {sfDeliverMin, soeOPTIONAL}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, + {sfDeliverMin, soeOPTIONAL, soeMPTSupported}, })) /** This transaction type cancels an existing check. */ @@ -236,24 +236,24 @@ TRANSACTION(ttCLAWBACK, 30, Clawback, ({ /** This transaction claws back tokens from an AMM pool. */ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, ({ {sfHolder, soeREQUIRED}, - {sfAsset, soeREQUIRED}, - {sfAsset2, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAsset2, soeREQUIRED, soeMPTSupported}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, })) /** This transaction type creates an AMM instance */ TRANSACTION(ttAMM_CREATE, 35, AMMCreate, ({ - {sfAmount, soeREQUIRED}, - {sfAmount2, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, + {sfAmount2, soeREQUIRED, soeMPTSupported}, {sfTradingFee, soeREQUIRED}, })) /** This transaction type deposits into an AMM instance */ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, ({ - {sfAsset, soeREQUIRED}, - {sfAsset2, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, - {sfAmount2, soeOPTIONAL}, + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAsset2, soeREQUIRED, soeMPTSupported}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, + {sfAmount2, soeOPTIONAL, soeMPTSupported}, {sfEPrice, soeOPTIONAL}, {sfLPTokenOut, soeOPTIONAL}, {sfTradingFee, soeOPTIONAL}, @@ -261,25 +261,25 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, ({ /** This transaction type withdraws from an AMM instance */ TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, ({ - {sfAsset, soeREQUIRED}, - {sfAsset2, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, - {sfAmount2, soeOPTIONAL}, + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAsset2, soeREQUIRED, soeMPTSupported}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, + {sfAmount2, soeOPTIONAL, soeMPTSupported}, {sfEPrice, soeOPTIONAL}, {sfLPTokenIn, soeOPTIONAL}, })) /** This transaction type votes for the trading fee */ TRANSACTION(ttAMM_VOTE, 38, AMMVote, ({ - {sfAsset, soeREQUIRED}, - {sfAsset2, soeREQUIRED}, + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAsset2, soeREQUIRED, soeMPTSupported}, {sfTradingFee, soeREQUIRED}, })) /** This transaction type bids for the auction slot */ TRANSACTION(ttAMM_BID, 39, AMMBid, ({ - {sfAsset, soeREQUIRED}, - {sfAsset2, soeREQUIRED}, + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAsset2, soeREQUIRED, soeMPTSupported}, {sfBidMin, soeOPTIONAL}, {sfBidMax, soeOPTIONAL}, {sfAuthAccounts, soeOPTIONAL}, @@ -287,8 +287,8 @@ TRANSACTION(ttAMM_BID, 39, AMMBid, ({ /** This transaction type deletes AMM in the empty state */ TRANSACTION(ttAMM_DELETE, 40, AMMDelete, ({ - {sfAsset, soeREQUIRED}, - {sfAsset2, soeREQUIRED}, + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAsset2, soeREQUIRED, soeMPTSupported}, })) /** This transactions creates a crosschain sequence number */ diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 483b69a962f..725674e2bb5 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -425,6 +425,8 @@ JSS(min_ledger); // in: LedgerCleaner JSS(minimum_fee); // out: TxQ JSS(minimum_level); // out: TxQ JSS(missingCommand); // error +JSS(mpt_issuance_id_a); // out: BookChanges +JSS(mpt_issuance_id_b); // out: BookChanges JSS(name); // out: AmendmentTableImpl, PeerImp JSS(needed_state_hashes); // out: InboundLedger JSS(needed_transaction_hashes); // out: InboundLedger diff --git a/src/libxrpl/protocol/AMMCore.cpp b/src/libxrpl/protocol/AMMCore.cpp index 3bebfc4659a..cb6ffa29607 100644 --- a/src/libxrpl/protocol/AMMCore.cpp +++ b/src/libxrpl/protocol/AMMCore.cpp @@ -39,12 +39,23 @@ ammAccountID( } Currency -ammLPTCurrency(Currency const& cur1, Currency const& cur2) +ammLPTCurrency(Asset const& asset1, Asset const& asset2) { // AMM LPToken is 0x03 plus 19 bytes of the hash std::int32_t constexpr AMMCurrencyCode = 0x03; - auto const [minC, maxC] = std::minmax(cur1, cur2); - auto const hash = sha512Half(minC, maxC); + auto const [minI, maxI] = std::minmax(asset1, asset2); + uint256 const hash = std::visit( + [](auto&& issue1, auto&& issue2) { + auto fromIss = [](T const& issue) { + if constexpr (std::is_same_v) + return issue.currency; + if constexpr (std::is_same_v) + return issue.getMptID(); + }; + return sha512Half(fromIss(issue1), fromIss(issue2)); + }, + minI.value(), + maxI.value()); Currency currency; *currency.begin() = AMMCurrencyCode; std::copy( @@ -54,38 +65,41 @@ ammLPTCurrency(Currency const& cur1, Currency const& cur2) Issue ammLPTIssue( - Currency const& cur1, - Currency const& cur2, + Asset const& asset1, + Asset const& asset2, AccountID const& ammAccountID) { - return Issue(ammLPTCurrency(cur1, cur2), ammAccountID); + return Issue(ammLPTCurrency(asset1, asset2), ammAccountID); } NotTEC invalidAMMAsset( - Issue const& issue, - std::optional> const& pair) + Asset const& asset, + std::optional> const& pair) { - if (badCurrency() == issue.currency) + if (asset.holds() && + asset.get().getIssuer() == beast::zero) + return temBAD_MPT; + if (asset.holds() && badCurrency() == asset.get().currency) return temBAD_CURRENCY; - if (isXRP(issue) && issue.account.isNonZero()) + if (isXRP(asset) && asset.getIssuer().isNonZero()) return temBAD_ISSUER; - if (pair && issue != pair->first && issue != pair->second) + if (pair && asset != pair->first && asset != pair->second) return temBAD_AMM_TOKENS; return tesSUCCESS; } NotTEC invalidAMMAssetPair( - Issue const& issue1, - Issue const& issue2, - std::optional> const& pair) + Asset const& asset1, + Asset const& asset2, + std::optional> const& pair) { - if (issue1 == issue2) + if (asset1 == asset2) return temBAD_AMM_TOKENS; - if (auto const res = invalidAMMAsset(issue1, pair)) + if (auto const res = invalidAMMAsset(asset1, pair)) return res; - if (auto const res = invalidAMMAsset(issue2, pair)) + if (auto const res = invalidAMMAsset(asset2, pair)) return res; return tesSUCCESS; } @@ -93,10 +107,10 @@ invalidAMMAssetPair( NotTEC invalidAMMAmount( STAmount const& amount, - std::optional> const& pair, + std::optional> const& pair, bool validZero) { - if (auto const res = invalidAMMAsset(amount.issue(), pair)) + if (auto const res = invalidAMMAsset(amount.asset(), pair)) return res; if (amount < beast::zero || (!validZero && amount == beast::zero)) return temBAD_AMOUNT; diff --git a/src/libxrpl/protocol/Asset.cpp b/src/libxrpl/protocol/Asset.cpp index 5a496352840..4162eb70c26 100644 --- a/src/libxrpl/protocol/Asset.cpp +++ b/src/libxrpl/protocol/Asset.cpp @@ -77,4 +77,14 @@ to_json(Asset const& asset) [&](auto const& issue) { return to_json(issue); }, asset.value()); } +std::ostream& +operator<<(std::ostream& os, Asset const& x) +{ + if (x.holds()) + os << x.get(); + else + os << x.get(); + return os; +} + } // namespace ripple diff --git a/src/libxrpl/protocol/Feature.cpp b/src/libxrpl/protocol/Feature.cpp index 05164489ec7..dbadb7d32f2 100644 --- a/src/libxrpl/protocol/Feature.cpp +++ b/src/libxrpl/protocol/Feature.cpp @@ -459,7 +459,8 @@ uint256 const retiredFix1201 = retireFeature("fix1201"), retiredFix1512 = retireFeature("fix1512"), retiredFix1523 = retireFeature("fix1523"), - retiredFix1528 = retireFeature("fix1528"); + retiredFix1528 = retireFeature("fix1528"), + retiredFlowCross = retireFeature("FlowCross"); // clang-format on diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 046be444224..a28bd3ef0f0 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -100,12 +100,37 @@ getBookBase(Book const& book) XRPL_ASSERT( isConsistent(book), "ripple::getBookBase : input is consistent"); - auto const index = indexHash( - LedgerNameSpace::BOOK_DIR, - book.in.currency, - book.out.currency, - book.in.account, - book.out.account); + auto const index = std::visit( + [&]( + TIn const& in, TOut const& out) { + if constexpr ( + std::is_same_v && std::is_same_v) + return indexHash( + LedgerNameSpace::BOOK_DIR, + in.currency, + out.currency, + in.account, + out.account); + else if constexpr ( + std::is_same_v && std::is_same_v) + return indexHash( + LedgerNameSpace::BOOK_DIR, + in.currency, + out.getMptID(), + in.account); + else if constexpr ( + std::is_same_v && std::is_same_v) + return indexHash( + LedgerNameSpace::BOOK_DIR, + in.getMptID(), + out.currency, + out.account); + else + return indexHash( + LedgerNameSpace::BOOK_DIR, in.getMptID(), out.getMptID()); + }, + book.in.value(), + book.out.value()); // Return with quality 0. auto k = keylet::quality({ltDIR_NODE, index}, 0); @@ -419,16 +444,44 @@ nft_sells(uint256 const& id) noexcept } Keylet -amm(Asset const& issue1, Asset const& issue2) noexcept -{ - auto const& [minI, maxI] = - std::minmax(issue1.get(), issue2.get()); - return amm(indexHash( - LedgerNameSpace::AMM, - minI.account, - minI.currency, - maxI.account, - maxI.currency)); +amm(Asset const& asset1, Asset const& asset2) noexcept +{ + auto const& [minA, maxA] = std::minmax(asset1, asset2); + return std::visit( + []( + TIss1 const& issue1, TIss2 const& issue2) { + if constexpr ( + std::is_same_v && std::is_same_v) + return amm(indexHash( + LedgerNameSpace::AMM, + issue1.account, + issue1.currency, + issue2.account, + issue2.currency)); + else if constexpr ( + std::is_same_v && std::is_same_v) + return amm(indexHash( + LedgerNameSpace::AMM, + issue1.account, + issue1.currency, + issue2.getMptID())); + else if constexpr ( + std::is_same_v && std::is_same_v) + return amm(indexHash( + LedgerNameSpace::AMM, + issue1.getMptID(), + issue2.account, + issue2.currency)); + else if constexpr ( + std::is_same_v && + std::is_same_v) + return amm(indexHash( + LedgerNameSpace::AMM, + issue1.getMptID(), + issue2.getMptID())); + }, + minA.value(), + maxA.value()); } Keylet @@ -497,11 +550,11 @@ mptIssuance(std::uint32_t seq, AccountID const& issuer) noexcept } Keylet -mptIssuance(MPTID const& issuanceID) noexcept +mptIssuance(MPTIssue const& mptIssue) noexcept { return { ltMPTOKEN_ISSUANCE, - indexHash(LedgerNameSpace::MPTOKEN_ISSUANCE, issuanceID)}; + indexHash(LedgerNameSpace::MPTOKEN_ISSUANCE, mptIssue.getMptID())}; } Keylet diff --git a/src/libxrpl/protocol/MPTIssue.cpp b/src/libxrpl/protocol/MPTIssue.cpp index 38022a0ed3a..d1a7a09e533 100644 --- a/src/libxrpl/protocol/MPTIssue.cpp +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include @@ -27,6 +28,11 @@ MPTIssue::MPTIssue(MPTID const& issuanceID) : mptID_(issuanceID) { } +MPTIssue::MPTIssue(std::uint32_t sequence, AccountID const& account) + : MPTIssue(ripple::makeMptID(sequence, account)) +{ +} + AccountID const& MPTIssue::getIssuer() const { @@ -104,4 +110,11 @@ mptIssueFromJson(Json::Value const& v) return MPTIssue{id}; } +std::ostream& +operator<<(std::ostream& os, MPTIssue const& x) +{ + os << to_string(x); + return os; +} + } // namespace ripple diff --git a/src/libxrpl/protocol/PathAsset.cpp b/src/libxrpl/protocol/PathAsset.cpp new file mode 100644 index 00000000000..c0963564521 --- /dev/null +++ b/src/libxrpl/protocol/PathAsset.cpp @@ -0,0 +1,39 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +namespace ripple { + +std::string +to_string(PathAsset const& asset) +{ + return std::visit( + [&](auto const& issue) { return to_string(issue); }, asset.value()); +} + +std::ostream& +operator<<(std::ostream& os, PathAsset const& x) +{ + os << to_string(x); + return os; +} + +} // namespace ripple diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 37830830ade..fc5078a0ebe 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -974,7 +974,7 @@ amountFromJson(SField const& name, Json::Value const& v) if (isMPT) { // sequence (32 bits) + account (160 bits) - uint192 u; + MPTID u; if (!u.parseHex(currencyOrMPTID.asString())) Throw("invalid MPTokenIssuanceID"); asset = u; diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 821f8f05c96..034afd4539d 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -725,6 +725,12 @@ STObject::setFieldH128(SField const& field, uint128 const& v) setFieldUsingSetValue(field, v); } +void +STObject::setFieldH192(SField const& field, uint192 const& v) +{ + setFieldUsingSetValue(field, v); +} + void STObject::setFieldH256(SField const& field, uint256 const& v) { diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index 7d08993a8ba..29610c65c43 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -632,7 +632,7 @@ parseLeaf( json_name + "." + ss.str()); // each element in this path has some combination of - // account, currency, or issuer + // account, asset, or issuer Json::Value pathEl = value[i][j]; @@ -642,20 +642,32 @@ parseLeaf( return ret; } - Json::Value const& account = pathEl["account"]; - Json::Value const& currency = pathEl["currency"]; - Json::Value const& issuer = pathEl["issuer"]; - bool hasCurrency = false; + if (pathEl.isMember(jss::currency) && + pathEl.isMember(jss::mpt_issuance_id)) + { + error = RPC::make_error( + rpcINVALID_PARAMS, "Invalid Asset."); + return ret; + } + + bool const isMPT = + pathEl.isMember(jss::mpt_issuance_id); + auto const assetName = + isMPT ? jss::mpt_issuance_id : jss::currency; + Json::Value const& account = pathEl[jss::account]; + Json::Value const& asset = pathEl[assetName]; + Json::Value const& issuer = pathEl[jss::issuer]; + bool hasAsset = false; AccountID uAccount, uIssuer; - Currency uCurrency; + PathAsset uAsset; if (account) { // human account id if (!account.isString()) { - error = - string_expected(element_name, "account"); + error = string_expected( + element_name, jss::account.c_str()); return ret; } @@ -667,35 +679,57 @@ parseLeaf( parseBase58(account.asString()); if (!a) { - error = - invalid_data(element_name, "account"); + error = invalid_data( + element_name, jss::account.c_str()); return ret; } uAccount = *a; } } - if (currency) + if (asset) { - // human currency - if (!currency.isString()) + // human asset + if (!asset.isString()) { - error = - string_expected(element_name, "currency"); + error = string_expected( + element_name, assetName.c_str()); return ret; } - hasCurrency = true; + hasAsset = true; - if (!uCurrency.parseHex(currency.asString())) + if (isMPT) { - if (!to_currency( - uCurrency, currency.asString())) + MPTID u; + if (!u.parseHex(asset.asString())) { - error = - invalid_data(element_name, "currency"); + error = invalid_data( + element_name, assetName.c_str()); return ret; } + if (getMPTIssuer(u) == beast::zero) + { + error = invalid_data( + element_name, jss::account.c_str()); + return ret; + } + uAsset = u; + } + else + { + Currency currency; + if (!currency.parseHex(asset.asString())) + { + if (!to_currency( + currency, asset.asString())) + { + error = invalid_data( + element_name, assetName.c_str()); + return ret; + } + } + uAsset = currency; } } @@ -704,7 +738,8 @@ parseLeaf( // human account id if (!issuer.isString()) { - error = string_expected(element_name, "issuer"); + error = string_expected( + element_name, jss::issuer.c_str()); return ret; } @@ -714,16 +749,23 @@ parseLeaf( parseBase58(issuer.asString()); if (!a) { - error = - invalid_data(element_name, "issuer"); + error = invalid_data( + element_name, jss::issuer.c_str()); return ret; } uIssuer = *a; } + + if (isMPT && + uIssuer != getMPTIssuer(uAsset.get())) + { + error = invalid_data( + element_name, jss::issuer.c_str()); + return ret; + } } - p.emplace_back( - uAccount, uCurrency, uIssuer, hasCurrency); + p.emplace_back(uAccount, uAsset, uIssuer, hasAsset); } tail.push_back(p); diff --git a/src/libxrpl/protocol/STPathSet.cpp b/src/libxrpl/protocol/STPathSet.cpp index 57bcaca6b2c..e3abfcbb593 100644 --- a/src/libxrpl/protocol/STPathSet.cpp +++ b/src/libxrpl/protocol/STPathSet.cpp @@ -40,8 +40,18 @@ STPathElement::get_hash(STPathElement const& element) for (auto const x : element.getAccountID()) hash_account += (hash_account * 257) ^ x; - for (auto const x : element.getCurrency()) - hash_currency += (hash_currency * 509) ^ x; + // Check pathAsset type instead of element's mType + // In some cases mType might be account but the asset + // is still set to either MPT or currency (see Pathfinder::addLink()) + if (element.getPathAsset().holds()) + { + hash_currency += beast::uhash<>{}(element.getPathAsset().get()); + } + else + { + for (auto const x : element.getPathAsset().get()) + hash_currency += (hash_currency * 509) ^ x; + } for (auto const x : element.getIssuerID()) hash_issuer += (hash_issuer * 911) ^ x; @@ -82,21 +92,28 @@ STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name) auto hasAccount = iType & STPathElement::typeAccount; auto hasCurrency = iType & STPathElement::typeCurrency; auto hasIssuer = iType & STPathElement::typeIssuer; + auto hasMPT = iType & STPathElement::typeMPT; AccountID account; - Currency currency; + PathAsset asset; AccountID issuer; if (hasAccount) account = sit.get160(); + XRPL_ASSERT( + !(hasCurrency && hasMPT), + "ripple::STPathSet::STPathSet : not has Currency and MPT"); if (hasCurrency) - currency = sit.get160(); + asset = static_cast(sit.get160()); + + if (hasMPT) + asset = sit.get192(); if (hasIssuer) issuer = sit.get160(); - path.emplace_back(account, currency, issuer, hasCurrency); + path.emplace_back(account, asset, issuer, hasCurrency); } } } @@ -150,12 +167,12 @@ STPathSet::isDefault() const bool STPath::hasSeen( AccountID const& account, - Currency const& currency, + PathAsset const& asset, AccountID const& issuer) const { for (auto& p : mPath) { - if (p.getAccountID() == account && p.getCurrency() == currency && + if (p.getAccountID() == account && p.getPathAsset() == asset && p.getIssuerID() == issuer) return true; } @@ -178,9 +195,16 @@ STPath::getJson(JsonOptions) const if (iType & STPathElement::typeAccount) elem[jss::account] = to_string(it.getAccountID()); + XRPL_ASSERT( + !(iType & STPathElement::typeCurrency && + iType & STPathElement::typeMPT), + "ripple::STPath::getJson : not type Currency and MPT"); if (iType & STPathElement::typeCurrency) elem[jss::currency] = to_string(it.getCurrency()); + if (iType & STPathElement::typeMPT) + elem[jss::mpt_issuance_id] = to_string(it.getMPTID()); + if (iType & STPathElement::typeIssuer) elem[jss::issuer] = to_string(it.getIssuerID()); @@ -230,6 +254,9 @@ STPathSet::add(Serializer& s) const if (iType & STPathElement::typeAccount) s.addBitString(speElement.getAccountID()); + if (iType & STPathElement::typeMPT) + s.addBitString(speElement.getMPTID()); + if (iType & STPathElement::typeCurrency) s.addBitString(speElement.getCurrency()); diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 815b27c0018..ee7db39424c 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -167,6 +167,7 @@ transResults() MAKE_ERROR(temBAD_FEE, "Invalid fee, negative or not XRP."), MAKE_ERROR(temBAD_ISSUER, "Malformed: Bad issuer."), MAKE_ERROR(temBAD_LIMIT, "Limits must be non-negative."), + MAKE_ERROR(temBAD_MPT, "Malformed: Bad MPT."), MAKE_ERROR(temBAD_OFFER, "Malformed: Bad offer."), MAKE_ERROR(temBAD_PATH, "Malformed: Bad path."), MAKE_ERROR(temBAD_PATH_LOOP, "Malformed: Loop in path."), diff --git a/src/test/app/AMMCalc_test.cpp b/src/test/app/AMMCalc_test.cpp index 058cdfd1d2d..05ffcef3e67 100644 --- a/src/test/app/AMMCalc_test.cpp +++ b/src/test/app/AMMCalc_test.cpp @@ -23,6 +23,8 @@ #include +#include + namespace ripple { namespace test { @@ -176,9 +178,8 @@ class AMMCalc_test : public beast::unit_test::suite std::string toString(STAmount const& a) { - std::stringstream str; - str << a.getText() << "/" << to_string(a.issue().currency); - return str.str(); + return std::format( + "{}/{}", a.getText(), to_string(a.get().currency)); } STAmount @@ -187,8 +188,8 @@ class AMMCalc_test : public beast::unit_test::suite if (a == b) return amt; if (amt.native()) - return toSTAmount(mulRatio(amt.xrp(), a, b, round), amt.issue()); - return toSTAmount(mulRatio(amt.iou(), a, b, round), amt.issue()); + return toSTAmount(mulRatio(amt.xrp(), a, b, round), amt.asset()); + return toSTAmount(mulRatio(amt.iou(), a, b, round), amt.asset()); } void @@ -203,8 +204,8 @@ class AMMCalc_test : public beast::unit_test::suite STAmount sin{}; int limitingStep = vp.size(); STAmount limitStepOut{}; - auto trate = [&](auto const& amt) { - auto const currency = to_string(amt.issue().currency); + auto trate = [&](STAmount const& amt) { + auto const currency = to_string(amt.get().currency); return rates.find(currency) != rates.end() ? rates.at(currency) : QUALITY_ONE; }; @@ -268,8 +269,8 @@ class AMMCalc_test : public beast::unit_test::suite STAmount sout{}; int limitingStep = 0; STAmount limitStepIn{}; - auto trate = [&](auto const& amt) { - auto const currency = to_string(amt.issue().currency); + auto trate = [&](STAmount const& amt) { + auto const currency = to_string(amt.get().currency); return rates.find(currency) != rates.end() ? rates.at(currency) : QUALITY_ONE; }; diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index 96053b93b44..6ecd1422b6e 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -1503,8 +1503,8 @@ struct AMMExtended_test : public jtx::AMMTest BEAST_EXPECT(sa == XRP(100'000'000)); // Bob gets ~99.99USD. This is the amount Bob // can get out of AMM for 100,000,000XRP. - BEAST_EXPECT(equal( - da, STAmount{bob["USD"].issue(), UINT64_C(99'9999000001), -10})); + BEAST_EXPECT( + equal(da, STAmount{bob["USD"], UINT64_C(99'9999000001), -10})); } // carol holds gateway AUD, sells gateway AUD for XRP @@ -2121,10 +2121,10 @@ struct AMMExtended_test : public jtx::AMMTest }; { // BTC -> USD - STPath p1({IPE(USD.issue())}); + STPath p1({IPE(USD)}); paths.push_back(p1); // BTC -> EUR -> USD - STPath p2({IPE(EUR.issue()), IPE(USD.issue())}); + STPath p2({IPE(EUR), IPE(USD)}); paths.push_back(p2); } diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index f1e81132c5e..28e8adc232b 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -128,11 +128,11 @@ struct AMM_test : public jtx::AMMTest // Make sure asset comparison works. BEAST_EXPECT( - STIssue(sfAsset, STAmount(XRP(2'000)).issue()) == - STIssue(sfAsset, STAmount(XRP(2'000)).issue())); + STIssue(sfAsset, STAmount(XRP(2'000)).asset()) == + STIssue(sfAsset, STAmount(XRP(2'000)).asset())); BEAST_EXPECT( - STIssue(sfAsset, STAmount(XRP(2'000)).issue()) != - STIssue(sfAsset, STAmount(USD(2'000)).issue())); + STIssue(sfAsset, STAmount(XRP(2'000)).asset()) != + STIssue(sfAsset, STAmount(USD(2'000)).asset())); } void @@ -6234,7 +6234,7 @@ struct AMM_test : public jtx::AMMTest takerGets}; } auto const takerPays = toAmount( - getIssue(poolIn), Number{1, -10} * poolIn); + getAsset(poolIn), Number{1, -10} * poolIn); return Amounts{ takerPays, swapAssetIn( @@ -6976,7 +6976,7 @@ struct AMM_test : public jtx::AMMTest // allowed for clawing back from an AMM account. Please notice the // `issuer` subfield represents the account being clawed back, which // is confusing. - Issue usd(USD.issue().currency, amm.ammAccount()); + Issue usd(USD.currency, amm.ammAccount()); auto amount = amountFromString(usd, "10"); env(claw(gw, amount), ter(tecAMM_ACCOUNT)); } diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index 2c4f44ce79f..cd377e4570a 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -1182,8 +1182,8 @@ class Check_test : public beast::unit_test::suite double pct, double amount) { // Capture bob's and alice's balances so we can test at the end. - STAmount const aliceStart{env.balance(alice, USD.issue()).value()}; - STAmount const bobStart{env.balance(bob, USD.issue()).value()}; + STAmount const aliceStart{env.balance(alice, USD).value()}; + STAmount const bobStart{env.balance(bob, USD).value()}; // Set the modified quality. env(trust(truster, iou(1000)), inOrOut(pct)); @@ -1207,8 +1207,8 @@ class Check_test : public beast::unit_test::suite double pct, double amount) { // Capture bob's and alice's balances so we can test at the end. - STAmount const aliceStart{env.balance(alice, USD.issue()).value()}; - STAmount const bobStart{env.balance(bob, USD.issue()).value()}; + STAmount const aliceStart{env.balance(alice, USD).value()}; + STAmount const bobStart{env.balance(bob, USD).value()}; // Set the modified quality. env(trust(truster, iou(1000)), inOrOut(pct)); @@ -1271,7 +1271,7 @@ class Check_test : public beast::unit_test::suite double max2) { // Capture alice's balance so we can test at the end. It doesn't // make any sense to look at the balance of a gateway. - STAmount const aliceStart{env.balance(alice, USD.issue()).value()}; + STAmount const aliceStart{env.balance(alice, USD).value()}; // Set the modified quality. env(trust(truster, iou(1000)), inOrOut(pct)); @@ -1304,7 +1304,7 @@ class Check_test : public beast::unit_test::suite double max2) { // Capture alice's balance so we can test at the end. It doesn't // make any sense to look at the balance of the issuer. - STAmount const aliceStart{env.balance(alice, USD.issue()).value()}; + STAmount const aliceStart{env.balance(alice, USD).value()}; // Set the modified quality. env(trust(truster, iou(1000)), inOrOut(pct)); @@ -1500,7 +1500,7 @@ class Check_test : public beast::unit_test::suite { IOU const wrongCurrency{gw["EUR"]}; STAmount badAmount{amount}; - badAmount.setIssue(wrongCurrency.issue()); + badAmount.setIssue(wrongCurrency); env(check::cash(bob, chkId, badAmount), ter(temMALFORMED)); env.close(); } @@ -1509,7 +1509,7 @@ class Check_test : public beast::unit_test::suite { IOU const wrongIssuer{alice["USD"]}; STAmount badAmount{amount}; - badAmount.setIssue(wrongIssuer.issue()); + badAmount.setIssue(wrongIssuer); env(check::cash(bob, chkId, badAmount), ter(temMALFORMED)); env.close(); } @@ -2146,8 +2146,7 @@ class Check_test : public beast::unit_test::suite return; BEAST_EXPECT( - offerAmount.issue().account == - checkAmount.issue().account); + offerAmount.getIssuer() == checkAmount.getIssuer()); BEAST_EXPECT( offerAmount.negative() == checkAmount.negative()); BEAST_EXPECT( diff --git a/src/test/app/CrossingLimits_test.cpp b/src/test/app/CrossingLimits_test.cpp index 6f6a7eb3e7f..1341b815ea6 100644 --- a/src/test/app/CrossingLimits_test.cpp +++ b/src/test/app/CrossingLimits_test.cpp @@ -76,10 +76,7 @@ class CrossingLimits_test : public beast::unit_test::suite auto const gw = Account("gateway"); auto const USD = gw["USD"]; - // The number of allowed offers to cross is different between - // Taker and FlowCross. Taker allows 850 and FlowCross allows 1000. - // Accommodate that difference in the test. - int const maxConsumed = features[featureFlowCross] ? 1000 : 850; + int const maxConsumed = 1000; env.fund(XRP(100000000), gw, "alice", "bob", "carol"); int const bobsOfferCount = maxConsumed + 150; @@ -118,11 +115,7 @@ class CrossingLimits_test : public beast::unit_test::suite env.fund(XRP(100000000), gw, "alice", "bob", "carol", "dan", "evita"); - // The number of offers allowed to cross is different between - // Taker and FlowCross. Taker allows 850 and FlowCross allows 1000. - // Accommodate that difference in the test. - bool const isFlowCross{features[featureFlowCross]}; - int const maxConsumed = isFlowCross ? 1000 : 850; + int const maxConsumed = 1000; int const evitasOfferCount{maxConsumed + 49}; env.trust(USD(1000), "alice"); @@ -132,14 +125,9 @@ class CrossingLimits_test : public beast::unit_test::suite env.trust(USD(evitasOfferCount + 1), "evita"); env(pay(gw, "evita", USD(evitasOfferCount + 1))); - // Taker and FlowCross have another difference we must accommodate. - // Taker allows a total of 1000 unfunded offers to be consumed - // beyond the 850 offers it can take. FlowCross draws no such - // distinction; its limit is 1000 funded or unfunded. - // // Give carol an extra 150 (unfunded) offers when we're using Taker // to accommodate that difference. - int const carolsOfferCount{isFlowCross ? 700 : 850}; + int const carolsOfferCount{700}; n_offers(env, 400, "alice", XRP(1), USD(1)); n_offers(env, carolsOfferCount, "carol", XRP(1), USD(1)); n_offers(env, evitasOfferCount, "evita", XRP(1), USD(1)); @@ -267,9 +255,13 @@ class CrossingLimits_test : public beast::unit_test::suite } void - testAutoBridgedLimitsFlowCross(FeatureBitset features) + testAutoBridgedLimits(FeatureBitset features) { - testcase("Auto Bridged Limits FlowCross"); + testcase("Auto Bridged Limits"); + + // Extracts as much as possible in one book at one Quality + // before proceeding to the other book. This reduces the number of + // times we change books. // If any book step in a payment strand consumes 1000 offers, the // liquidity from the offers is used, but that strand will be marked as @@ -451,26 +443,6 @@ class CrossingLimits_test : public beast::unit_test::suite } } - void - testAutoBridgedLimits(FeatureBitset features) - { - // Taker and FlowCross are too different in the way they handle - // autobridging to make one test suit both approaches. - // - // o Taker alternates between books, completing one full increment - // before returning to make another pass. - // - // o FlowCross extracts as much as possible in one book at one Quality - // before proceeding to the other book. This reduces the number of - // times we change books. - // - // So the tests for the two forms of autobridging are separate. - if (features[featureFlowCross]) - testAutoBridgedLimitsFlowCross(features); - else - testAutoBridgedLimitsTaker(features); - } - void testOfferOverflow(FeatureBitset features) { @@ -521,11 +493,10 @@ class CrossingLimits_test : public beast::unit_test::suite n_offers(env, 998, alice, XRP(0.96), USD(1)); n_offers(env, 998, alice, XRP(0.95), USD(1)); - bool const withFlowCross = features[featureFlowCross]; bool const withSortStrands = features[featureFlowSortStrands]; auto const expectedTER = [&]() -> TER { - if (withFlowCross && !withSortStrands) + if (!withSortStrands) return TER{tecOVERSIZE}; return tesSUCCESS; }(); @@ -534,8 +505,6 @@ class CrossingLimits_test : public beast::unit_test::suite env.close(); auto const expectedUSD = [&] { - if (!withFlowCross) - return USD(850); if (!withSortStrands) return USD(0); return USD(1996); @@ -558,7 +527,6 @@ class CrossingLimits_test : public beast::unit_test::suite auto const sa = supported_amendments(); testAll(sa); testAll(sa - featureFlowSortStrands); - testAll(sa - featureFlowCross - featureFlowSortStrands); } }; diff --git a/src/test/app/DeliverMin_test.cpp b/src/test/app/DeliverMin_test.cpp index 3c62a47a4a4..e33623ee0bc 100644 --- a/src/test/app/DeliverMin_test.cpp +++ b/src/test/app/DeliverMin_test.cpp @@ -133,7 +133,6 @@ class DeliverMin_test : public beast::unit_test::suite { using namespace jtx; auto const sa = supported_amendments(); - test_convert_all_of_an_asset(sa - featureFlowCross); test_convert_all_of_an_asset(sa); } }; diff --git a/src/test/app/Discrepancy_test.cpp b/src/test/app/Discrepancy_test.cpp index 1eaa1ad86dd..16e9e1c8951 100644 --- a/src/test/app/Discrepancy_test.cpp +++ b/src/test/app/Discrepancy_test.cpp @@ -146,7 +146,6 @@ class Discrepancy_test : public beast::unit_test::suite { using namespace test::jtx; auto const sa = supported_amendments(); - testXRPDiscrepancy(sa - featureFlowCross); testXRPDiscrepancy(sa); } }; diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index 4d1397eab83..c40bf15df3a 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -457,10 +457,10 @@ struct Flow_test : public beast::unit_test::suite }; { // BTC -> USD - STPath p1({IPE(USD.issue())}); + STPath p1({IPE(USD)}); paths.push_back(p1); // BTC -> EUR -> USD - STPath p2({IPE(EUR.issue()), IPE(USD.issue())}); + STPath p2({IPE(EUR), IPE(USD)}); paths.push_back(p2); } @@ -1019,17 +1019,9 @@ struct Flow_test : public beast::unit_test::suite env(trust(bob, USD(20))); STAmount tinyAmt1{ - USD.issue(), - 9000000000000000ll, - -17, - false, - STAmount::unchecked{}}; + USD, 9000000000000000ll, -17, false, STAmount::unchecked{}}; STAmount tinyAmt3{ - USD.issue(), - 9000000000000003ll, - -17, - false, - STAmount::unchecked{}}; + USD, 9000000000000003ll, -17, false, STAmount::unchecked{}}; env(offer(gw, drops(9000000000), tinyAmt3)); env(pay(alice, bob, tinyAmt1), @@ -1052,17 +1044,9 @@ struct Flow_test : public beast::unit_test::suite env(trust(alice, USD(20))); STAmount tinyAmt1{ - USD.issue(), - 9000000000000000ll, - -17, - false, - STAmount::unchecked{}}; + USD, 9000000000000000ll, -17, false, STAmount::unchecked{}}; STAmount tinyAmt3{ - USD.issue(), - 9000000000000003ll, - -17, - false, - STAmount::unchecked{}}; + USD, 9000000000000003ll, -17, false, STAmount::unchecked{}}; env(pay(gw, alice, tinyAmt1)); @@ -1097,39 +1081,34 @@ struct Flow_test : public beast::unit_test::suite BEAST_EXPECT(!getNoRippleFlag(env, gw, alice, usdC)); - env(pay( - gw, - alice, - // 12.55.... - STAmount{ - USD.issue(), std::uint64_t(1255555555555555ull), -14, false})); + env( + pay(gw, + alice, + // 12.55.... + STAmount{USD, std::uint64_t(1255555555555555ull), -14, false})); env(offer( gw, // 5.0... - STAmount{ - USD.issue(), std::uint64_t(5000000000000000ull), -15, false}, + STAmount{USD, std::uint64_t(5000000000000000ull), -15, false}, XRP(1000))); env(offer( gw, // .555... - STAmount{ - USD.issue(), std::uint64_t(5555555555555555ull), -16, false}, + STAmount{USD, std::uint64_t(5555555555555555ull), -16, false}, XRP(10))); env(offer( gw, // 4.44.... - STAmount{ - USD.issue(), std::uint64_t(4444444444444444ull), -15, false}, + STAmount{USD, std::uint64_t(4444444444444444ull), -15, false}, XRP(.1))); env(offer( alice, // 17 - STAmount{ - USD.issue(), std::uint64_t(1700000000000000ull), -14, false}, + STAmount{USD, std::uint64_t(1700000000000000ull), -14, false}, XRP(.001))); env(pay(alice, bob, XRP(10000)), @@ -1435,7 +1414,6 @@ struct Flow_test : public beast::unit_test::suite using namespace jtx; auto const sa = supported_amendments(); - testWithFeats(sa - featureFlowCross); testWithFeats(sa); testEmptyStrand(sa); } @@ -1448,11 +1426,8 @@ struct Flow_manual_test : public Flow_test { using namespace jtx; auto const all = supported_amendments(); - FeatureBitset const flowCross{featureFlowCross}; FeatureBitset const f1513{fix1513}; - testWithFeats(all - flowCross - f1513); - testWithFeats(all - flowCross); testWithFeats(all - f1513); testWithFeats(all); diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index 99696c11f6e..900d4d41cc7 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -959,24 +959,12 @@ class Freeze_test : public beast::unit_test::suite env.close(); // test: A1 wants to buy, must fail - if (features[featureFlowCross]) - { - env(offer(A1, USD(1), XRP(2)), - txflags(tfFillOrKill), - ter(tecKILLED)); - env.close(); - env.require( - balance(A1, USD(1002)), - balance(A2, USD(997)), - offers(A1, 0)); - } - else - { - // The transaction that should be here would succeed. - // I don't want to adjust balances in following tests. Flow - // cross feature flag is not relevant to this particular test - // case so we're not missing out some corner cases checks. - } + env(offer(A1, USD(1), XRP(2)), + txflags(tfFillOrKill), + ter(tecKILLED)); + env.close(); + env.require( + balance(A1, USD(1002)), balance(A2, USD(997)), offers(A1, 0)); // test: A1 can create passive sell offer env(offer(A1, XRP(2), USD(1)), txflags(tfPassive)); @@ -2018,8 +2006,6 @@ class Freeze_test : public beast::unit_test::suite }; using namespace test::jtx; auto const sa = supported_amendments(); - testAll(sa - featureFlowCross - featureDeepFreeze); - testAll(sa - featureFlowCross); testAll(sa - featureDeepFreeze); testAll(sa); } diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 9fd4927d5eb..366a829d71b 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -18,6 +18,10 @@ //============================================================================== #include +#include +#include +#include +#include #include #include #include @@ -689,10 +693,12 @@ class MPToken_test : public beast::unit_test::suite mptAlice.authorize({.account = bob}); - for (auto flags : {tfNoRippleDirect, tfLimitQuality}) - env(pay(alice, bob, MPT(10)), - txflags(flags), - ter(temINVALID_FLAG)); + auto err = !features[featureMPTokensV2] ? ter(temINVALID_FLAG) + : ter(temRIPPLE_EMPTY); + env(pay(alice, bob, MPT(10)), txflags(tfNoRippleDirect), err); + err = !features[featureMPTokensV2] ? ter(temINVALID_FLAG) + : ter(tesSUCCESS); + env(pay(alice, bob, MPT(10)), txflags(tfLimitQuality), err); } // Invalid combination of send, sendMax, deliverMin, paths @@ -703,27 +709,28 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {carol}}); - mptAlice.create({.ownerCount = 1, .holderCount = 0}); + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); mptAlice.authorize({.account = carol}); // sendMax and DeliverMin are valid XRP amount, // but is invalid combination with MPT amount auto const MPT = mptAlice["MPT"]; - env(pay(alice, carol, MPT(100)), - sendmax(XRP(100)), - ter(temMALFORMED)); + auto const MPTokensV2 = features[featureMPTokensV2]; + auto err = !MPTokensV2 ? ter(temMALFORMED) : ter(tecPATH_PARTIAL); + env(pay(alice, carol, MPT(100)), sendmax(XRP(100)), err); env(pay(alice, carol, MPT(100)), delivermin(XRP(100)), ter(temBAD_AMOUNT)); // sendMax MPT is invalid with IOU or XRP auto const USD = alice["USD"]; - env(pay(alice, carol, USD(100)), - sendmax(MPT(100)), - ter(temMALFORMED)); - env(pay(alice, carol, XRP(100)), - sendmax(MPT(100)), - ter(temMALFORMED)); + err = !MPTokensV2 ? ter(temMALFORMED) : ter(tecPATH_DRY); + env(pay(alice, carol, USD(100)), sendmax(MPT(100)), err); + err = !MPTokensV2 ? ter(temMALFORMED) : ter(tecPATH_PARTIAL); + env(pay(alice, carol, XRP(100)), sendmax(MPT(100)), err); env(pay(alice, carol, USD(100)), delivermin(MPT(100)), ter(temBAD_AMOUNT)); @@ -733,16 +740,16 @@ class MPToken_test : public beast::unit_test::suite // sendmax and amount are different MPT issue test::jtx::MPT const MPT1( "MPT", makeMptID(env.seq(alice) + 10, alice)); - env(pay(alice, carol, MPT1(100)), - sendmax(MPT(100)), - ter(temMALFORMED)); - // paths is invalid - env(pay(alice, carol, MPT(100)), path(~USD), ter(temMALFORMED)); + err = !MPTokensV2 ? ter(temMALFORMED) : ter(tecOBJECT_NOT_FOUND); + env(pay(alice, carol, MPT1(100)), sendmax(MPT(100)), err); + // "paths" is invalid in V1 + err = !MPTokensV2 ? ter(temMALFORMED) : ter(tesSUCCESS); + env(pay(alice, carol, MPT(100)), path(~USD), err); } // build_path is invalid if MPT { - Env env{*this, features}; + Env env{*this, features - featureMPTokensV2}; Account const alice("alice"); Account const carol("carol"); @@ -1022,10 +1029,13 @@ class MPToken_test : public beast::unit_test::suite env(pay(bob, carol, MPT(100)), sendmax(MPT(90)), txflags(tfPartialPayment)); - // 82 to carol, 8 to issuer (90 / 1.1 ~ 81.81 (rounded to nearest) = - // 82) + // 82 to carol, 8 to issuer (90 / 1.1 ~ 81.81 (rounded to nearest in + // v1) = 82) BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 690)); - BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 282)); + // In V2 the payments are executed via the payment engine and + // the rounding results in a higher quality trade + BEAST_EXPECT(mptAlice.checkMPTokenAmount( + carol, !features[featureMPTokensV2] ? 282 : 281)); } // Insufficient SendMax with no transfer fee @@ -1169,6 +1179,7 @@ class MPToken_test : public beast::unit_test::suite env(pay(bob, carol, MPT(10'000)), sendmax(MPT(10'000)), txflags(tfPartialPayment)); + // Verify the metadata auto const meta = env.meta()->getJson( JsonOptions::none)[sfAffectedNodes.fieldName]; @@ -1250,7 +1261,10 @@ class MPToken_test : public beast::unit_test::suite env.fund(XRP(1'000), alice, bob); STAmount const mpt{MPTID{0}, 100}; - env(pay(alice, bob, mpt), ter(tecOBJECT_NOT_FOUND)); + auto const err = !features[featureMPTokensV2] + ? ter(tecOBJECT_NOT_FOUND) + : ter(temBAD_PATH); + env(pay(alice, bob, mpt), err); } // Issuer fails trying to send to an account, which doesn't own MPT for @@ -1317,7 +1331,7 @@ class MPToken_test : public beast::unit_test::suite } void - testDepositPreauth() + testDepositPreauth(FeatureBitset features) { testcase("DepositPreauth"); @@ -1330,7 +1344,7 @@ class MPToken_test : public beast::unit_test::suite const char credType[] = "abcde"; { - Env env(*this); + Env env(*this, features); env.fund(XRP(50000), diana, dpIssuer); env.close(); @@ -1405,7 +1419,7 @@ class MPToken_test : public beast::unit_test::suite testcase("DepositPreauth disabled featureCredentials"); { - Env env(*this, supported_amendments() - featureCredentials); + Env env(*this, features - featureCredentials); std::string const credIdx = "D007AE4B6E1274B4AF872588267B810C2F82716726351D1C7D38D3E5499FC6" @@ -1530,9 +1544,6 @@ class MPToken_test : public beast::unit_test::suite jrr = env.rpc("json", "sign", to_string(jv1)); BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); }; - auto toSFieldRef = [](SField const& field) { - return std::ref(field); - }; auto setMPTFields = [&](SField const& field, Json::Value& jv, bool withAmount = true) { @@ -1552,22 +1563,6 @@ class MPToken_test : public beast::unit_test::suite // Transactions with amount fields, which can't be MPT. // Transactions with issue fields, which can't be MPT. - // AMMCreate - auto ammCreate = [&](SField const& field) { - Json::Value jv; - jv[jss::TransactionType] = jss::AMMCreate; - jv[jss::Account] = alice.human(); - jv[jss::Amount] = (field.fieldName == sfAmount.fieldName) - ? mpt.getJson(JsonOptions::none) - : "100000000"; - jv[jss::Amount2] = (field.fieldName == sfAmount2.fieldName) - ? mpt.getJson(JsonOptions::none) - : "100000000"; - jv[jss::TradingFee] = 0; - test(jv, field.fieldName); - }; - ammCreate(sfAmount); - ammCreate(sfAmount2); // AMMDeposit auto ammDeposit = [&](SField const& field) { Json::Value jv; @@ -1578,12 +1573,7 @@ class MPToken_test : public beast::unit_test::suite test(jv, field.fieldName); }; for (SField const& field : - {toSFieldRef(sfAmount), - toSFieldRef(sfAmount2), - toSFieldRef(sfEPrice), - toSFieldRef(sfLPTokenOut), - toSFieldRef(sfAsset), - toSFieldRef(sfAsset2)}) + {std::ref(sfEPrice), std::ref(sfLPTokenOut)}) ammDeposit(field); // AMMWithdraw auto ammWithdraw = [&](SField const& field) { @@ -1594,13 +1584,8 @@ class MPToken_test : public beast::unit_test::suite setMPTFields(field, jv); test(jv, field.fieldName); }; - ammWithdraw(sfAmount); for (SField const& field : - {toSFieldRef(sfAmount2), - toSFieldRef(sfEPrice), - toSFieldRef(sfLPTokenIn), - toSFieldRef(sfAsset), - toSFieldRef(sfAsset2)}) + {std::ref(sfEPrice), std::ref(sfLPTokenIn)}) ammWithdraw(field); // AMMBid auto ammBid = [&](SField const& field) { @@ -1610,67 +1595,8 @@ class MPToken_test : public beast::unit_test::suite setMPTFields(field, jv); test(jv, field.fieldName); }; - for (SField const& field : - {toSFieldRef(sfBidMin), - toSFieldRef(sfBidMax), - toSFieldRef(sfAsset), - toSFieldRef(sfAsset2)}) - ammBid(field); - // AMMClawback - auto ammClawback = [&](SField const& field) { - Json::Value jv; - jv[jss::TransactionType] = jss::AMMClawback; - jv[jss::Account] = alice.human(); - jv[jss::Holder] = carol.human(); - setMPTFields(field, jv); - test(jv, field.fieldName); - }; - for (SField const& field : - {toSFieldRef(sfAmount), - toSFieldRef(sfAsset), - toSFieldRef(sfAsset2)}) - ammClawback(field); - // AMMDelete - auto ammDelete = [&](SField const& field) { - Json::Value jv; - jv[jss::TransactionType] = jss::AMMDelete; - jv[jss::Account] = alice.human(); - setMPTFields(field, jv, false); - test(jv, field.fieldName); - }; - ammDelete(sfAsset); - ammDelete(sfAsset2); - // AMMVote - auto ammVote = [&](SField const& field) { - Json::Value jv; - jv[jss::TransactionType] = jss::AMMVote; - jv[jss::Account] = alice.human(); - jv[jss::TradingFee] = 100; - setMPTFields(field, jv, false); - test(jv, field.fieldName); - }; - ammVote(sfAsset); - ammVote(sfAsset2); - // CheckCash - auto checkCash = [&](SField const& field) { - Json::Value jv; - jv[jss::TransactionType] = jss::CheckCash; - jv[jss::Account] = alice.human(); - jv[sfCheckID.fieldName] = to_string(uint256{1}); - jv[field.fieldName] = mpt.getJson(JsonOptions::none); - test(jv, field.fieldName); - }; - checkCash(sfAmount); - checkCash(sfDeliverMin); - // CheckCreate - { - Json::Value jv; - jv[jss::TransactionType] = jss::CheckCreate; - jv[jss::Account] = alice.human(); - jv[jss::Destination] = carol.human(); - jv[jss::SendMax] = mpt.getJson(JsonOptions::none); - test(jv, jss::SendMax.c_str()); - } + ammBid(sfBidMin); + ammBid(sfBidMax); // EscrowCreate { Json::Value jv; @@ -1680,13 +1606,6 @@ class MPToken_test : public beast::unit_test::suite jv[jss::Amount] = mpt.getJson(JsonOptions::none); test(jv, jss::Amount.c_str()); } - // OfferCreate - { - Json::Value jv = offer(alice, USD(100), mpt); - test(jv, jss::TakerPays.c_str()); - jv = offer(alice, mpt, USD(100)); - test(jv, jss::TakerGets.c_str()); - } // PaymentChannelCreate { Json::Value jv; @@ -2272,48 +2191,1939 @@ class MPToken_test : public beast::unit_test::suite } } -public: void - run() override + testOfferCrossing(FeatureBitset features) { + testcase("Offer Crossing"); using namespace test::jtx; - FeatureBitset const all{supported_amendments()}; + Account const gw = Account("gw"); + Account const alice = Account("alice"); + Account const carol = Account("carol"); + auto const USD = gw["USD"]; + + // Blocking flags + for (auto flags : + {tfMPTCanLock | + tfMPTCanTransfer, // locked, issuer and holder fails + tfMPTRequireAuth | + tfMPTCanTransfer, // not authorized, holder fails + tfMPTCanTrade, // can't transfer, holder fails + tfMPTCanLock}) // lock mptoken, holder fails + { + Env env{*this, features}; - // MPTokenIssuanceCreate - testCreateValidation(all); - testCreateEnabled(all); + MPTTester mpt(env, gw, {.holders = {alice}}); - // MPTokenIssuanceDestroy - testDestroyValidation(all); - testDestroyEnabled(all); + auto const lockMPToken = + (flags & (tfMPTCanLock | tfMPTCanTransfer)) == tfMPTCanLock; + auto const lockMPTIssue = + (flags & (tfMPTCanLock | tfMPTCanTransfer)) == + (tfMPTCanLock | tfMPTCanTransfer); + flags = lockMPToken ? (flags | tfMPTCanTransfer) : flags; - // MPTokenAuthorize - testAuthorizeValidation(all); - testAuthorizeEnabled(all); + mpt.create({.ownerCount = 1, .holderCount = 0, .flags = flags}); + auto const MPT = mpt["MPT"]; - // MPTokenIssuanceSet - testSetValidation(all); - testSetEnabled(all); + if ((flags & tfMPTRequireAuth) == 0) + { + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + } + if (lockMPToken) + mpt.set({.holder = alice, .flags = tfMPTLock}); + else if (lockMPTIssue) + mpt.set({.flags = tfMPTLock}); - // MPT clawback - testClawbackValidation(all); - testClawback(all); + auto const err = + flags & tfMPTRequireAuth ? tecUNFUNDED_OFFER : tecNO_PERMISSION; - // Test Direct Payment - testPayment(all); - testDepositPreauth(); + env(offer(alice, XRP(100), MPT(101)), ter(err)); + env.close(); + } - // Test MPT Amount is invalid in Tx, which don't support MPT - testMPTInvalidInTx(all); + // MPTokenV2 is disabled + { + Env env{*this, features - featureMPTokensV2}; - // Test parsed MPTokenIssuanceID in API response metadata - testTxJsonMetaFields(all); + MPTTester mpt(env, gw, {.holders = {alice}}); - // Test tokens equality - testTokensEquality(); + mpt.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); - // Test helpers - testHelperFunctions(); + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + + env(offer(alice, XRP(100), mpt.mpt(101)), ter(temDISABLED)); + env.close(); + } + + // XRP/MPT + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice, carol}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + env(offer(alice, XRP(100), MPT(101))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{XRP(100), MPT(101)}}})); + + env(offer(carol, MPT(101), XRP(100))); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(expectOffers(env, carol, 0)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 301)); + } + + // IOU/MPT + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice, carol}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + env(trust(alice, USD(2'000))); + env(pay(gw, alice, USD(1'000))); + env.close(); + + env(trust(carol, USD(2'000))); + env(pay(gw, carol, USD(1'000))); + env.close(); + + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + env(offer(alice, USD(100), MPT(101))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{USD(100), MPT(101)}}})); + + env(offer(carol, MPT(101), USD(100))); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == USD(1'100)); + BEAST_EXPECT(env.balance(carol, USD) == USD(900)); + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(expectOffers(env, carol, 0)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 301)); + } + + // MPT/MPT + { + Env env{*this, features}; + + MPTTester mpt1(env, gw, {.holders = {alice, carol}}); + mpt1.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + + MPTTester mpt2(env, gw, {.holders = {alice, carol}, .fund = false}); + mpt2.create( + {.ownerCount = 2, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT2 = mpt2["MPT2"]; + + mpt1.authorize({.account = alice}); + mpt1.authorize({.account = carol}); + mpt1.pay(gw, alice, 200); + mpt1.pay(gw, carol, 200); + + mpt2.authorize({.account = alice}); + mpt2.authorize({.account = carol}); + mpt2.pay(gw, alice, 200); + mpt2.pay(gw, carol, 200); + + env(offer(alice, MPT2(100), MPT1(101))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{MPT2(100), MPT1(101)}}})); + + env(offer(carol, MPT1(101), MPT2(100))); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(expectOffers(env, carol, 0)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(carol, 301)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 300)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 100)); + } + } + + void + testCrossAssetPayment(FeatureBitset features) + { + testcase("Cross Asset Payment"); + using namespace test::jtx; + Account const gw = Account("gw"); + Account const alice = Account("alice"); + Account const carol = Account("carol"); + Account const bob = Account("bob"); + auto const USD = gw["USD"]; + + // Blocking flags + for (auto flags : + {tfMPTCanLock | + tfMPTCanTransfer, // locked, issuer and holder fails + tfMPTRequireAuth | + tfMPTCanTransfer, // not authorized, holder fails + tfMPTCanTrade, // can't transfer, holder fails + tfMPTCanLock}) // lock mptoken, holder fails + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice}}); + + auto const lockMPToken = + (flags & (tfMPTCanLock | tfMPTCanTransfer)) == tfMPTCanLock; + auto const lockMPTIssue = + (flags & (tfMPTCanLock | tfMPTCanTransfer)) == + (tfMPTCanLock | tfMPTCanTransfer); + flags = lockMPToken ? (flags | tfMPTCanTransfer) : flags; + + mpt.create({.ownerCount = 1, .holderCount = 0, .flags = flags}); + auto const MPT = mpt["MPT"]; + + if ((flags & tfMPTRequireAuth) == 0) + { + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + } + if (lockMPToken) + mpt.set({.holder = alice, .flags = tfMPTLock}); + else if (lockMPTIssue) + mpt.set({.flags = tfMPTLock}); + + auto const err = + flags & tfMPTRequireAuth ? tecUNFUNDED_OFFER : tecNO_PERMISSION; + + env(offer(alice, XRP(100), MPT(101)), ter(err)); + env.close(); + } + + // Loop + { + Env env{*this, features}; + MPTTester mpt(env, gw, {.holders = {carol, bob}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + mpt.authorize({.account = bob}); + + // holder to holder + env(pay(carol, bob, MPT(1)), + test::jtx::path(~MPT, ~USD, ~MPT), + sendmax(XRP(1)), + txflags(tfPartialPayment), + ter(temBAD_PATH_LOOP)); + env.close(); + + // issuer to holder + env(pay(gw, bob, MPT(1)), + test::jtx::path(~MPT, ~USD, ~MPT), + sendmax(XRP(1)), + txflags(tfPartialPayment), + ter(temBAD_PATH_LOOP)); + env.close(); + + // holder to issuer + env(pay(bob, gw, MPT(1)), + test::jtx::path(~MPT, ~USD, ~MPT), + sendmax(XRP(1)), + txflags(tfPartialPayment), + ter(temBAD_PATH_LOOP)); + env.close(); + } + + // Rippling + { + Env env{*this, features}; + MPTTester mpt(env, gw, {.holders = {carol, bob}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + mpt.authorize({.account = bob}); + + // holder to holder + env(pay(carol, bob, MPT(1)), + test::jtx::path(~MPT, gw), + sendmax(XRP(1)), + txflags(tfPartialPayment), + ter(temBAD_PATH)); + env.close(); + + // issuer to holder + env(pay(gw, bob, MPT(1)), + test::jtx::path(~MPT, carol), + sendmax(XRP(1)), + txflags(tfPartialPayment), + ter(temBAD_PATH)); + env.close(); + + // holder to issuer + env(pay(bob, gw, MPT(1)), + test::jtx::path(~MPT, carol), + sendmax(XRP(1)), + txflags(tfPartialPayment), + ter(temBAD_PATH)); + env.close(); + } + + // MPTokenV2 is disabled + { + Env env{*this, features - featureMPTokensV2}; + + MPTTester mpt(env, gw, {.holders = {alice}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + + env(pay(gw, alice, MPT(101)), + test::jtx::path(~MPT), + sendmax(XRP(100)), + txflags(tfPartialPayment), + ter(temMALFORMED)); + } + + // MPT/XRP + { + Env env{*this, features}; + MPTTester mpt(env, gw, {.holders = {alice, carol, bob}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + mpt.authorize({.account = bob}); + + env(offer(alice, XRP(100), MPT(101))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{XRP(100), MPT(101)}}})); + + env(pay(carol, bob, MPT(101)), + test::jtx::path(~MPT), + sendmax(XRP(100)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 101)); + } + + // MPT/IOU + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice, carol, bob}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + env(trust(alice, USD(2'000))); + env(pay(gw, alice, USD(1'000))); + env(trust(bob, USD(2'000))); + env(pay(gw, bob, USD(1'000))); + env(trust(carol, USD(2'000))); + env(pay(gw, carol, USD(1'000))); + env.close(); + + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + mpt.authorize({.account = bob}); + + env(offer(alice, USD(100), MPT(101))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{USD(100), MPT(101)}}})); + + env(pay(carol, bob, MPT(101)), + test::jtx::path(~MPT), + sendmax(USD(100)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(env.balance(carol, USD) == USD(900)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 101)); + } + + // IOU/MPT + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice, carol, bob}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + env(trust(alice, USD(2'000)), txflags(tfClearNoRipple)); + env(pay(gw, alice, USD(1'000))); + env(trust(bob, USD(2'000)), txflags(tfClearNoRipple)); + env.close(); + + mpt.authorize({.account = alice}); + env(pay(gw, alice, MPT(200))); + + mpt.authorize({.account = carol}); + env(pay(gw, carol, MPT(200))); + + env(offer(alice, MPT(101), USD(100))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{MPT(101), USD(100)}}})); + + env(pay(carol, bob, USD(100)), + test::jtx::path(~USD), + sendmax(MPT(101)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(env.balance(alice, USD) == USD(900)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 301)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 99)); + BEAST_EXPECT(env.balance(bob, USD) == USD(100)); + } + + // MPT/MPT + { + Env env{*this, features}; + + MPTTester mpt1(env, gw, {.holders = {alice, carol, bob}}); + mpt1.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + + MPTTester mpt2( + env, gw, {.holders = {alice, carol, bob}, .fund = false}); + mpt2.create( + {.ownerCount = 2, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT2 = mpt2["MPT2"]; + + mpt1.authorize({.account = alice}); + mpt1.pay(gw, alice, 200); + mpt2.authorize({.account = alice}); + + mpt2.authorize({.account = carol}); + mpt2.pay(gw, carol, 200); + + mpt1.authorize({.account = bob}); + mpt2.authorize({.account = bob}); + mpt2.pay(gw, bob, 200); + + env(offer(alice, MPT2(100), MPT1(100))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{MPT2(100), MPT1(100)}}})); + + // holder to holder + env(pay(carol, bob, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 190)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 10)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(200)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 10)); + + // issuer to holder + env(pay(gw, bob, MPT1(20)), + test::jtx::path(~MPT1), + sendmax(MPT2(20)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 170)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 30)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(200)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(420)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 30)); + + // holder to issuer + env(pay(bob, gw, MPT1(70)), + test::jtx::path(~MPT1), + sendmax(MPT2(70)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 100)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 100)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(130)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(420)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 30)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 130)); + } + + // MPT/MPT, issuer owns the offer + { + Env env{*this, features}; + + MPTTester mpt1(env, gw, {.holders = {carol, bob}}); + mpt1.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + + MPTTester mpt2(env, gw, {.holders = {carol, bob}, .fund = false}); + mpt2.create( + {.ownerCount = 2, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT2 = mpt2["MPT2"]; + + mpt2.authorize({.account = carol}); + mpt2.pay(gw, carol, 200); + + mpt1.authorize({.account = bob}); + mpt2.authorize({.account = bob}); + mpt2.pay(gw, bob, 200); + + env(offer(gw, MPT2(100), MPT1(100))); + env.close(); + BEAST_EXPECT( + expectOffers(env, gw, 1, {{Amounts{MPT2(100), MPT1(100)}}})); + + // holder to holder + env(pay(carol, bob, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, gw, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(10)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(390)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 10)); + + // issuer to holder + env(pay(gw, bob, MPT1(20)), + test::jtx::path(~MPT1), + sendmax(MPT2(20)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, gw, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(30)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(390)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 30)); + + // holder to issuer + env(pay(bob, gw, MPT1(70)), + test::jtx::path(~MPT1), + sendmax(MPT2(70)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, gw, 0)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(30)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(320)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 30)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 130)); + } + + // MPT/MPT, different issuer + { + Env env{*this, features}; + Account gw1{"gw1"}; + + MPTTester mpt1(env, gw, {.holders = {alice, carol, bob}}); + mpt1.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + + env.fund(XRP(1'000), gw1); + MPTTester mpt2( + env, gw1, {.holders = {alice, carol, bob}, .fund = false}); + mpt2.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT2 = mpt2["MPT2"]; + + mpt1.authorize({.account = alice}); + mpt1.pay(gw, alice, 200); + mpt2.authorize({.account = alice}); + + mpt2.authorize({.account = carol}); + mpt2.pay(gw1, carol, 200); + + mpt1.authorize({.account = bob}); + mpt1.pay(gw, bob, 200); + mpt2.authorize({.account = bob}); + mpt2.pay(gw1, bob, 200); + + mpt1.authorize({.account = gw1}); + mpt1.pay(gw, gw1, 200); + + mpt2.authorize({.account = gw}); + mpt2.pay(gw1, gw, 200); + + env(offer(alice, MPT2(100), MPT1(100))); + env.close(); + BEAST_EXPECT( + expectOffers(env, alice, 1, {{Amounts{MPT2(100), MPT1(100)}}})); + + env(pay(carol, bob, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(600)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(600)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 200)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 200)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 210)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 200)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 190)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 10)); + + env(pay(bob, gw, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(600)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 200)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 200)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 210)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 180)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 20)); + + env(pay(gw, bob, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(600)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 200)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 220)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 170)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 30)); + + env(pay(bob, gw1, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(600)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 210)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 220)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 180)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 160)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 40)); + + env(pay(gw1, bob, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(610)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 210)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 190)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 230)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 180)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 150)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 50)); + + env(pay(gw, gw1, MPT1(10)), + test::jtx::path(~MPT1), + sendmax(MPT2(10)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 1)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(610)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 220)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 180)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 140)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 60)); + + env(pay(gw1, gw, MPT1(40)), + test::jtx::path(~MPT1), + sendmax(MPT2(40)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(550)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(650)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 220)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 180)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 100)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 100)); + } + + // MPT/IOU IOU/MPT1 + { + Env env = pathTestEnv(*this); + Account const gw1{"gw1"}; + Account const gw2{"gw2"}; + Account const dan{"dan"}; + env.fund(XRP(1'000), gw2); + auto const USD = gw2["USD"]; + + MPTTester mpt(env, gw, {.holders = {alice, carol}}); + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = alice}); + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + MPTTester mpt1(env, gw1, {.holders = {bob, dan}}); + mpt1.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + mpt1.authorize({.account = bob}); + mpt1.pay(gw1, bob, 200); + mpt1.authorize({.account = dan}); + + env(trust(alice, USD(400))); + env(pay(gw2, alice, USD(200))); + env(trust(bob, USD(400))); + + env(offer(alice, MPT(100), USD(100))); + env(offer(bob, USD(100), MPT1(100))); + env.close(); + + env(pay(carol, dan, MPT1(100)), + sendmax(MPT(100)), + path(~USD, ~MPT1), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(expectOffers(env, bob, 0)); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 100)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(dan, 100)); + } + + // XRP/MPT AMM + { + Env env{*this, features}; + + fund(env, gw, {alice, carol, bob}, XRP(11'000), {USD(20'000)}); + + MPTTester mpt(env, gw, {.fund = false}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.authorize({.account = bob}); + mpt.pay(gw, alice, 10'100); + + AMM amm(env, alice, XRP(10'000), MPT(10'100)); + + env(pay(carol, bob, MPT(100)), + test::jtx::path(~MPT), + sendmax(XRP(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(XRP(10'100), MPT(10'000), amm.tokens())); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 100)); + } + + // IOU/MPT AMM + { + Env env{*this, features}; + + fund(env, gw, {alice, carol, bob}, XRP(11'000), {USD(20'000)}); + + MPTTester mpt(env, gw, {.fund = false}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.authorize({.account = bob}); + mpt.pay(gw, alice, 10'100); + + AMM amm(env, alice, USD(10'000), MPT(10'100)); + + env(pay(carol, bob, MPT(100)), + test::jtx::path(~MPT), + sendmax(USD(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(USD(10'100), MPT(10'000), amm.tokens())); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 100)); + } + + // MPT/MPT AMM cross-asset payment + { + Env env{*this, features}; + env.fund(XRP(20'000), gw, alice, carol, bob); + env.close(); + + MPTTester mpt1(env, gw, {.fund = false}); + mpt1.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + mpt1.authorize({.account = alice}); + mpt1.authorize({.account = bob}); + mpt1.pay(gw, alice, 10'100); + + MPTTester mpt2(env, gw, {.fund = false}); + mpt2.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT2 = mpt2["MPT1"]; + mpt2.authorize({.account = alice}); + mpt2.authorize({.account = bob}); + mpt2.authorize({.account = carol}); + mpt2.pay(gw, alice, 10'100); + mpt2.pay(gw, carol, 100); + + AMM amm(env, alice, MPT2(10'000), MPT1(10'100)); + + env(pay(carol, bob, MPT1(100)), + test::jtx::path(~MPT1), + sendmax(MPT2(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(MPT2(10'100), MPT1(10'000), amm.tokens())); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 100)); + } + + // Multi-steps with AMM + // EUR/MPT1 MPT1/MPT2 MPT2/USD USD/CRN AMM:CRN/MPT MPT/YAN + { + Env env{*this, features}; + auto const USD = gw["USD"]; + auto const EUR = gw["EUR"]; + auto const CRN = gw["CRN"]; + auto const YAN = gw["YAN"]; + + fund( + env, + gw, + {alice, carol, bob}, + XRP(1'000), + {USD(1'000), EUR(1'000), CRN(2'000), YAN(1'000)}); + + auto createMPT = [&]() -> std::pair { + MPTTester mpt(env, gw, {.fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 2'000); + return {mpt, mpt["MPT"]}; + }; + + auto const [mpt1, MPT1] = createMPT(); + auto const [mpt2, MPT2] = createMPT(); + auto const [mpt3, MPT3] = createMPT(); + + env(offer(alice, EUR(100), MPT1(101))); + env(offer(alice, MPT1(101), MPT2(102))); + env(offer(alice, MPT2(102), USD(103))); + env(offer(alice, USD(103), CRN(104))); + env.close(); + AMM amm(env, alice, CRN(1'000), MPT3(1'104)); + env(offer(alice, MPT3(104), YAN(100))); + + env(pay(carol, bob, YAN(100)), + test::jtx::path(~MPT1, ~MPT2, ~USD, ~CRN, ~MPT3, ~YAN), + sendmax(EUR(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT(env.balance(carol, EUR) == EUR(900)); + BEAST_EXPECT(env.balance(bob, YAN) == YAN(1'100)); + BEAST_EXPECT( + amm.expectBalances(CRN(1'104), MPT3(1'000), amm.tokens())); + BEAST_EXPECT(expectOffers(env, alice, 0)); + } + + // Multi-steps with AMM and MPT endpoints + // MPT1/EUR EUR/MPT2 MPT2/USD USD/CRN AMM:CRN/MPT3 MPT3/MPT4 + { + Env env{*this, features}; + auto const USD = gw["USD"]; + auto const EUR = gw["EUR"]; + auto const CRN = gw["CRN"]; + + fund( + env, + gw, + {alice, carol, bob}, + XRP(1'000), + {USD(1'000), EUR(1'000), CRN(2'000)}); + + auto createMPT = [&]() -> std::pair { + MPTTester mpt(env, gw, {.fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 2'000); + return {mpt, mpt["MPT"]}; + }; + + auto const [mpt1, MPT1] = createMPT(); + auto const [mpt2, MPT2] = createMPT(); + auto const [mpt3, MPT3] = createMPT(); + auto [mpt4, MPT4] = createMPT(); + mpt4.authorize({.account = bob}); + + env(offer(alice, EUR(100), MPT1(101))); + env(offer(alice, MPT1(101), MPT2(102))); + env(offer(alice, MPT2(102), USD(103))); + env(offer(alice, USD(103), CRN(104))); + env.close(); + AMM amm(env, alice, CRN(1'000), MPT3(1'104)); + env(offer(alice, MPT3(104), MPT4(100))); + + env(pay(carol, bob, MPT4(100)), + test::jtx::path(~MPT1, ~MPT2, ~USD, ~CRN, ~MPT3, ~MPT4), + sendmax(EUR(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT(env.balance(carol, EUR) == EUR(900)); + BEAST_EXPECT(mpt4.checkMPTokenAmount(bob, 100)); + BEAST_EXPECT( + amm.expectBalances(CRN(1'104), MPT3(1'000), amm.tokens())); + BEAST_EXPECT(expectOffers(env, alice, 0)); + } + + // Check that limiting step reduces maximumAmount returned by + // MPTEndpointStep::maxPaymentFlow() + { + Env env(*this, features); + + env.fund(XRP(1'000), gw, alice, carol, bob); + + MPTTester usd( + env, gw, {.holders = {alice, carol, bob}, .fund = false}); + usd.create( + {.maxAmt = 1'000, + .authorize = MPTCreate::AllHolders, + .pay = {{{alice}, 1'000}}, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const USD = usd["USD"]; + + MPTTester eur( + env, gw, {.holders = {alice, carol, bob}, .fund = false}); + eur.create( + {.maxAmt = 1'000, + .authorize = {{alice, carol}}, + .pay = {{{carol}, 100}}, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const EUR = eur["EUR"]; + + env(offer(alice, EUR(10), USD(10))); + + env(pay(carol, bob, USD(10)), + sendmax(EUR(10)), + path(~USD), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + } + + void + testPath(FeatureBitset features) + { + testcase("Path"); + using namespace test::jtx; + Account const gw{"gw"}; + Account const gw1{"gw1"}; + Account const alice{"alice"}; + Account const carol{"carol"}; + Account const bob{"bob"}; + Account const dan{"dan"}; + auto const USD = gw["USD"]; + auto const EUR = gw1["EUR"]; + + // MPT can be a mpt end point step or a book-step + + // Direct MPT payment + { + Env env = pathTestEnv(*this); + + MPTTester mpt(env, gw, {.holders = {dan, carol}}); + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = dan}); + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + auto const [pathSet, srcAmt, dstAmt] = + find_paths(env, carol, dan, MPT(-1)); + BEAST_EXPECT(srcAmt == MPT(200)); + BEAST_EXPECT(dstAmt == MPT(200)); + // Direct payment, no path + BEAST_EXPECT(pathSet.empty()); + } + + // Cross-asset payment via XRP/MPT offer (one step) + { + Env env = pathTestEnv(*this); + + env.fund(XRP(1'000), carol); + + MPTTester mpt(env, gw, {.holders = {alice, dan}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.authorize({.account = dan}); + mpt.pay(gw, alice, 200); + + env(offer(alice, XRP(100), MPT(100))); + env.close(); + + auto const [pathSet, srcAmt, dstAmt] = + find_paths(env, carol, dan, MPT(-1)); + BEAST_EXPECT(srcAmt == XRP(100)); + BEAST_EXPECT(dstAmt == MPT(100)); + if (BEAST_EXPECT(same(pathSet, stpath(IPE(mpt.issuanceID()))))) + { + // validate a payment works with the path + env(pay(carol, dan, MPT(10)), + path(~MPT), + sendmax(XRP(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + } + + // Cross-asset payment via IOU/MPT offer (one step) + { + Env env = pathTestEnv(*this); + + env.fund(XRP(1'000), carol); + env.fund(XRP(1'000), gw); + + MPTTester mpt(env, gw1, {.holders = {alice, dan}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.authorize({.account = dan}); + mpt.pay(gw1, alice, 200); + + env(trust(alice, USD(400))); + env(trust(carol, USD(400))); + env(pay(gw, carol, USD(200))); + + env(offer(alice, USD(100), MPT(100))); + env.close(); + + // No sendMax + STPathSet pathSet; + STAmount srcAmt; + STAmount dstAmt; + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, MPT(-1)); + BEAST_EXPECT(srcAmt == USD(100)); + BEAST_EXPECT(dstAmt == MPT(100)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same(pathSet, stpath(gw, IPE(mpt.issuanceID()))))) + { + // Validate the payment works with the path + env(pay(carol, dan, MPT(10)), + path(pathSet[0]), + sendmax(USD(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + + // Include sendMax + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, MPT(-1), USD(-1)); + BEAST_EXPECT(srcAmt == USD(90)); + BEAST_EXPECT(dstAmt == MPT(90)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same(pathSet, stpath(IPE(mpt.issuanceID()))))) + { + // validate a payment works with the path + env(pay(carol, dan, MPT(10)), + path(pathSet[0]), + sendmax(USD(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + + // Include source token + std::tie(pathSet, srcAmt, dstAmt) = find_paths( + env, carol, dan, MPT(-1), std::nullopt, USD.currency); + BEAST_EXPECT(srcAmt == USD(80)); + BEAST_EXPECT(dstAmt == MPT(80)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same(pathSet, stpath(gw, IPE(mpt.issuanceID()))))) + { + // validate a payment works with the path + env(pay(carol, dan, MPT(10)), + path(pathSet[0]), + sendmax(USD(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + } + + // Cross-asset payment via MPT/IOU offer (one step) + { + Env env = pathTestEnv(*this); + + env.fund(XRP(1'000), dan); + env.fund(XRP(1'000), gw); + + MPTTester mpt(env, gw1, {.holders = {carol, alice}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = carol}); + mpt.authorize({.account = alice}); + mpt.pay(gw1, carol, 200); + + env(trust(dan, USD(400))); + env(trust(alice, USD(400))); + env(pay(gw, alice, USD(200))); + + env(offer(alice, MPT(100), USD(100))); + env.close(); + + // No sendMax + STPathSet pathSet; + STAmount srcAmt; + STAmount dstAmt; + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, USD(-1)); + BEAST_EXPECT(srcAmt == MPT(100)); + BEAST_EXPECT(dstAmt == USD(100)); + if (BEAST_EXPECT( + pathSet.size() == 1 && same(pathSet, stpath(IPE(USD))))) + { + // Validate the payment works with the path + env(pay(carol, dan, USD(10)), + path(pathSet[0]), + sendmax(MPT(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + + // Include sendMax + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, USD(-1), MPT(-1)); + BEAST_EXPECT(srcAmt == MPT(90)); + BEAST_EXPECT(dstAmt == USD(90)); + if (BEAST_EXPECT( + pathSet.size() == 1 && same(pathSet, stpath(IPE(USD))))) + { + // validate a payment works with the path + env(pay(carol, dan, USD(10)), + path(pathSet[0]), + sendmax(MPT(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + + // Include source token + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, USD(-1), std::nullopt, MPT.mpt()); + BEAST_EXPECT(srcAmt == MPT(80)); + BEAST_EXPECT(dstAmt == USD(80)); + if (BEAST_EXPECT( + pathSet.size() == 1 && same(pathSet, stpath(IPE(USD))))) + { + // validate a payment works with the path + env(pay(carol, dan, USD(10)), + path(pathSet[0]), + sendmax(MPT(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + } + + // Cross-asset payment via MPT1/MPT offer (one step) + { + Env env = pathTestEnv(*this); + + MPTTester mpt(env, gw, {.holders = {alice, dan}}); + MPTTester mpt1(env, gw1, {.holders = {carol}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt1.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + + mpt.authorize({.account = alice}); + mpt.authorize({.account = dan}); + mpt.pay(gw, alice, 200); + + mpt1.authorize({.account = carol}); + mpt1.authorize({.account = alice}); + mpt1.pay(gw1, carol, 200); + + env(offer(alice, MPT1(100), MPT(100))); + env.close(); + + // No sendMax + STPathSet pathSet; + STAmount srcAmt; + STAmount dstAmt; + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, MPT(-1)); + BEAST_EXPECT(srcAmt == MPT1(100)); + BEAST_EXPECT(dstAmt == MPT(100)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same(pathSet, stpath(IPE(mpt.issuanceID()))))) + { + // validate a payment works with the path + env(pay(carol, dan, MPT(10)), + path(pathSet[0]), + sendmax(MPT1(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + + // Include sendMax + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, MPT(-1), MPT1(-1)); + BEAST_EXPECT(srcAmt == MPT1(90)); + BEAST_EXPECT(dstAmt == MPT(90)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same(pathSet, stpath(IPE(mpt.issuanceID()))))) + { + // validate a payment works with the path + env(pay(carol, dan, MPT(10)), + path(pathSet[0]), + sendmax(MPT1(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + + // Include source token + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, MPT(-1), std::nullopt, MPT1.mpt()); + BEAST_EXPECT(srcAmt == MPT1(80)); + BEAST_EXPECT(dstAmt == MPT(80)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same(pathSet, stpath(IPE(mpt.issuanceID()))))) + { + // validate a payment works with the path + env(pay(carol, dan, MPT(10)), + path(pathSet[0]), + sendmax(MPT1(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + } + + // Cross-asset payment via offers (two steps) + { + Env env = pathTestEnv(*this); + + env.fund(XRP(1'000), carol); + env.fund(XRP(1'000), dan); + + MPTTester mpt(env, gw, {.holders = {alice, bob}}); + + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = alice}); + mpt.authorize({.account = bob}); + mpt.pay(gw, alice, 200); + mpt.pay(gw, bob, 200); + + env(trust(bob, USD(200))); + env(pay(gw, bob, USD(100))); + env(trust(dan, USD(200))); + env(trust(alice, USD(200))); + + env(offer(alice, XRP(100), MPT(100))); + env(offer(bob, MPT(100), USD(100))); + env.close(); + + // No sendMax + STPathSet pathSet; + STAmount srcAmt; + STAmount dstAmt; + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, USD(-1)); + BEAST_EXPECT(srcAmt == XRP(100)); + BEAST_EXPECT(dstAmt == USD(100)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same(pathSet, stpath(IPE(mpt.issuanceID()), IPE(USD))))) + { + // validate a payment works with the path + env(pay(carol, dan, USD(10)), + path(pathSet[0]), + sendmax(XRP(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + + // Include sendMax + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, USD(-1), XRP(100)); + BEAST_EXPECT(srcAmt == XRP(90)); + BEAST_EXPECT(dstAmt == USD(90)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same(pathSet, stpath(IPE(mpt.issuanceID()), IPE(USD))))) + { + // validate a payment works with the path + env(pay(carol, dan, USD(10)), + path(pathSet[0]), + sendmax(XRP(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + } + + // Cross-asset payment via offers (two steps) + // Start/End with mpt/mp1 and book steps in the middle + { + Env env = pathTestEnv(*this); + Account const gw2{"gw2"}; + env.fund(XRP(1'000), gw2); + auto const USD2 = gw2["USD"]; + + MPTTester mpt(env, gw, {.holders = {alice, carol}}); + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = alice}); + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + MPTTester mpt1(env, gw1, {.holders = {bob, dan}}); + mpt1.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + mpt1.authorize({.account = bob}); + mpt1.pay(gw1, bob, 200); + mpt1.authorize({.account = dan}); + + env(trust(alice, USD2(400))); + env(pay(gw2, alice, USD2(200))); + env(trust(bob, USD2(400))); + + env(offer(alice, MPT(100), USD2(100))); + env(offer(bob, USD2(100), MPT1(100))); + env.close(); + + // No sendMax + STPathSet pathSet; + STAmount srcAmt; + STAmount dstAmt; + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, MPT1(-1)); + BEAST_EXPECT(srcAmt == MPT(100)); + BEAST_EXPECT(dstAmt == MPT1(100)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same(pathSet, stpath(IPE(USD2), IPE(mpt1.issuanceID()))))) + { + // validate a payment works with the path + env(pay(carol, dan, MPT1(10)), + path(pathSet[0]), + sendmax(MPT(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + + // Include sendMax + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, MPT1(-1), MPT(-1)); + BEAST_EXPECT(srcAmt == MPT(90)); + BEAST_EXPECT(dstAmt == MPT1(90)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same(pathSet, stpath(IPE(USD2), IPE(mpt1.issuanceID()))))) + { + // validate a payment works with the path + env(pay(carol, dan, MPT1(10)), + path(pathSet[0]), + sendmax(MPT(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + + // Include source token + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, MPT1(-1), std::nullopt, MPT.mpt()); + BEAST_EXPECT(srcAmt == MPT(80)); + BEAST_EXPECT(dstAmt == MPT1(80)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same(pathSet, stpath(IPE(USD2), IPE(mpt1.issuanceID()))))) + { + // validate a payment works with the path + env(pay(carol, dan, MPT1(10)), + path(pathSet[0]), + sendmax(MPT(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + } + + // Cross-asset payment via offers (two steps) + // Start/End with mpt/mp2 and book steps in the middle + // offers are MPT/MPT + { + Env env = pathTestEnv(*this); + Account const gw2{"gw2"}; + env.fund(XRP(1'000), gw, gw1, gw2, alice, bob, carol, dan); + + MPTTester mpt(env, gw, {.holders = {alice, carol}, .fund = false}); + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = alice}); + mpt.authorize({.account = carol}); + mpt.pay(gw, carol, 200); + + MPTTester mpt1(env, gw1, {.holders = {bob, alice}, .fund = false}); + mpt1.create( + {.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + mpt1.authorize({.account = alice}); + mpt1.pay(gw1, alice, 200); + mpt1.authorize({.account = bob}); + + MPTTester mpt2(env, gw2, {.holders = {bob, dan}, .fund = false}); + mpt2.create( + {.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT2 = mpt2["MPT2"]; + mpt2.authorize({.account = bob}); + mpt2.pay(gw2, bob, 200); + mpt2.authorize({.account = dan}); + + env(offer(alice, MPT(100), MPT1(100))); + env(offer(bob, MPT1(100), MPT2(100))); + env.close(); + + // No sendMax + STPathSet pathSet; + STAmount srcAmt; + STAmount dstAmt; + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, MPT2(-1)); + BEAST_EXPECT(srcAmt == MPT(100)); + BEAST_EXPECT(dstAmt == MPT2(100)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same( + pathSet, + stpath( + IPE(mpt1.issuanceID()), IPE(mpt2.issuanceID()))))) + { + // validate a payment works with the path + env(pay(carol, dan, MPT2(10)), + path(pathSet[0]), + sendmax(MPT(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + + // Include sendMax + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, MPT2(-1), MPT(-1)); + BEAST_EXPECT(srcAmt == MPT(90)); + BEAST_EXPECT(dstAmt == MPT2(90)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same( + pathSet, + stpath( + IPE(mpt1.issuanceID()), IPE(mpt2.issuanceID()))))) + { + // validate a payment works with the path + env(pay(carol, dan, MPT2(10)), + path(pathSet[0]), + sendmax(MPT(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + + // Include source token + std::tie(pathSet, srcAmt, dstAmt) = + find_paths(env, carol, dan, MPT2(-1), std::nullopt, MPT.mpt()); + BEAST_EXPECT(srcAmt == MPT(80)); + BEAST_EXPECT(dstAmt == MPT2(80)); + if (BEAST_EXPECT( + pathSet.size() == 1 && + same( + pathSet, + stpath( + IPE(mpt1.issuanceID()), IPE(mpt2.issuanceID()))))) + { + // validate a payment works with the path + env(pay(carol, dan, MPT2(10)), + path(pathSet[0]), + sendmax(MPT(10)), + txflags(tfNoRippleDirect | tfPartialPayment)); + } + } + + // verify no MPT rippling + { + Env env = pathTestEnv(*this); + Account const gw{"gw"}; + Account const gw1{"gw1"}; + Account const carol{"carol"}; + Account const bob{"bob"}; + Account const dan{"dan"}; + Account const john{"john"}; + Account const sean{"sean"}; + + env.fund(XRP(1'000'000), gw); + env.fund(XRP(1'000'000), gw1); + env.fund(XRP(1'000'000), carol); + env.fund(XRP(1'000'000), dan); + env.fund(XRP(1'000'000), bob); + env.fund(XRP(1'000'000), john); + env.fund(XRP(1'000'000), sean); + env.close(); + + MPTTester usd(env, gw, {.holders = {carol, dan}, .fund = false}); + usd.create( + {.authorize = MPTCreate::AllHolders, + .pay = {{MPTCreate::AllHolders, 100}}, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const USD = usd["USD"]; + env(offer(carol, XRP(100), USD(100))); + + MPTTester gbp(env, gw, {.holders = {bob, sean}, .fund = false}); + gbp.create( + {.authorize = MPTCreate::AllHolders, + .pay = {{{bob}, 100}}, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const GBP = gbp["GBP"]; + + MPTTester usd1(env, gw1, {.holders = {bob, dan}, .fund = false}); + usd1.create( + {.authorize = MPTCreate::AllHolders, + .pay = {{{dan}, 100}}, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const USD1 = usd1["USD1"]; + env(offer(bob, USD1(100), GBP(100))); + + // dan has USD/gw and USD1/gw. Had USD been IOU, it would have + // rippled through dan's account. + auto const [pathSet, srcAmt, dstAmt] = + find_paths(env, john, sean, GBP(-1), XRP(-1)); + BEAST_EXPECT(pathSet.size() == 0); + + env(pay(john, sean, GBP(10)), + sendmax(XRP(20)), + path(~USD, dan, gw1, ~GBP), + txflags(tfNoRippleDirect | tfPartialPayment), + ter(temBAD_PATH)); + } + } + + void + testCheck(FeatureBitset features) + { + testcase("Check Create/Cash"); + + using namespace test::jtx; + Account const gw{"gw"}; + Account const alice{"alice"}; + + // MPTokensV2 is disabled + { + Env env{*this, features - featureMPTokensV2}; + + MPTTester mpt(env, gw, {.holders = {alice}}); + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = alice}); + + uint256 const checkId{keylet::check(gw, env.seq(gw)).key}; + + env(check::create(gw, alice, MPT(100)), ter(temDISABLED)); + env.close(); + + env(check::cash(alice, checkId, MPT(100)), ter(temDISABLED)); + env.close(); + } + + // Insufficient funds + { + Env env{*this, features}; + Account const carol{"carol"}; + + MPTTester mpt(env, gw, {.holders = {alice, carol}}); + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 50); + + uint256 const checkId{keylet::check(alice, env.seq(alice)).key}; + + // can create + env(check::create(alice, carol, MPT(100))); + env.close(); + + // can't cash since alice only has 50 of MPT + env(check::cash(carol, checkId, MPT(100)), ter(tecPATH_PARTIAL)); + env.close(); + + // can cash if DeliverMin is set + // carol is not authorized, MPToken is authorized by CheckCash + env(check::cash(carol, checkId, check::DeliverMin(MPT(50)))); + env.close(); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 50)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(50)); + } + + // Exceed max amount + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice}}); + mpt.create( + {.maxAmt = 100, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + + uint256 const checkId{keylet::check(gw, env.seq(gw)).key}; + + // can create + env(check::create(gw, alice, MPT(200))); + env.close(); + + // can't cash since the outstanding amount exceeds max amount + env(check::cash(alice, checkId, MPT(200)), ter(tecPATH_PARTIAL)); + env.close(); + + // can cash if DeliverMin is set + env(check::cash(alice, checkId, check::DeliverMin(MPT(100)))); + env.close(); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 100)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(100)); + } + + // Normal create/cash + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {alice}}); + mpt.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = alice}); + + uint256 const checkId{keylet::check(gw, env.seq(gw)).key}; + + env(check::create(gw, alice, MPT(100))); + env.close(); + + env(check::cash(alice, checkId, MPT(100))); + env.close(); + + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 100)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(100)); + } + } + + void + testAMMClawback(FeatureBitset features) + { + using namespace jtx; + testcase("AMMClawback"); + + { + Account const gw{"gw"}; + Account const alice{"alice"}; + auto const USD = gw["USD"]; + Env env(*this, features); + fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}); + MPTTester mpt(env, gw, {.fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + AMM amm(env, gw, MPT(100), XRP(100)); + amm.deposit(DepositArg{.account = alice, .asset1In = XRP(10)}); + amm::ammClawback( + gw, alice, MPTIssue(mpt.issuanceID()), xrpIssue(), MPT(10)); + } + + { + Account const gw{"gw"}; + Account const alice{"alice"}; + auto const USD = gw["USD"]; + Env env(*this, features); + fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}); + MPTTester mpt(env, gw, {.fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + mpt.authorize({.account = alice}); + mpt.pay(gw, alice, 1'000); + auto const MPT = mpt["MPT"]; + AMM amm(env, gw, MPT(100), XRP(100)); + amm.deposit(DepositArg{.account = alice, .tokens = 10'000}); + amm::ammClawback( + gw, alice, MPTIssue(mpt.issuanceID()), xrpIssue(), MPT(10)); + } + } + + void + testBasicAMM(FeatureBitset features) + { + testcase("Basic AMM"); + using namespace jtx; + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const carol{"carol"}; + Account const bob{"bob"}; + auto const USD = gw["USD"]; + Env env{*this}; + + fund(env, gw, {alice, carol, bob}, XRP(1'000), {USD(1'000)}); + + MPTTester mpt(env, gw, {.fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + mpt.authorize({.account = alice}); + mpt.authorize({.account = carol}); + mpt.pay(gw, alice, 1'000); + mpt.pay(gw, carol, 1'000); + + MPTTester mpt1(env, gw, {.fund = false}); + mpt1.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT1 = mpt1["MPT1"]; + mpt1.authorize({.account = alice}); + mpt1.authorize({.account = carol}); + mpt1.pay(gw, alice, 1'000); + mpt1.pay(gw, carol, 1'000); + + std::vector> pools = { + {XRP(100), MPT(100), IOUAmount{100'000}}, + {USD(100), MPT(100), IOUAmount{100}}, + {MPT(100), MPT1(100), IOUAmount{100}}}; + for (auto& pool : pools) + { + AMM amm(env, gw, std::get<0>(pool), std::get<1>(pool)); + amm.deposit(alice, std::get<2>(pool)); + amm.deposit(carol, std::get<2>(pool)); + // bob doesn't own MPT + amm.deposit(DepositArg{ + .account = bob, + .tokens = std::get<2>(pool), + .err = ter(tecNO_AUTH)}); + amm.withdrawAll(alice); + amm.withdrawAll(carol); + amm.withdrawAll(gw); + BEAST_EXPECT(!amm.ammExists()); + } + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + + // MPTokenIssuanceCreate + testCreateValidation(all); + testCreateEnabled(all); + + // MPTokenIssuanceDestroy + testDestroyValidation(all); + testDestroyValidation(all - featureMPTokensV2); + testDestroyEnabled(all); + + // MPTokenAuthorize + testAuthorizeValidation(all); + testAuthorizeValidation(all - featureMPTokensV2); + testAuthorizeEnabled(all); + + // MPTokenIssuanceSet + testSetValidation(all); + testSetEnabled(all); + + // MPT clawback + testClawbackValidation(all); + testClawbackValidation(all - featureMPTokensV2); + testClawback(all); + testClawback(all - featureMPTokensV2); + + // Test Direct Payment + testPayment(all); + testPayment(all - featureMPTokensV2); + + testDepositPreauth(all); + testDepositPreauth(all - featureMPTokensV2); + + // Test MPT Amount is invalid in Tx, which don't support MPT + testMPTInvalidInTx(all); + + // Test parsed MPTokenIssuanceID in API response metadata + testTxJsonMetaFields(all); + + // Test tokens equality + testTokensEquality(); + + // Test helpers + testHelperFunctions(); + + // Test offer crossing + testOfferCrossing(all); + + // Test cross asset payment + testCrossAssetPayment(all); + + // Test path finding + testPath(all); + + // Test checks + testCheck(all); + + // Add AMMClawback + testAMMClawback(all); + + // Test AMM + testBasicAMM(all); } }; diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index ec1ee5111e4..92469e366bd 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -2448,15 +2448,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite env.close(); STAmount const startXAUBalance( - gwXAU.issue(), STAmount::cMinValue, STAmount::cMinOffset + 5); + gwXAU, STAmount::cMinValue, STAmount::cMinOffset + 5); env(pay(gw, alice, startXAUBalance)); env(pay(gw, minter, startXAUBalance)); env(pay(gw, becky, startXAUBalance)); env.close(); // Here is the smallest expressible gwXAU amount. - STAmount tinyXAU( - gwXAU.issue(), STAmount::cMinValue, STAmount::cMinOffset); + STAmount tinyXAU(gwXAU, STAmount::cMinValue, STAmount::cMinOffset); // minter buys the nft for tinyXAU. Since the transfer involves // alice there should be no transfer fee. @@ -2494,7 +2493,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite // carol sells to becky. This is the smallest gwXAU amount // to pay for a transfer that enables a transfer fee of 1. STAmount const cheapNFT( - gwXAU.issue(), STAmount::cMinValue, STAmount::cMinOffset + 5); + gwXAU, STAmount::cMinValue, STAmount::cMinOffset + 5); STAmount beckyBalance = env.balance(becky, gwXAU); uint256 const beckyBuyOfferIndex = diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 2b4245a1ae4..208a51e3365 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -487,7 +487,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite auto tinyAmount = [&](IOU const& iou) -> PrettyAmount { STAmount amt( - iou.issue(), + iou, /*mantissa*/ 1, /*exponent*/ -81); return PrettyAmount(amt, iou.account.name()); @@ -1321,20 +1321,12 @@ class OfferBaseUtil_test : public beast::unit_test::suite BEAST_EXPECT(jrr[jss::offers].isArray()); BEAST_EXPECT(jrr[jss::offers].size() == 0); - // NOTE : // At this point, all offers are expected to be consumed. - // Alas, they are not - because of a bug in the Taker auto-bridging - // implementation which is addressed by fixTakerDryOfferRemoval. - // The pre-fixTakerDryOfferRemoval implementation (incorrect) leaves - // an empty offer in the second leg of the bridge. Validate both the - // old and the new behavior. { auto acctOffers = offersOnAccount(env, account_to_test); - bool const noStaleOffers{ - features[featureFlowCross] || - features[fixTakerDryOfferRemoval]}; - BEAST_EXPECT(acctOffers.size() == (noStaleOffers ? 0 : 1)); + // No stale offers + BEAST_EXPECT(acctOffers.size() == 0); for (auto const& offerPtr : acctOffers) { auto const& offer = *offerPtr; @@ -1406,7 +1398,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite auto const alice_initial_balance = drops(499946999680); auto const bob_initial_balance = drops(10199999920); auto const small_amount = - STAmount{bob["USD"].issue(), UINT64_C(2710505431213761), -33}; + STAmount{bob["USD"], UINT64_C(2710505431213761), -33}; env.fund(gw_initial_balance, gw); env.fund(alice_initial_balance, alice); @@ -1443,8 +1435,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite std::uint32_t const bobOfferSeq = env.seq(bob); env(offer(bob, XRP(2000), USD(1))); - if (localFeatures[featureFlowCross] && - localFeatures[fixReducedOffersV2]) + if (localFeatures[fixReducedOffersV2]) { // With the rounding introduced by fixReducedOffersV2, bob's // offer does not cross alice's offer and goes straight into @@ -1468,8 +1459,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite // crossing algorithms becomes apparent. The old offer crossing // would consume small_amount and transfer no XRP. The new offer // crossing transfers a single drop, rather than no drops. - auto const crossingDelta = - localFeatures[featureFlowCross] ? drops(1) : drops(0); + auto const crossingDelta = drops(1); jrr = ledgerEntryState(env, alice, gw, "USD"); BEAST_EXPECT( @@ -2013,15 +2003,9 @@ class OfferBaseUtil_test : public beast::unit_test::suite env.require(balance(carol, USD(0))); env.require(balance(carol, EUR(none))); - // If neither featureFlowCross nor fixTakerDryOfferRemoval are defined - // then carol's offer will be left on the books, but with zero value. - int const emptyOfferCount{ - features[featureFlowCross] || features[fixTakerDryOfferRemoval] - ? 0 - : 1}; - - env.require(offers(carol, 0 + emptyOfferCount)); - env.require(owners(carol, 1 + emptyOfferCount)); + + env.require(offers(carol, 0)); + env.require(owners(carol, 1)); } void @@ -2302,12 +2286,11 @@ class OfferBaseUtil_test : public beast::unit_test::suite jtx::Account const& account, jtx::PrettyAmount const& expectBalance) { - auto const sleTrust = - env.le(keylet::line(account.id(), expectBalance.value().issue())); + Issue const& issue = expectBalance.value().get(); + auto const sleTrust = env.le(keylet::line(account.id(), issue)); BEAST_EXPECT(sleTrust); if (sleTrust) { - Issue const issue = expectBalance.value().issue(); bool const accountLow = account.id() < issue.account; STAmount low{issue}; @@ -2444,7 +2427,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite } std::uint32_t const acctOfferSeq = env.seq(acct) - 1; - BEAST_EXPECT(env.balance(acct, USD.issue()) == t.balanceUsd); + BEAST_EXPECT(env.balance(acct, USD) == t.balanceUsd); BEAST_EXPECT( env.balance(acct, xrpIssue()) == t.fundXrp - t.spentXrp); env.require(offers(acct, t.offers)); @@ -2471,8 +2454,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite else { // Verify that no trustline was created. - auto const sleTrust = - env.le(keylet::line(acct, USD.issue())); + auto const sleTrust = env.le(keylet::line(acct, USD)); BEAST_EXPECT(!sleTrust); } } @@ -2660,8 +2642,8 @@ class OfferBaseUtil_test : public beast::unit_test::suite env.require(offers(bob, 0)); // The two trustlines that were generated by offers should be gone. - BEAST_EXPECT(!env.le(keylet::line(alice.id(), EUR.issue()))); - BEAST_EXPECT(!env.le(keylet::line(bob.id(), USD.issue()))); + BEAST_EXPECT(!env.le(keylet::line(alice.id(), EUR))); + BEAST_EXPECT(!env.le(keylet::line(bob.id(), USD))); // Make two more offers that leave one of the offers non-dry. We // need to properly sequence the transactions: @@ -2938,7 +2920,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite std::uint32_t const acctOfferSeq = env.seq(acct) - 1; // Check results - BEAST_EXPECT(env.balance(acct, USD.issue()) == t.finalUsd); + BEAST_EXPECT(env.balance(acct, USD) == t.finalUsd); BEAST_EXPECT( env.balance(acct, xrpIssue()) == t.fundXrp - t.spentXrp); env.require(offers(acct, t.offers)); @@ -3850,7 +3832,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT( offer[sfTakerGets] == - STAmount(JPY.issue(), std::uint64_t(2230682446713524ul), -12)); + STAmount(JPY, std::uint64_t(2230682446713524ul), -12)); BEAST_EXPECT(offer[sfTakerPays] == BTC(0.035378)); } } @@ -3887,24 +3869,24 @@ class OfferBaseUtil_test : public beast::unit_test::suite env( pay(gw1, alice, - STAmount{USD.issue(), std::uint64_t(2185410179555600), -14})); + STAmount{USD, std::uint64_t(2185410179555600), -14})); env( pay(gw2, bob, - STAmount{JPY.issue(), std::uint64_t(6351823459548956), -12})); + STAmount{JPY, std::uint64_t(6351823459548956), -12})); env.close(); env(offer( bob, - STAmount{USD.issue(), std::uint64_t(4371257532306000), -17}, - STAmount{JPY.issue(), std::uint64_t(4573216636606000), -15})); + STAmount{USD, std::uint64_t(4371257532306000), -17}, + STAmount{JPY, std::uint64_t(4573216636606000), -15})); env.close(); // This offer did not partially cross correctly. env(offer( alice, - STAmount{JPY.issue(), std::uint64_t(2291181510070762), -12}, - STAmount{USD.issue(), std::uint64_t(2190218999914694), -14})); + STAmount{JPY, std::uint64_t(2291181510070762), -12}, + STAmount{USD, std::uint64_t(2190218999914694), -14})); env.close(); auto const aliceOffers = offersOnAccount(env, alice); @@ -3915,10 +3897,10 @@ class OfferBaseUtil_test : public beast::unit_test::suite BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT( offer[sfTakerGets] == - STAmount(USD.issue(), std::uint64_t(2185847305256635), -14)); + STAmount(USD, std::uint64_t(2185847305256635), -14)); BEAST_EXPECT( offer[sfTakerPays] == - STAmount(JPY.issue(), std::uint64_t(2286608293434156), -12)); + STAmount(JPY, std::uint64_t(2286608293434156), -12)); } } @@ -3947,21 +3929,21 @@ class OfferBaseUtil_test : public beast::unit_test::suite // Place alice's tiny offer in the book first. Let's see what happens // when a reasonable offer crosses it. STAmount const alicesCnyOffer{ - CNY.issue(), std::uint64_t(4926000000000000), -23}; + CNY, std::uint64_t(4926000000000000), -23}; env(offer(alice, alicesCnyOffer, drops(1), tfPassive)); env.close(); // bob places an ordinary offer STAmount const bobsCnyStartBalance{ - CNY.issue(), std::uint64_t(3767479960090235), -15}; + CNY, std::uint64_t(3767479960090235), -15}; env(pay(gw, bob, bobsCnyStartBalance)); env.close(); env(offer( bob, drops(203), - STAmount{CNY.issue(), std::uint64_t(1000000000000000), -20})); + STAmount{CNY, std::uint64_t(1000000000000000), -20})); env.close(); env.require(balance(alice, alicesCnyOffer)); @@ -4197,12 +4179,6 @@ class OfferBaseUtil_test : public beast::unit_test::suite }; // clang-format off - TestData const takerTests[]{ - // btcStart ------------------- actor[0] -------------------- ------------------- actor[1] -------------------- - {0, 0, 1, BTC(5), {{"deb", 0, drops(3899999999960), BTC(5), USD(3000)}, {"dan", 0, drops(4099999999970), BTC(0), USD(750)}}}, // no BTC xfer fee - {0, 0, 0, BTC(5), {{"flo", 0, drops(3999999999950), BTC(5), USD(2000)} }} // no xfer fee - }; - TestData const flowTests[]{ // btcStart ------------------- actor[0] -------------------- ------------------- actor[1] -------------------- {0, 0, 1, BTC(5), {{"gay", 1, drops(3949999999960), BTC(5), USD(2500)}, {"gar", 1, drops(4049999999970), BTC(0), USD(1375)}}}, // no BTC xfer fee @@ -4210,10 +4186,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite }; // clang-format on - // Pick the right tests. - auto const& tests = features[featureFlowCross] ? flowTests : takerTests; - - for (auto const& t : tests) + for (auto const& t : flowTests) { Account const& self = t.actors[t.self].acct; Account const& leg0 = t.actors[t.leg0].acct; @@ -4339,8 +4312,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite // 1. alice creates an offer to acquire USD/gw, an asset for which // she does not have a trust line. At some point in the future, // gw adds lsfRequireAuth. Then, later, alice's offer is crossed. - // a. With Taker alice's unauthorized offer is consumed. - // b. With FlowCross alice's offer is deleted, not consumed, + // alice's offer is deleted, not consumed, // since alice is not authorized to hold USD/gw. // // 2. alice tries to create an offer for USD/gw, now that gw has @@ -4389,33 +4361,18 @@ class OfferBaseUtil_test : public beast::unit_test::suite // gw now requires authorization and bob has gwUSD(50). Let's see if // bob can cross alice's offer. // - // o With Taker bob's offer should cross alice's. - // o With FlowCross bob's offer shouldn't cross and alice's + // bob's offer shouldn't cross and alice's // unauthorized offer should be deleted. env(offer(bob, XRP(4000), gwUSD(40))); env.close(); std::uint32_t const bobOfferSeq = env.seq(bob) - 1; - bool const flowCross = features[featureFlowCross]; - env.require(offers(alice, 0)); - if (flowCross) - { - // alice's unauthorized offer is deleted & bob's offer not crossed. - env.require(balance(alice, gwUSD(none))); - env.require(offers(bob, 1)); - env.require(balance(bob, gwUSD(50))); - } - else - { - // alice's offer crosses bob's - env.require(balance(alice, gwUSD(40))); - env.require(offers(bob, 0)); - env.require(balance(bob, gwUSD(10))); - // The rest of the test verifies FlowCross behavior. - return; - } + // alice's unauthorized offer is deleted & bob's offer not crossed. + env.require(balance(alice, gwUSD(none))); + env.require(offers(bob, 1)); + env.require(balance(bob, gwUSD(50))); // See if alice can create an offer without authorization. alice // should not be able to create the offer and bob's offer should be @@ -5144,9 +5101,7 @@ class OfferBaseUtil_test : public beast::unit_test::suite // tfFillOrKill, TakerPays must be filled { TER const err = - features[fixFillOrKill] || !features[featureFlowCross] - ? TER(tesSUCCESS) - : tecKILLED; + features[fixFillOrKill] ? TER(tesSUCCESS) : tecKILLED; env(offer(maker, XRP(100), USD(100))); env.close(); @@ -5368,7 +5323,6 @@ class OfferBaseUtil_test : public beast::unit_test::suite { using namespace jtx; static FeatureBitset const all{supported_amendments()}; - static FeatureBitset const flowCross{featureFlowCross}; static FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval}; static FeatureBitset const rmSmallIncreasedQOffers{ fixRmSmallIncreasedQOffers}; @@ -5376,10 +5330,9 @@ class OfferBaseUtil_test : public beast::unit_test::suite featureImmediateOfferKilled}; FeatureBitset const fillOrKill{fixFillOrKill}; - static std::array const feats{ + static std::array const feats{ all - takerDryOffer - immediateOfferKilled, - all - flowCross - takerDryOffer - immediateOfferKilled, - all - flowCross - immediateOfferKilled, + all - immediateOfferKilled, all - rmSmallIncreasedQOffers - immediateOfferKilled - fillOrKill, all - fillOrKill, all}; @@ -5399,21 +5352,12 @@ class OfferBaseUtil_test : public beast::unit_test::suite } }; -class OfferWOFlowCross_test : public OfferBaseUtil_test -{ - void - run() override - { - OfferBaseUtil_test::run(1); - } -}; - class OfferWTakerDryOffer_test : public OfferBaseUtil_test { void run() override { - OfferBaseUtil_test::run(2); + OfferBaseUtil_test::run(1); } }; @@ -5422,7 +5366,7 @@ class OfferWOSmallQOffers_test : public OfferBaseUtil_test void run() override { - OfferBaseUtil_test::run(3); + OfferBaseUtil_test::run(2); } }; @@ -5431,7 +5375,7 @@ class OfferWOFillOrKill_test : public OfferBaseUtil_test void run() override { - OfferBaseUtil_test::run(4); + OfferBaseUtil_test::run(3); } }; @@ -5440,7 +5384,7 @@ class OfferAllFeatures_test : public OfferBaseUtil_test void run() override { - OfferBaseUtil_test::run(5, true); + OfferBaseUtil_test::run(4, true); } }; @@ -5451,24 +5395,22 @@ class Offer_manual_test : public OfferBaseUtil_test { using namespace jtx; FeatureBitset const all{supported_amendments()}; - FeatureBitset const flowCross{featureFlowCross}; FeatureBitset const f1513{fix1513}; FeatureBitset const immediateOfferKilled{featureImmediateOfferKilled}; FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval}; FeatureBitset const fillOrKill{fixFillOrKill}; - testAll(all - flowCross - f1513 - immediateOfferKilled); - testAll(all - flowCross - immediateOfferKilled); + testAll(all - f1513 - immediateOfferKilled); + testAll(all - immediateOfferKilled); testAll(all - immediateOfferKilled - fillOrKill); testAll(all - fillOrKill); testAll(all); - testAll(all - flowCross - takerDryOffer); + testAll(all - takerDryOffer); } }; BEAST_DEFINE_TESTSUITE_PRIO(OfferBaseUtil, tx, ripple, 2); -BEAST_DEFINE_TESTSUITE_PRIO(OfferWOFlowCross, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWTakerDryOffer, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWOSmallQOffers, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWOFillOrKill, tx, ripple, 2); diff --git a/src/test/app/Path_test.cpp b/src/test/app/Path_test.cpp index 1db15388ff0..ba7db00be0f 100644 --- a/src/test/app/Path_test.cpp +++ b/src/test/app/Path_test.cpp @@ -19,7 +19,7 @@ #include #include -#include +#include #include #include #include @@ -736,8 +736,7 @@ class Path_test : public beast::unit_test::suite jv); auto const jv_l = - env.le(keylet::line( - Account("bob").id(), Account("alice")["USD"].issue())) + env.le(keylet::line(Account("bob").id(), Account("alice")["USD"])) ->getJson(JsonOptions::none); for (auto it = jv.begin(); it != jv.end(); ++it) BEAST_EXPECT(*it == jv_l[it.memberName()]); @@ -779,8 +778,7 @@ class Path_test : public beast::unit_test::suite jv); auto const jv_l = - env.le(keylet::line( - Account("bob").id(), Account("alice")["USD"].issue())) + env.le(keylet::line(Account("bob").id(), Account("alice")["USD"])) ->getJson(JsonOptions::none); for (auto it = jv.begin(); it != jv.end(); ++it) BEAST_EXPECT(*it == jv_l[it.memberName()]); @@ -789,8 +787,7 @@ class Path_test : public beast::unit_test::suite env.trust(Account("alice")["USD"](0), "bob"); BEAST_EXPECT( env.le(keylet::line( - Account("bob").id(), Account("alice")["USD"].issue())) == - nullptr); + Account("bob").id(), Account("alice")["USD"])) == nullptr); } void @@ -833,8 +830,7 @@ class Path_test : public beast::unit_test::suite jv); auto const jv_l = - env.le(keylet::line( - Account("alice").id(), Account("bob")["USD"].issue())) + env.le(keylet::line(Account("alice").id(), Account("bob")["USD"])) ->getJson(JsonOptions::none); for (auto it = jv.begin(); it != jv.end(); ++it) BEAST_EXPECT(*it == jv_l[it.memberName()]); @@ -842,8 +838,7 @@ class Path_test : public beast::unit_test::suite env(pay("alice", "bob", Account("alice")["USD"](50))); BEAST_EXPECT( env.le(keylet::line( - Account("alice").id(), Account("bob")["USD"].issue())) == - nullptr); + Account("alice").id(), Account("bob")["USD"])) == nullptr); } void diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp index f00a7361292..58408926c68 100644 --- a/src/test/app/PayStrand_test.cpp +++ b/src/test/app/PayStrand_test.cpp @@ -681,7 +681,7 @@ struct PayStrand_test : public beast::unit_test::suite alice, /*deliver*/ xrpIssue(), /*limitQuality*/ std::nullopt, - /*sendMaxIssue*/ EUR.issue(), + /*sendMaxIssue*/ EUR, path, true, OfferCrossing::no, @@ -698,7 +698,7 @@ struct PayStrand_test : public beast::unit_test::suite alice, /*deliver*/ xrpIssue(), /*limitQuality*/ std::nullopt, - /*sendMaxIssue*/ EUR.issue(), + /*sendMaxIssue*/ EUR, path, true, OfferCrossing::no, @@ -736,7 +736,7 @@ struct PayStrand_test : public beast::unit_test::suite test( env, EUR, - USD.issue(), + USD, STPath(), tesSUCCESS, D{alice, gw, usdC}, @@ -747,7 +747,7 @@ struct PayStrand_test : public beast::unit_test::suite test( env, EUR, - USD.issue(), + USD, STPath({ipe(EUR)}), tesSUCCESS, D{alice, gw, usdC}, @@ -759,7 +759,7 @@ struct PayStrand_test : public beast::unit_test::suite test( env, carol["USD"], - USD.issue(), + USD, STPath({iape(carol)}), tesSUCCESS, D{alice, gw, usdC}, @@ -781,7 +781,7 @@ struct PayStrand_test : public beast::unit_test::suite test( env, xrpIssue(), - USD.issue(), + USD, STPath({STPathElement{ STPathElement::typeCurrency, xrpAccount(), @@ -796,7 +796,7 @@ struct PayStrand_test : public beast::unit_test::suite test( env, EUR, - USD.issue(), + USD, STPath({cpe(xrpCurrency())}), tesSUCCESS, D{alice, gw, usdC}, @@ -818,7 +818,7 @@ struct PayStrand_test : public beast::unit_test::suite xrpAccount(), XRP, std::nullopt, - USD.issue(), + USD, STPath(), true, OfferCrossing::no, @@ -861,12 +861,7 @@ struct PayStrand_test : public beast::unit_test::suite } // Create an offer with the same in/out issue - test( - env, - EUR, - USD.issue(), - STPath({ipe(USD), ipe(EUR)}), - temBAD_PATH); + test(env, EUR, USD, STPath({ipe(USD), ipe(EUR)}), temBAD_PATH); // Path element with type zero test( @@ -891,7 +886,7 @@ struct PayStrand_test : public beast::unit_test::suite test( env, EUR, - USD.issue(), + USD, STPath({ipe(EUR), ipe(USD), ipe(EUR)}), temBAD_PATH_LOOP); } @@ -1014,7 +1009,7 @@ struct PayStrand_test : public beast::unit_test::suite bob, XRP, std::nullopt, - USD.issue(), + USD, path, false, OfferCrossing::no, @@ -1022,10 +1017,7 @@ struct PayStrand_test : public beast::unit_test::suite env.app().logs().journal("Flow")); BEAST_EXPECT(ter == tesSUCCESS); BEAST_EXPECT(equal( - strand, - D{alice, gw, usdC}, - B{USD.issue(), xrpIssue()}, - XRPS{bob})); + strand, D{alice, gw, usdC}, B{USD, xrpIssue()}, XRPS{bob})); } } @@ -1184,7 +1176,7 @@ struct PayStrand_test : public beast::unit_test::suite Env env(*this, features); env.fund(XRP(10000), alice, bob, gw); - STAmount sendMax{USD.issue(), 100, 1}; + STAmount sendMax{USD, 100, 1}; STAmount noAccountAmount{Issue{USD.currency, noAccount()}, 100, 1}; STAmount deliver; AccountID const srcAcc = alice.id(); @@ -1255,13 +1247,10 @@ struct PayStrand_test : public beast::unit_test::suite { using namespace jtx; auto const sa = supported_amendments(); - testToStrand(sa - featureFlowCross); testToStrand(sa); - testRIPD1373(sa - featureFlowCross); testRIPD1373(sa); - testLoop(sa - featureFlowCross); testLoop(sa); testNoAccount(sa); diff --git a/src/test/app/ReducedOffer_test.cpp b/src/test/app/ReducedOffer_test.cpp index a070051e435..b1028bf3658 100644 --- a/src/test/app/ReducedOffer_test.cpp +++ b/src/test/app/ReducedOffer_test.cpp @@ -186,8 +186,7 @@ class ReducedOffer_test : public beast::unit_test::suite }; // bob's offer (the new offer) is the same every time: - Amounts const bobsOffer{ - STAmount(XRP(1)), STAmount(USD.issue(), 1, 0)}; + Amounts const bobsOffer{STAmount(XRP(1)), STAmount(USD, 1, 0)}; // alice's offer has a slightly smaller TakerPays with each // iteration. This should mean that the size of the offer bob @@ -198,11 +197,11 @@ class ReducedOffer_test : public beast::unit_test::suite mantissaReduce += 20'000'000ull) { STAmount aliceUSD{ - bobsOffer.out.issue(), + bobsOffer.out.asset(), bobsOffer.out.mantissa() - mantissaReduce, bobsOffer.out.exponent()}; STAmount aliceXRP{ - bobsOffer.in.issue(), bobsOffer.in.mantissa() - 1}; + bobsOffer.in.asset(), bobsOffer.in.mantissa() - 1}; Amounts alicesOffer{aliceUSD, aliceXRP}; blockedCount += exerciseOfferPair(alicesOffer, bobsOffer); } @@ -344,8 +343,7 @@ class ReducedOffer_test : public beast::unit_test::suite }; // alice's offer (the old offer) is the same every time: - Amounts const aliceOffer{ - STAmount(XRP(1)), STAmount(USD.issue(), 1, 0)}; + Amounts const aliceOffer{STAmount(XRP(1)), STAmount(USD, 1, 0)}; // bob's offer has a slightly smaller TakerPays with each iteration. // This should mean that the size of the offer alice leaves in the @@ -356,11 +354,11 @@ class ReducedOffer_test : public beast::unit_test::suite mantissaReduce += 20'000'000ull) { STAmount bobUSD{ - aliceOffer.out.issue(), + aliceOffer.out.asset(), aliceOffer.out.mantissa() - mantissaReduce, aliceOffer.out.exponent()}; STAmount bobXRP{ - aliceOffer.in.issue(), aliceOffer.in.mantissa() - 1}; + aliceOffer.in.asset(), aliceOffer.in.mantissa() - 1}; Amounts bobsOffer{bobUSD, bobXRP}; blockedCount += exerciseOfferPair(aliceOffer, bobsOffer); @@ -504,7 +502,7 @@ class ReducedOffer_test : public beast::unit_test::suite auto const USD = gw["USD"]; auto const EUR = gw["EUR"]; - STAmount const tinyUSD(USD.issue(), /*mantissa*/ 1, /*exponent*/ -81); + STAmount const tinyUSD(USD, /*mantissa*/ 1, /*exponent*/ -81); // Make one test run without fixReducedOffersV1 and one with. for (FeatureBitset features : @@ -518,13 +516,10 @@ class ReducedOffer_test : public beast::unit_test::suite env.trust(USD(1000), alice, bob); env.trust(EUR(1000), alice, bob); - STAmount const eurOffer( - EUR.issue(), /*mantissa*/ 2957, /*exponent*/ -76); - STAmount const usdOffer( - USD.issue(), /*mantissa*/ 7109, /*exponent*/ -76); + STAmount const eurOffer(EUR, /*mantissa*/ 2957, /*exponent*/ -76); + STAmount const usdOffer(USD, /*mantissa*/ 7109, /*exponent*/ -76); - STAmount const endLoop( - USD.issue(), /*mantissa*/ 50, /*exponent*/ -81); + STAmount const endLoop(USD, /*mantissa*/ 50, /*exponent*/ -81); int blockedOrderBookCount = 0; for (STAmount initialBobUSD = tinyUSD; initialBobUSD <= endLoop; @@ -724,7 +719,7 @@ class ReducedOffer_test : public beast::unit_test::suite if (badRate == 0) { STAmount const tweakedTakerGets( - aliceReducedOffer.in.issue(), + aliceReducedOffer.in.asset(), aliceReducedOffer.in.mantissa() + 1, aliceReducedOffer.in.exponent(), aliceReducedOffer.in.negative()); @@ -763,7 +758,7 @@ class ReducedOffer_test : public beast::unit_test::suite unsigned int blockedCount = 0; { STAmount increaseGets = USD(0); - STAmount const step(increaseGets.issue(), 1, -8); + STAmount const step(increaseGets.asset(), 1, -8); for (unsigned int i = 0; i < loopCount; ++i) { blockedCount += exerciseOfferTrio( diff --git a/src/test/app/SetAuth_test.cpp b/src/test/app/SetAuth_test.cpp index 3dd8ab590a4..9c6f3ed18de 100644 --- a/src/test/app/SetAuth_test.cpp +++ b/src/test/app/SetAuth_test.cpp @@ -73,7 +73,6 @@ struct SetAuth_test : public beast::unit_test::suite { using namespace jtx; auto const sa = supported_amendments(); - testAuth(sa - featureFlowCross); testAuth(sa); } }; diff --git a/src/test/app/Taker_test.cpp b/src/test/app/Taker_test.cpp deleted file mode 100644 index 89e44b2b98b..00000000000 --- a/src/test/app/Taker_test.cpp +++ /dev/null @@ -1,1395 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include -#include -#include -#include - -namespace ripple { - -class Taker_test : public beast::unit_test::suite -{ - static bool const Buy = false; - static bool const Sell = true; - - class TestTaker : public BasicTaker - { - STAmount funds_; - STAmount cross_funds; - - public: - TestTaker( - CrossType cross_type, - Amounts const& amount, - Quality const& quality, - STAmount const& funds, - std::uint32_t flags, - Rate const& rate_in, - Rate const& rate_out) - : BasicTaker( - cross_type, - AccountID(0x4701), - amount, - quality, - flags, - rate_in, - rate_out) - , funds_(funds) - { - } - - void - set_funds(STAmount const& funds) - { - cross_funds = funds; - } - - STAmount - get_funds(AccountID const& owner, STAmount const& funds) const override - { - if (owner == account()) - return funds_; - - return cross_funds; - } - - Amounts - cross(Amounts offer, Quality quality) - { - if (reject(quality)) - return Amounts(offer.in.zeroed(), offer.out.zeroed()); - - // we need to emulate "unfunded offers" behavior - if (get_funds(AccountID(0x4702), offer.out) == beast::zero) - return Amounts(offer.in.zeroed(), offer.out.zeroed()); - - if (done()) - return Amounts(offer.in.zeroed(), offer.out.zeroed()); - - auto result = do_cross(offer, quality, AccountID(0x4702)); - - funds_ -= result.order.in; - - return result.order; - } - - std::pair - cross( - Amounts offer1, - Quality quality1, - Amounts offer2, - Quality quality2) - { - /* check if composed quality should be rejected */ - Quality const quality(composed_quality(quality1, quality2)); - - if (reject(quality)) - return std::make_pair( - Amounts{offer1.in.zeroed(), offer1.out.zeroed()}, - Amounts{offer2.in.zeroed(), offer2.out.zeroed()}); - - if (done()) - return std::make_pair( - Amounts{offer1.in.zeroed(), offer1.out.zeroed()}, - Amounts{offer2.in.zeroed(), offer2.out.zeroed()}); - - auto result = do_cross( - offer1, - quality1, - AccountID(0x4703), - offer2, - quality2, - AccountID(0x4704)); - - return std::make_pair(result.first.order, result.second.order); - } - }; - -private: - Issue const& - usd() const - { - static Issue const issue( - Currency(0x5553440000000000), AccountID(0x4985601)); - return issue; - } - - Issue const& - eur() const - { - static Issue const issue( - Currency(0x4555520000000000), AccountID(0x4985602)); - return issue; - } - - Issue const& - xrp() const - { - static Issue const issue(xrpCurrency(), xrpAccount()); - return issue; - } - - STAmount - parse_amount(std::string const& amount, Issue const& issue) - { - return amountFromString(issue, amount); - } - - Amounts - parse_amounts( - std::string const& amount_in, - Issue const& issue_in, - std::string const& amount_out, - Issue const& issue_out) - { - STAmount const in(parse_amount(amount_in, issue_in)); - STAmount const out(parse_amount(amount_out, issue_out)); - - return {in, out}; - } - - struct cross_attempt_offer - { - cross_attempt_offer(std::string const& in_, std::string const& out_) - : in(in_), out(out_) - { - } - - std::string in; - std::string out; - }; - -private: - std::string - format_amount(STAmount const& amount) - { - std::string txt = amount.getText(); - txt += "/"; - txt += to_string(amount.issue().currency); - return txt; - } - - void - attempt( - bool sell, - std::string name, - Quality taker_quality, - cross_attempt_offer const offer, - std::string const funds, - Quality cross_quality, - cross_attempt_offer const cross, - std::string const cross_funds, - cross_attempt_offer const flow, - Issue const& issue_in, - Issue const& issue_out, - Rate rate_in = parityRate, - Rate rate_out = parityRate) - { - Amounts taker_offer( - parse_amounts(offer.in, issue_in, offer.out, issue_out)); - - Amounts cross_offer( - parse_amounts(cross.in, issue_in, cross.out, issue_out)); - - CrossType cross_type; - - if (isXRP(issue_out)) - cross_type = CrossType::IouToXrp; - else if (isXRP(issue_in)) - cross_type = CrossType::XrpToIou; - else - cross_type = CrossType::IouToIou; - - // FIXME: We are always invoking the IOU-to-IOU taker. We should select - // the correct type dynamically. - TestTaker taker( - cross_type, - taker_offer, - taker_quality, - parse_amount(funds, issue_in), - sell ? tfSell : 0, - rate_in, - rate_out); - - taker.set_funds(parse_amount(cross_funds, issue_out)); - - auto result = taker.cross(cross_offer, cross_quality); - - Amounts const expected( - parse_amounts(flow.in, issue_in, flow.out, issue_out)); - - BEAST_EXPECT(expected == result); - - if (expected != result) - { - log << "Expected: " << format_amount(expected.in) << " : " - << format_amount(expected.out) << '\n' - << " Actual: " << format_amount(result.in) << " : " - << format_amount(result.out) << std::endl; - } - } - - Quality - get_quality(std::string in, std::string out) - { - return Quality(parse_amounts(in, xrp(), out, xrp())); - } - -public: - // Notation for clamp scenario descriptions: - // - // IN:OUT (with the last in the list being limiting factor) - // N = Nothing - // T = Taker Offer Balance - // A = Taker Account Balance - // B = Owner Account Balance - // - // (s) = sell semantics: taker wants unlimited output - // (b) = buy semantics: taker wants a limited amount out - - // NIKB TODO: Augment TestTaker so currencies and rates can be specified - // once without need for repetition. - void - test_xrp_to_iou() - { - testcase("XRP Quantization: input"); - - Quality q1 = get_quality("1", "1"); - - for (auto NumberSwitchOver : {false, true}) - { - NumberSO stNumberSO{NumberSwitchOver}; - // TAKER OWNER - // QUAL OFFER FUNDS QUAL OFFER FUNDS - // EXPECTED - // XRP USD - attempt( - Sell, - "N:N", - q1, - {"2", "2"}, - "2", - q1, - {"2", "2"}, - "2", - {"2", "2"}, - xrp(), - usd()); - if (NumberSwitchOver) - { - attempt( - Sell, - "N:B", - q1, - {"2", "2"}, - "2", - q1, - {"2", "2"}, - "1.8", - {"2", "1.8"}, - xrp(), - usd()); - } - else - { - attempt( - Sell, - "N:B", - q1, - {"2", "2"}, - "2", - q1, - {"2", "2"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - } - attempt( - Buy, - "N:T", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - xrp(), - usd()); - attempt( - Buy, - "N:BT", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "1.8", - {"1", "1"}, - xrp(), - usd()); - if (NumberSwitchOver) - { - attempt( - Buy, - "N:TB", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "0.8", - {"1", "0.8"}, - xrp(), - usd()); - } - else - { - attempt( - Buy, - "N:TB", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "0.8", - {"0", "0.8"}, - xrp(), - usd()); - } - attempt( - Sell, - "T:N", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - xrp(), - usd()); - if (NumberSwitchOver) - { - attempt( - Sell, - "T:B", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "1.8", - {"1", "1"}, - xrp(), - usd()); - } - else - { - attempt( - Sell, - "T:B", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - } - attempt( - Buy, - "T:T", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - xrp(), - usd()); - attempt( - Buy, - "T:BT", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "1.8", - {"1", "1"}, - xrp(), - usd()); - if (NumberSwitchOver) - { - attempt( - Buy, - "T:TB", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "0.8", - {"1", "0.8"}, - xrp(), - usd()); - } - else - { - attempt( - Buy, - "T:TB", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "0.8", - {"0", "0.8"}, - xrp(), - usd()); - } - - attempt( - Sell, - "A:N", - q1, - {"2", "2"}, - "1", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - xrp(), - usd()); - if (NumberSwitchOver) - { - attempt( - Sell, - "A:B", - q1, - {"2", "2"}, - "1", - q1, - {"2", "2"}, - "1.8", - {"1", "1"}, - xrp(), - usd()); - } - else - { - attempt( - Sell, - "A:B", - q1, - {"2", "2"}, - "1", - q1, - {"2", "2"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - } - attempt( - Buy, - "A:T", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "3", - {"1", "1"}, - xrp(), - usd()); - attempt( - Buy, - "A:BT", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "2.4", - {"1", "1"}, - xrp(), - usd()); - if (NumberSwitchOver) - { - attempt( - Buy, - "A:TB", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "0.8", - {"1", "0.8"}, - xrp(), - usd()); - } - else - { - attempt( - Buy, - "A:TB", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "0.8", - {"0", "0.8"}, - xrp(), - usd()); - } - - attempt( - Sell, - "TA:N", - q1, - {"2", "2"}, - "1", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - xrp(), - usd()); - if (NumberSwitchOver) - { - attempt( - Sell, - "TA:B", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1"}, - xrp(), - usd()); - } - else - { - attempt( - Sell, - "TA:B", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - } - attempt( - Buy, - "TA:T", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "3", - {"1", "1"}, - xrp(), - usd()); - if (NumberSwitchOver) - { - attempt( - Buy, - "TA:BT", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1"}, - xrp(), - usd()); - attempt( - Buy, - "TA:TB", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1"}, - xrp(), - usd()); - } - else - { - attempt( - Buy, - "TA:BT", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - attempt( - Buy, - "TA:TB", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - } - - attempt( - Sell, - "AT:N", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "3", - {"1", "1"}, - xrp(), - usd()); - if (NumberSwitchOver) - { - attempt( - Sell, - "AT:B", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1"}, - xrp(), - usd()); - } - else - { - attempt( - Sell, - "AT:B", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - } - attempt( - Buy, - "AT:T", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "3", - {"1", "1"}, - xrp(), - usd()); - if (NumberSwitchOver) - { - attempt( - Buy, - "AT:BT", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1"}, - xrp(), - usd()); - attempt( - Buy, - "AT:TB", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "0.8", - {"1", "0.8"}, - xrp(), - usd()); - } - else - { - attempt( - Buy, - "AT:BT", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - attempt( - Buy, - "AT:TB", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "0.8", - {"0", "0.8"}, - xrp(), - usd()); - } - } - } - - void - test_iou_to_xrp() - { - testcase("XRP Quantization: output"); - - for (auto NumberSwitchOver : {false, true}) - { - NumberSO stNumberSO{NumberSwitchOver}; - Quality q1 = get_quality("1", "1"); - - // TAKER OWNER - // QUAL OFFER FUNDS QUAL OFFER FUNDS - // EXPECTED - // USD XRP - attempt( - Sell, - "N:N", - q1, - {"3", "3"}, - "3", - q1, - {"3", "3"}, - "3", - {"3", "3"}, - usd(), - xrp()); - attempt( - Sell, - "N:B", - q1, - {"3", "3"}, - "3", - q1, - {"3", "3"}, - "2", - {"2", "2"}, - usd(), - xrp()); - if (NumberSwitchOver) - { - attempt( - Buy, - "N:T", - q1, - {"3", "3"}, - "2.5", - q1, - {"5", "5"}, - "5", - {"2.5", "3"}, - usd(), - xrp()); - attempt( - Buy, - "N:BT", - q1, - {"3", "3"}, - "1.5", - q1, - {"5", "5"}, - "4", - {"1.5", "2"}, - usd(), - xrp()); - } - else - { - attempt( - Buy, - "N:T", - q1, - {"3", "3"}, - "2.5", - q1, - {"5", "5"}, - "5", - {"2.5", "2"}, - usd(), - xrp()); - attempt( - Buy, - "N:BT", - q1, - {"3", "3"}, - "1.5", - q1, - {"5", "5"}, - "4", - {"1.5", "1"}, - usd(), - xrp()); - } - attempt( - Buy, - "N:TB", - q1, - {"3", "3"}, - "2.2", - q1, - {"5", "5"}, - "1", - {"1", "1"}, - usd(), - xrp()); - - attempt( - Sell, - "T:N", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - usd(), - xrp()); - attempt( - Sell, - "T:B", - q1, - {"2", "2"}, - "2", - q1, - {"3", "3"}, - "1", - {"1", "1"}, - usd(), - xrp()); - attempt( - Buy, - "T:T", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - usd(), - xrp()); - attempt( - Buy, - "T:BT", - q1, - {"1", "1"}, - "2", - q1, - {"3", "3"}, - "2", - {"1", "1"}, - usd(), - xrp()); - attempt( - Buy, - "T:TB", - q1, - {"2", "2"}, - "2", - q1, - {"3", "3"}, - "1", - {"1", "1"}, - usd(), - xrp()); - - if (NumberSwitchOver) - { - attempt( - Sell, - "A:N", - q1, - {"2", "2"}, - "1.5", - q1, - {"2", "2"}, - "2", - {"1.5", "2"}, - usd(), - xrp()); - attempt( - Sell, - "A:B", - q1, - {"2", "2"}, - "1.8", - q1, - {"3", "3"}, - "2", - {"1.8", "2"}, - usd(), - xrp()); - } - else - { - attempt( - Sell, - "A:N", - q1, - {"2", "2"}, - "1.5", - q1, - {"2", "2"}, - "2", - {"1.5", "1"}, - usd(), - xrp()); - attempt( - Sell, - "A:B", - q1, - {"2", "2"}, - "1.8", - q1, - {"3", "3"}, - "2", - {"1.8", "1"}, - usd(), - xrp()); - } - attempt( - Buy, - "A:T", - q1, - {"2", "2"}, - "1.2", - q1, - {"3", "3"}, - "3", - {"1.2", "1"}, - usd(), - xrp()); - if (NumberSwitchOver) - { - attempt( - Buy, - "A:BT", - q1, - {"2", "2"}, - "1.5", - q1, - {"4", "4"}, - "3", - {"1.5", "2"}, - usd(), - xrp()); - } - else - { - attempt( - Buy, - "A:BT", - q1, - {"2", "2"}, - "1.5", - q1, - {"4", "4"}, - "3", - {"1.5", "1"}, - usd(), - xrp()); - } - attempt( - Buy, - "A:TB", - q1, - {"2", "2"}, - "1.5", - q1, - {"4", "4"}, - "1", - {"1", "1"}, - usd(), - xrp()); - - if (NumberSwitchOver) - { - attempt( - Sell, - "TA:N", - q1, - {"2", "2"}, - "1.5", - q1, - {"2", "2"}, - "2", - {"1.5", "2"}, - usd(), - xrp()); - } - else - { - attempt( - Sell, - "TA:N", - q1, - {"2", "2"}, - "1.5", - q1, - {"2", "2"}, - "2", - {"1.5", "1"}, - usd(), - xrp()); - } - attempt( - Sell, - "TA:B", - q1, - {"2", "2"}, - "1.5", - q1, - {"3", "3"}, - "1", - {"1", "1"}, - usd(), - xrp()); - if (NumberSwitchOver) - { - attempt( - Buy, - "TA:T", - q1, - {"2", "2"}, - "1.5", - q1, - {"3", "3"}, - "3", - {"1.5", "2"}, - usd(), - xrp()); - attempt( - Buy, - "TA:BT", - q1, - {"2", "2"}, - "1.8", - q1, - {"4", "4"}, - "3", - {"1.8", "2"}, - usd(), - xrp()); - } - else - { - attempt( - Buy, - "TA:T", - q1, - {"2", "2"}, - "1.5", - q1, - {"3", "3"}, - "3", - {"1.5", "1"}, - usd(), - xrp()); - attempt( - Buy, - "TA:BT", - q1, - {"2", "2"}, - "1.8", - q1, - {"4", "4"}, - "3", - {"1.8", "1"}, - usd(), - xrp()); - } - attempt( - Buy, - "TA:TB", - q1, - {"2", "2"}, - "1.2", - q1, - {"3", "3"}, - "1", - {"1", "1"}, - usd(), - xrp()); - - attempt( - Sell, - "AT:N", - q1, - {"2", "2"}, - "2.5", - q1, - {"4", "4"}, - "4", - {"2", "2"}, - usd(), - xrp()); - attempt( - Sell, - "AT:B", - q1, - {"2", "2"}, - "2.5", - q1, - {"3", "3"}, - "1", - {"1", "1"}, - usd(), - xrp()); - attempt( - Buy, - "AT:T", - q1, - {"2", "2"}, - "2.5", - q1, - {"3", "3"}, - "3", - {"2", "2"}, - usd(), - xrp()); - attempt( - Buy, - "AT:BT", - q1, - {"2", "2"}, - "2.5", - q1, - {"4", "4"}, - "3", - {"2", "2"}, - usd(), - xrp()); - attempt( - Buy, - "AT:TB", - q1, - {"2", "2"}, - "2.5", - q1, - {"3", "3"}, - "1", - {"1", "1"}, - usd(), - xrp()); - } - } - - void - test_iou_to_iou() - { - testcase("IOU to IOU"); - - for (auto NumberSwitchOver : {false, true}) - { - NumberSO stNumberSO{NumberSwitchOver}; - Quality q1 = get_quality("1", "1"); - - // Highly exaggerated 50% transfer rate for the input and output: - Rate const rate{parityRate.value + (parityRate.value / 2)}; - - // TAKER OWNER - // QUAL OFFER FUNDS QUAL OFFER FUNDS - // EXPECTED - // EUR USD - attempt( - Sell, - "N:N", - q1, - {"2", "2"}, - "10", - q1, - {"2", "2"}, - "10", - {"2", "2"}, - eur(), - usd(), - rate, - rate); - if (NumberSwitchOver) - { - attempt( - Sell, - "N:B", - q1, - {"4", "4"}, - "10", - q1, - {"4", "4"}, - "4", - {"2.666666666666667", "2.666666666666667"}, - eur(), - usd(), - rate, - rate); - } - else - { - attempt( - Sell, - "N:B", - q1, - {"4", "4"}, - "10", - q1, - {"4", "4"}, - "4", - {"2.666666666666666", "2.666666666666666"}, - eur(), - usd(), - rate, - rate); - } - attempt( - Buy, - "N:T", - q1, - {"1", "1"}, - "10", - q1, - {"2", "2"}, - "10", - {"1", "1"}, - eur(), - usd(), - rate, - rate); - attempt( - Buy, - "N:BT", - q1, - {"2", "2"}, - "10", - q1, - {"6", "6"}, - "5", - {"2", "2"}, - eur(), - usd(), - rate, - rate); - attempt( - Buy, - "N:TB", - q1, - {"2", "2"}, - "2", - q1, - {"6", "6"}, - "1", - {"0.6666666666666667", "0.6666666666666667"}, - eur(), - usd(), - rate, - rate); - if (NumberSwitchOver) - { - attempt( - Sell, - "A:N", - q1, - {"2", "2"}, - "2.5", - q1, - {"2", "2"}, - "10", - {"1.666666666666667", "1.666666666666667"}, - eur(), - usd(), - rate, - rate); - } - else - { - attempt( - Sell, - "A:N", - q1, - {"2", "2"}, - "2.5", - q1, - {"2", "2"}, - "10", - {"1.666666666666666", "1.666666666666666"}, - eur(), - usd(), - rate, - rate); - } - } - } - - void - run() override - { - test_xrp_to_iou(); - test_iou_to_xrp(); - test_iou_to_iou(); - } -}; - -BEAST_DEFINE_TESTSUITE(Taker, tx, ripple); - -} // namespace ripple diff --git a/src/test/app/TheoreticalQuality_test.cpp b/src/test/app/TheoreticalQuality_test.cpp index 917d23377bf..c7b28f0e7c6 100644 --- a/src/test/app/TheoreticalQuality_test.cpp +++ b/src/test/app/TheoreticalQuality_test.cpp @@ -248,9 +248,9 @@ class TheoreticalQuality_test : public beast::unit_test::suite PaymentSandbox sb(closed.get(), tapNONE); AMMContext ammContext(rcp.srcAccount, false); - auto const sendMaxIssue = [&rcp]() -> std::optional { + auto const sendMaxIssue = [&rcp]() -> std::optional { if (rcp.sendMax) - return rcp.sendMax->issue(); + return rcp.sendMax->asset(); return std::nullopt; }(); @@ -260,7 +260,7 @@ class TheoreticalQuality_test : public beast::unit_test::suite sb, rcp.srcAccount, rcp.dstAccount, - rcp.dstAmt.issue(), + rcp.dstAmt.asset(), /*limitQuality*/ std::nullopt, sendMaxIssue, rcp.paths, diff --git a/src/test/app/TrustAndBalance_test.cpp b/src/test/app/TrustAndBalance_test.cpp index b438d797276..102371662c2 100644 --- a/src/test/app/TrustAndBalance_test.cpp +++ b/src/test/app/TrustAndBalance_test.cpp @@ -397,7 +397,7 @@ class TrustAndBalance_test : public beast::unit_test::suite env.require(balance( alice, STAmount( - carol["USD"].issue(), + carol["USD"], 6500000000000000ull, -14, true, @@ -480,7 +480,6 @@ class TrustAndBalance_test : public beast::unit_test::suite using namespace test::jtx; auto const sa = supported_amendments(); - testWithFeatures(sa - featureFlowCross); testWithFeatures(sa); } }; diff --git a/src/test/app/XChain_test.cpp b/src/test/app/XChain_test.cpp index 3d25e9e989a..6ba1a5b224e 100644 --- a/src/test/app/XChain_test.cpp +++ b/src/test/app/XChain_test.cpp @@ -336,7 +336,7 @@ struct BalanceTransfer bool check_payer = true) { auto reward_cost = - multiply(reward, STAmount(reward_accounts.size()), reward.issue()); + multiply(reward, STAmount(reward_accounts.size()), reward.asset()); return check_most_balances(amt, reward) && (!check_payer || payor_.diff() == -(reward_cost + txFees_)); } @@ -1666,7 +1666,7 @@ struct XChain_test : public beast::unit_test::suite, BEAST_EXPECT(!scEnv.claimID(jvb, 1)); // claim id deleted BEAST_EXPECT(transfer.has_happened( - amt, divide(reward, STAmount(3), reward.issue()))); + amt, divide(reward, STAmount(3), reward.asset()))); } // 4,4 => should succeed @@ -1691,7 +1691,7 @@ struct XChain_test : public beast::unit_test::suite, return result; }(); STAmount const split_reward_ = - divide(reward, STAmount(signers_.size()), reward.issue()); + divide(reward, STAmount(signers_.size()), reward.asset()); mcEnv.tx(create_bridge(mcDoor, jvb)).close(); @@ -1744,7 +1744,7 @@ struct XChain_test : public beast::unit_test::suite, BEAST_EXPECT(!scEnv.claimID(jvb, claimID)); // claim id deleted BEAST_EXPECT(transfer.has_happened( - amt, divide(reward, STAmount(2), reward.issue()))); + amt, divide(reward, STAmount(2), reward.asset()))); } // 1,2 => should fail @@ -4451,7 +4451,7 @@ struct XChainSim_test : public beast::unit_test::suite, STAmount amt, std::uint64_t divisor = 1) { - if (amt.issue() != xrpIssue()) + if (amt.asset() != xrpIssue()) return; auto it = accounts.find(acct); if (it == accounts.end()) @@ -4465,22 +4465,22 @@ struct XChainSim_test : public beast::unit_test::suite, (divisor == 1 ? amt : divide( amt, - STAmount(amt.issue(), divisor), - amt.issue())); + STAmount(amt.asset(), divisor), + amt.asset())); } } void spend(jtx::Account const& acct, STAmount amt, std::uint64_t times = 1) { - if (amt.issue() != xrpIssue()) + if (amt.asset() != xrpIssue()) return; receive( acct, times == 1 ? -amt : -multiply( - amt, STAmount(amt.issue(), times), amt.issue())); + amt, STAmount(amt.asset(), times), amt.asset())); } void @@ -4708,7 +4708,7 @@ struct XChainSim_test : public beast::unit_test::suite, assert(cr.claim_id - 1 == counters.claim_count); auto r = cr.reward; - auto reward = divide(r, STAmount(num_attestors), r.issue()); + auto reward = divide(r, STAmount(num_attestors), r.asset()); for (auto i : signers) st.receive(bridge_.signers[i].account, reward); @@ -4793,7 +4793,7 @@ struct XChainSim_test : public beast::unit_test::suite, ChainStateTrack& st = srcState(); jtx::Account const& srcdoor = srcDoor(); - if (xfer.amt.issue() != xrpIssue()) + if (xfer.amt.asset() != xrpIssue()) { st.env.tx(pay(srcdoor, xfer.from, xfer.amt)); st.spendFee(srcdoor); @@ -4814,7 +4814,7 @@ struct XChainSim_test : public beast::unit_test::suite, distribute_reward(ChainStateTrack& st) { auto r = bridge_.reward; - auto reward = divide(r, STAmount(bridge_.quorum), r.issue()); + auto reward = divide(r, STAmount(bridge_.quorum), r.asset()); for (size_t i = 0; i < num_signers; ++i) { diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index 52039f74aea..470458b4839 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -77,7 +77,7 @@ struct DepositArg std::optional asset2In = std::nullopt; std::optional maxEP = std::nullopt; std::optional flags = std::nullopt; - std::optional> assets = std::nullopt; + std::optional> assets = std::nullopt; std::optional seq = std::nullopt; std::optional tfee = std::nullopt; std::optional err = std::nullopt; @@ -91,7 +91,7 @@ struct WithdrawArg std::optional asset2Out = std::nullopt; std::optional maxEP = std::nullopt; std::optional flags = std::nullopt; - std::optional> assets = std::nullopt; + std::optional> assets = std::nullopt; std::optional seq = std::nullopt; std::optional err = std::nullopt; }; @@ -102,7 +102,7 @@ struct VoteArg std::uint32_t tfee = 0; std::optional flags = std::nullopt; std::optional seq = std::nullopt; - std::optional> assets = std::nullopt; + std::optional> assets = std::nullopt; std::optional err = std::nullopt; }; @@ -113,7 +113,7 @@ struct BidArg std::optional> bidMax = std::nullopt; std::vector authAccounts = {}; std::optional flags = std::nullopt; - std::optional> assets = std::nullopt; + std::optional> assets = std::nullopt; }; /** Convenience class to test AMM functionality. @@ -171,8 +171,8 @@ class AMM ammRpcInfo( std::optional const& account = std::nullopt, std::optional const& ledgerIndex = std::nullopt, - std::optional issue1 = std::nullopt, - std::optional issue2 = std::nullopt, + std::optional asset1 = std::nullopt, + std::optional asset2 = std::nullopt, std::optional const& ammAccount = std::nullopt, bool ignoreParams = false, unsigned apiVersion = RPC::apiInvalidVersion) const; @@ -190,8 +190,8 @@ class AMM */ std::tuple balances( - Issue const& issue1, - Issue const& issue2, + Asset const& asset1, + Asset const& asset2, std::optional const& account = std::nullopt) const; [[nodiscard]] bool @@ -251,7 +251,7 @@ class AMM std::optional const& asset2In, std::optional const& maxEP, std::optional const& flags, - std::optional> const& assets, + std::optional> const& assets, std::optional const& seq, std::optional const& tfee = std::nullopt, std::optional const& ter = std::nullopt); @@ -297,7 +297,7 @@ class AMM std::optional const& asset2Out, std::optional const& maxEP, std::optional const& flags, - std::optional> const& assets, + std::optional> const& assets, std::optional const& seq, std::optional const& ter = std::nullopt); @@ -310,7 +310,7 @@ class AMM std::uint32_t feeVal, std::optional const& flags = std::nullopt, std::optional const& seq = std::nullopt, - std::optional> const& assets = std::nullopt, + std::optional> const& assets = std::nullopt, std::optional const& ter = std::nullopt); void @@ -381,7 +381,7 @@ class AMM void setTokens( Json::Value& jv, - std::optional> const& assets = std::nullopt); + std::optional> const& assets = std::nullopt); private: AccountID @@ -395,7 +395,7 @@ class AMM deposit( std::optional const& account, Json::Value& jv, - std::optional> const& assets = std::nullopt, + std::optional> const& assets = std::nullopt, std::optional const& seq = std::nullopt, std::optional const& ter = std::nullopt); @@ -404,7 +404,7 @@ class AMM std::optional const& account, Json::Value& jv, std::optional const& seq, - std::optional> const& assets = std::nullopt, + std::optional> const& assets = std::nullopt, std::optional const& ter = std::nullopt); void @@ -443,8 +443,8 @@ Json::Value ammClawback( Account const& issuer, Account const& holder, - Issue const& asset, - Issue const& asset2, + Asset const& asset, + Asset const& asset2, std::optional const& amount); } // namespace amm diff --git a/src/test/jtx/PathSet.h b/src/test/jtx/PathSet.h index 0f4c4ddd3dd..1d5a300a8e0 100644 --- a/src/test/jtx/PathSet.h +++ b/src/test/jtx/PathSet.h @@ -33,15 +33,15 @@ inline std::size_t countOffers( jtx::Env& env, jtx::Account const& account, - Issue const& takerPays, - Issue const& takerGets) + Asset const& takerPays, + Asset const& takerGets) { size_t count = 0; forEachItem( *env.current(), account, [&](std::shared_ptr const& sle) { if (sle->getType() == ltOFFER && - sle->getFieldAmount(sfTakerPays).issue() == takerPays && - sle->getFieldAmount(sfTakerGets).issue() == takerGets) + sle->getFieldAmount(sfTakerPays).asset() == takerPays && + sle->getFieldAmount(sfTakerGets).asset() == takerGets) ++count; }); return count; @@ -83,8 +83,8 @@ inline bool isOffer( jtx::Env& env, jtx::Account const& account, - Issue const& takerPays, - Issue const& takerGets) + Asset const& takerPays, + Asset const& takerGets) { return countOffers(env, account, takerPays, takerGets) > 0; } @@ -143,7 +143,7 @@ Path::push_back(Issue const& iss) inline Path& Path::push_back(jtx::Account const& account) { - path.emplace_back(account.id(), beast::zero, beast::zero); + path.emplace_back(account.id(), Currency{beast::zero}, beast::zero); return *this; } diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index d81551aa840..13dc00b0d23 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -140,6 +140,9 @@ equal(STAmount const& sa1, STAmount const& sa2); STPathElement IPE(Issue const& iss); +STPathElement +IPE(MPTIssue const& iss); + template STPath stpath(Args const&... args) @@ -166,6 +169,66 @@ same(STPathSet const& st1, Args const&... args) return true; } +Json::Value +rpf(jtx::Account const& src, + jtx::Account const& dst, + STAmount const& dstAmount, + std::optional const& sendMax = std::nullopt, + std::optional const& srcAsset = std::nullopt, + std::optional const& srcIssuer = std::nullopt); + +jtx::Env +pathTestEnv(beast::unit_test::suite& suite); + +class gate +{ +private: + std::condition_variable cv_; + std::mutex mutex_; + bool signaled_ = false; + +public: + // Thread safe, blocks until signaled or period expires. + // Returns `true` if signaled. + template + bool + wait_for(std::chrono::duration const& rel_time) + { + std::unique_lock lk(mutex_); + auto b = cv_.wait_for(lk, rel_time, [this] { return signaled_; }); + signaled_ = false; + return b; + } + + void + signal() + { + std::lock_guard lk(mutex_); + signaled_ = true; + cv_.notify_all(); + } +}; + +Json::Value +find_paths_request( + jtx::Env& env, + jtx::Account const& src, + jtx::Account const& dst, + STAmount const& saDstAmount, + std::optional const& saSendMax = std::nullopt, + std::optional const& srcAsset = std::nullopt, + std::optional const& srcIssuer = std::nullopt); + +std::tuple +find_paths( + jtx::Env& env, + jtx::Account const& src, + jtx::Account const& dst, + STAmount const& saDstAmount, + std::optional const& saSendMax = std::nullopt, + std::optional const& srcAsset = std::nullopt, + std::optional const& srcIssuer = std::nullopt); + /******************************************************************************/ XRPAmount diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index 9990c77c38c..3491f1de47f 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -154,11 +154,9 @@ operator<<(std::ostream& os, PrettyAmount const& amount); // Specifies an order book struct BookSpec { - AccountID account; - ripple::Currency currency; + ripple::Asset asset; - BookSpec(AccountID const& account_, ripple::Currency const& currency_) - : account(account_), currency(currency_) + BookSpec(ripple::Asset const& asset_) : asset(asset_) { } }; @@ -176,6 +174,10 @@ struct XRP_t { return xrpIssue(); } + operator Asset() const + { + return xrpIssue(); + } /** Returns an amount of XRP as PrettyAmount, which is trivially convertable to STAmount @@ -220,7 +222,7 @@ struct XRP_t friend BookSpec operator~(XRP_t const&) { - return BookSpec(xrpAccount(), xrpCurrency()); + return BookSpec(Issue{xrpCurrency(), xrpAccount()}); } }; @@ -350,7 +352,7 @@ class IOU friend BookSpec operator~(IOU const& iou) { - return BookSpec(iou.account.id(), iou.currency); + return BookSpec(Issue{iou.currency, iou.account.id()}); } }; @@ -392,6 +394,10 @@ class MPT { return MPTIssue{issuanceID}; } + operator ripple::Asset() const + { + return mpt(); + } template requires(sizeof(T) >= sizeof(int) && std::is_arithmetic_v) @@ -409,9 +415,7 @@ class MPT friend BookSpec operator~(MPT const& mpt) { - assert(false); - Throw("MPT is not supported"); - return BookSpec{beast::zero, noCurrency()}; + return BookSpec{Asset{mpt}}; } }; diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 089d3508d70..7cf06a3768b 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -63,7 +63,7 @@ AMM::AMM( , creatorAccount_(account) , asset1_(asset1) , asset2_(asset2) - , ammID_(keylet::amm(asset1_.issue(), asset2_.issue()).key) + , ammID_(keylet::amm(asset1_.asset(), asset2_.asset()).key) , initialLPTokens_(initialTokens(asset1, asset2)) , log_(log) , doClose_(close) @@ -73,10 +73,8 @@ AMM::AMM( , msig_(ms) , fee_(fee) , ammAccount_(create(tfee, flags, seq, ter)) - , lptIssue_(ripple::ammLPTIssue( - asset1_.issue().currency, - asset2_.issue().currency, - ammAccount_)) + , lptIssue_( + ripple::ammLPTIssue(asset1_.asset(), asset2_.asset(), ammAccount_)) { } @@ -148,7 +146,7 @@ AMM::create( if (!ter || env_.ter() == tesSUCCESS) { if (auto const amm = env_.current()->read( - keylet::amm(asset1_.issue(), asset2_.issue()))) + keylet::amm(asset1_.asset(), asset2_.asset()))) { return amm->getAccountID(sfAccount); } @@ -160,8 +158,8 @@ Json::Value AMM::ammRpcInfo( std::optional const& account, std::optional const& ledgerIndex, - std::optional issue1, - std::optional issue2, + std::optional asset1, + std::optional asset2, std::optional const& ammAccount, bool ignoreParams, unsigned apiVersion) const @@ -173,21 +171,21 @@ AMM::ammRpcInfo( jv[jss::ledger_index] = *ledgerIndex; if (!ignoreParams) { - if (issue1 || issue2) + if (asset1 || asset2) { - if (issue1) + if (asset1) jv[jss::asset] = - STIssue(sfAsset, *issue1).getJson(JsonOptions::none); - if (issue2) + STIssue(sfAsset, *asset1).getJson(JsonOptions::none); + if (asset2) jv[jss::asset2] = - STIssue(sfAsset2, *issue2).getJson(JsonOptions::none); + STIssue(sfAsset2, *asset2).getJson(JsonOptions::none); } else if (!ammAccount) { jv[jss::asset] = - STIssue(sfAsset, asset1_.issue()).getJson(JsonOptions::none); + STIssue(sfAsset, asset1_.asset()).getJson(JsonOptions::none); jv[jss::asset2] = - STIssue(sfAsset2, asset2_.issue()).getJson(JsonOptions::none); + STIssue(sfAsset2, asset2_.asset()).getJson(JsonOptions::none); } if (ammAccount) jv[jss::amm_account] = to_string(*ammAccount); @@ -204,20 +202,21 @@ AMM::ammRpcInfo( std::tuple AMM::balances( - Issue const& issue1, - Issue const& issue2, + Asset const& asset1, + Asset const& asset2, std::optional const& account) const { if (auto const amm = - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()))) + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) { auto const ammAccountID = amm->getAccountID(sfAccount); auto const [asset1Balance, asset2Balance] = ammPoolHolds( *env_.current(), ammAccountID, - issue1, - issue2, + asset1, + asset2, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, env_.journal); auto const lptAMMBalance = account ? ammLPHolds(*env_.current(), *amm, *account, env_.journal) @@ -235,7 +234,7 @@ AMM::expectBalances( std::optional const& account) const { auto const [asset1Balance, asset2Balance, lptAMMBalance] = - balances(asset1.issue(), asset2.issue(), account); + balances(asset1.asset(), asset2.asset(), account); return asset1 == asset1Balance && asset2 == asset2Balance && lptAMMBalance == STAmount{lpt, lptIssue_}; } @@ -252,7 +251,7 @@ AMM::getLPTokensBalance(std::optional const& account) const env_.journal) .iou(); if (auto const amm = - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()))) + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) return amm->getFieldAmount(sfLPTokenBalance).iou(); return IOUAmount{0}; } @@ -261,7 +260,7 @@ bool AMM::expectLPTokens(AccountID const& account, IOUAmount const& expTokens) const { if (auto const amm = - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()))) + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) { auto const lptAMMBalance = ammLPHolds(*env_.current(), *amm, account, env_.journal); @@ -311,7 +310,7 @@ bool AMM::expectTradingFee(std::uint16_t fee) const { auto const amm = - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue())); + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset())); return amm && (*amm)[sfTradingFee] == fee; } @@ -319,7 +318,7 @@ bool AMM::ammExists() const { return env_.current()->read(keylet::account(ammAccount_)) != nullptr && - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue())) != + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset())) != nullptr; } @@ -360,7 +359,7 @@ AMM::expectAmmInfo( if (!amountFromJsonNoThrow(lptBalance, jv[jss::lp_token])) return false; // ammRpcInfo returns unordered assets - if (asset1Info.issue() != asset1.issue()) + if (asset1Info.asset() != asset1.asset()) std::swap(asset1Info, asset2Info); return asset1 == asset1Info && asset2 == asset2Info && lptBalance == STAmount{balance, lptIssue_}; @@ -369,7 +368,7 @@ AMM::expectAmmInfo( void AMM::setTokens( Json::Value& jv, - std::optional> const& assets) + std::optional> const& assets) { if (assets) { @@ -381,9 +380,9 @@ AMM::setTokens( else { jv[jss::Asset] = - STIssue(sfAsset, asset1_.issue()).getJson(JsonOptions::none); + STIssue(sfAsset, asset1_.asset()).getJson(JsonOptions::none); jv[jss::Asset2] = - STIssue(sfAsset, asset2_.issue()).getJson(JsonOptions::none); + STIssue(sfAsset, asset2_.asset()).getJson(JsonOptions::none); } } @@ -391,7 +390,7 @@ IOUAmount AMM::deposit( std::optional const& account, Json::Value& jv, - std::optional> const& assets, + std::optional> const& assets, std::optional const& seq, std::optional const& ter) { @@ -436,7 +435,8 @@ AMM::deposit( std::optional const& flags, std::optional const& ter) { - assert(!(asset2In && maxEP)); + if (asset2In && maxEP) + Throw("Invalid options: asset2In and maxEP"); return deposit( account, std::nullopt, @@ -458,7 +458,7 @@ AMM::deposit( std::optional const& asset2In, std::optional const& maxEP, std::optional const& flags, - std::optional> const& assets, + std::optional> const& assets, std::optional const& seq, std::optional const& tfee, std::optional const& ter) @@ -518,7 +518,7 @@ AMM::withdraw( std::optional const& account, Json::Value& jv, std::optional const& seq, - std::optional> const& assets, + std::optional> const& assets, std::optional const& ter) { auto const& acct = account ? *account : creatorAccount_; @@ -560,7 +560,8 @@ AMM::withdraw( std::optional const& maxEP, std::optional const& ter) { - assert(!(asset2Out && maxEP)); + if (asset2Out && maxEP) + Throw("Invalid options: asset2Out and maxEP"); return withdraw( account, std::nullopt, @@ -581,7 +582,7 @@ AMM::withdraw( std::optional const& asset2Out, std::optional const& maxEP, std::optional const& flags, - std::optional> const& assets, + std::optional> const& assets, std::optional const& seq, std::optional const& ter) { @@ -638,7 +639,7 @@ AMM::vote( std::uint32_t feeVal, std::optional const& flags, std::optional const& seq, - std::optional> const& assets, + std::optional> const& assets, std::optional const& ter) { Json::Value jv; @@ -663,11 +664,11 @@ Json::Value AMM::bid(BidArg const& arg) { if (auto const amm = - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()))) + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) { - assert( - !env_.current()->rules().enabled(fixInnerObjTemplate) || - amm->isFieldPresent(sfAuctionSlot)); + if (env_.current()->rules().enabled(fixInnerObjTemplate) && + !amm->isFieldPresent(sfAuctionSlot)) + Throw("AMM::Bid"); if (amm->isFieldPresent(sfAuctionSlot)) { auto const& auctionSlot = @@ -758,11 +759,11 @@ bool AMM::expectAuctionSlot(auto&& cb) const { if (auto const amm = - env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()))) + env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) { - assert( - !env_.current()->rules().enabled(fixInnerObjTemplate) || - amm->isFieldPresent(sfAuctionSlot)); + if (env_.current()->rules().enabled(fixInnerObjTemplate) && + !amm->isFieldPresent(sfAuctionSlot)) + Throw("AMM::expectAuctionSlot"); if (amm->isFieldPresent(sfAuctionSlot)) { auto const& auctionSlot = @@ -828,8 +829,8 @@ Json::Value ammClawback( Account const& issuer, Account const& holder, - Issue const& asset, - Issue const& asset2, + Asset const& asset, + Asset const& asset2, std::optional const& amount) { Json::Value jv; diff --git a/src/test/jtx/impl/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index 575e2e1d889..95748f1e34d 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -63,7 +63,7 @@ fund( for (auto const& amt : amts) { env.trust(amt + amt, account); - env(pay(amt.issue().account, account, amt)); + env(pay(amt.getIssuer(), account, amt)); } } env.close(); @@ -121,10 +121,10 @@ AMMTestBase::testAMM( return defXRP; return a + XRP(1000); } - auto const defIOU = STAmount{a.issue(), 30000}; + auto const defIOU = STAmount{a.asset(), 30000}; if (a <= defIOU) return defIOU; - return a + STAmount{a.issue(), 1000}; + return a + STAmount{a.asset(), 1000}; }; auto const toFund1 = tofund(asset1); auto const toFund2 = tofund(asset2); diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 3bf69729ab0..28fd5f2f0f8 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -21,7 +21,10 @@ #include #include +#include +#include #include +#include namespace ripple { namespace test { @@ -73,7 +76,7 @@ stpath_append_one(STPath& st, STPathElement const& pe) bool equal(STAmount const& sa1, STAmount const& sa2) { - return sa1 == sa2 && sa1.issue().account == sa2.issue().account; + return sa1 == sa2 && sa1.getIssuer() == sa2.getIssuer(); } // Issue path element @@ -83,9 +86,192 @@ IPE(Issue const& iss) return STPathElement( STPathElement::typeCurrency | STPathElement::typeIssuer, xrpAccount(), - iss.currency, + PathAsset{iss.currency}, iss.account); } +STPathElement +IPE(MPTIssue const& iss) +{ + return STPathElement( + STPathElement::typeMPT | STPathElement::typeIssuer, + xrpAccount(), + PathAsset{iss.getMptID()}, + iss.getIssuer()); +} + +static void +addSourceAsset( + Json::Value& jv, + PathAsset const& srcAsset, + std::optional const& srcIssuer) +{ + std::visit( + [&](TAsset const& asset) { + if constexpr (std::is_same_v) + { + jv[jss::currency] = to_string(asset); + if (srcIssuer) + jv[jss::issuer] = to_string(*srcIssuer); + } + else + { + if (srcIssuer) + Throw( + "MPT source_currencies can't have issuer"); + jv[jss::mpt_issuance_id] = to_string(asset); + } + }, + srcAsset.value()); +} + +Json::Value +rpf(jtx::Account const& src, + jtx::Account const& dst, + STAmount const& dstAmount, + std::optional const& sendMax, + std::optional const& srcAsset, + std::optional const& srcIssuer) +{ + Json::Value jv = Json::objectValue; + jv[jss::command] = "ripple_path_find"; + jv[jss::source_account] = toBase58(src); + jv[jss::destination_account] = toBase58(dst); + jv[jss::destination_amount] = dstAmount.getJson(JsonOptions::none); + if (sendMax) + jv[jss::send_max] = sendMax->getJson(JsonOptions::none); + if (srcAsset) + { + auto& sc = jv[jss::source_currencies] = Json::arrayValue; + Json::Value j = Json::objectValue; + addSourceAsset(j, *srcAsset, srcIssuer); + sc.append(j); + } + + return jv; +} + +jtx::Env +pathTestEnv(beast::unit_test::suite& suite) +{ + // These tests were originally written with search parameters that are + // different from the current defaults. This function creates an env + // with the search parameters that the tests were written for. + using namespace jtx; + return Env(suite, envconfig([](std::unique_ptr cfg) { + cfg->PATH_SEARCH_OLD = 7; + cfg->PATH_SEARCH = 7; + cfg->PATH_SEARCH_MAX = 10; + return cfg; + })); +} + +Json::Value +find_paths_request( + jtx::Env& env, + jtx::Account const& src, + jtx::Account const& dst, + STAmount const& saDstAmount, + std::optional const& saSendMax, + std::optional const& srcAsset, + std::optional const& srcIssuer) +{ + using namespace jtx; + + auto& app = env.app(); + Resource::Charge loadType = Resource::feeReferenceRPC; + Resource::Consumer c; + + RPC::JsonContext context{ + {env.journal, + app, + loadType, + app.getOPs(), + app.getLedgerMaster(), + c, + Role::USER, + {}, + {}, + RPC::apiVersionIfUnspecified}, + {}, + {}}; + + Json::Value params = Json::objectValue; + params[jss::command] = "ripple_path_find"; + params[jss::source_account] = toBase58(src); + params[jss::destination_account] = toBase58(dst); + params[jss::destination_amount] = saDstAmount.getJson(JsonOptions::none); + if (saSendMax) + params[jss::send_max] = saSendMax->getJson(JsonOptions::none); + + if (srcAsset) + { + auto& sc = params[jss::source_currencies] = Json::arrayValue; + Json::Value j = Json::objectValue; + addSourceAsset(j, *srcAsset, srcIssuer); + sc.append(j); + } + + Json::Value result; + gate g; + app.getJobQueue().postCoro(jtCLIENT, "RPC-Client", [&](auto const& coro) { + context.params = std::move(params); + context.coro = coro; + RPC::doCommand(context, result); + g.signal(); + }); + + using namespace std::chrono_literals; + using namespace beast::unit_test; + g.wait_for(5s); + return result; +} + +std::tuple +find_paths( + jtx::Env& env, + jtx::Account const& src, + jtx::Account const& dst, + STAmount const& saDstAmount, + std::optional const& saSendMax, + std::optional const& srcAsset, + std::optional const& srcIssuer) +{ + Json::Value result = find_paths_request( + env, src, dst, saDstAmount, saSendMax, srcAsset, srcIssuer); + if (result.isMember(jss::error)) + return std::make_tuple(STPathSet{}, STAmount{}, STAmount{}); + + STAmount da; + if (result.isMember(jss::destination_amount)) + da = amountFromJson(sfGeneric, result[jss::destination_amount]); + + STAmount sa; + STPathSet paths; + if (result.isMember(jss::alternatives)) + { + auto const& alts = result[jss::alternatives]; + if (alts.size() > 0) + { + auto const& path = alts[0u]; + + if (path.isMember(jss::source_amount)) + sa = amountFromJson(sfGeneric, path[jss::source_amount]); + + if (path.isMember(jss::destination_amount)) + da = amountFromJson(sfGeneric, path[jss::destination_amount]); + + if (path.isMember(jss::paths_computed)) + { + Json::Value p; + p["Paths"] = path[jss::paths_computed]; + STParsedJSONObject po("generic", p); + paths = po.object->getFieldPathSet(sfPaths); + } + } + } + + return std::make_tuple(std::move(paths), std::move(sa), std::move(da)); +} /******************************************************************************/ @@ -109,9 +295,9 @@ expectLine( STAmount const& value, bool defaultLimits) { - if (auto const sle = env.le(keylet::line(account, value.issue()))) + if (auto const sle = env.le(keylet::line(account, value.get()))) { - Issue const issue = value.issue(); + Issue const issue = value.get(); bool const accountLow = account < issue.account; bool expectDefaultTrustLine = true; @@ -128,7 +314,7 @@ expectLine( } auto amount = sle->getFieldAmount(sfBalance); - amount.setIssuer(value.issue().account); + amount.setIssuer(value.getIssuer()); if (!accountLow) amount.negate(); return amount == value && expectDefaultTrustLine; diff --git a/src/test/jtx/impl/amount.cpp b/src/test/jtx/impl/amount.cpp index 5be53dc0a95..7937c6a7b27 100644 --- a/src/test/jtx/impl/amount.cpp +++ b/src/test/jtx/impl/amount.cpp @@ -27,25 +27,6 @@ namespace ripple { namespace test { namespace jtx { -#if 0 -std::ostream& -operator<<(std::ostream&& os, - AnyAmount const& amount) -{ - if (amount.is_any) - { - os << amount.value.getText() << "/" << - to_string(amount.value.issue().currency) << - "*"; - return os; - } - os << amount.value.getText() << "/" << - to_string(amount.value.issue().currency) << - "(" << amount.name() << ")"; - return os; -} -#endif - PrettyAmount::operator AnyAmount() const { return {amount_}; @@ -90,10 +71,16 @@ operator<<(std::ostream& os, PrettyAmount const& amount) os << to_places(d, 6) << " XRP"; } + else if (amount.value().holds()) + { + os << amount.value().getText() << "/" + << to_string(amount.value().get().currency) << "(" + << amount.name() << ")"; + } else { os << amount.value().getText() << "/" - << to_string(amount.value().issue().currency) << "(" << amount.name() + << to_string(amount.value().get()) << "(" << amount.name() << ")"; } return os; @@ -119,7 +106,14 @@ IOU::operator()(detail::epsilon_multiple m) const std::ostream& operator<<(std::ostream& os, IOU const& iou) { - os << to_string(iou.issue().currency) << "(" << iou.account.name() << ")"; + os << to_string(iou.currency) << "(" << iou.account.name() << ")"; + return os; +} + +std::ostream& +operator<<(std::ostream& os, MPT const& mpt) +{ + os << to_string(mpt.issuanceID); return os; } diff --git a/src/test/jtx/impl/balance.cpp b/src/test/jtx/impl/balance.cpp index 42330658eb0..8a26fa30830 100644 --- a/src/test/jtx/impl/balance.cpp +++ b/src/test/jtx/impl/balance.cpp @@ -26,7 +26,7 @@ namespace jtx { void balance::operator()(Env& env) const { - if (isXRP(value_.issue())) + if (isXRP(value_.asset())) { auto const sle = env.le(account_); if (none_) @@ -38,9 +38,10 @@ balance::operator()(Env& env) const env.test.expect(sle->getFieldAmount(sfBalance) == value_); } } - else + else if (value_.holds()) { - auto const sle = env.le(keylet::line(account_.id(), value_.issue())); + auto const sle = + env.le(keylet::line(account_.id(), value_.get())); if (none_) { env.test.expect(!sle); @@ -48,12 +49,28 @@ balance::operator()(Env& env) const else if (env.test.expect(sle)) { auto amount = sle->getFieldAmount(sfBalance); - amount.setIssuer(value_.issue().account); - if (account_.id() > value_.issue().account) + amount.setIssuer(value_.getIssuer()); + if (account_.id() > value_.getIssuer()) amount.negate(); env.test.expect(amount == value_); } } + else + { + auto const issuanceKey = + keylet::mptIssuance(value_.get().getMptID()); + auto const mptokenKey = keylet::mptoken(issuanceKey.key, account_); + auto const sle = env.le(mptokenKey); + if (none_) + { + env.test.expect(!sle); + } + else if (env.test.expect(sle)) + { + auto amount = sle->getFieldU64(sfMPTAmount); + env.test.expect(amount == value_.mpt().value()); + } + } } } // namespace jtx diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index ead6a47c25e..97f127e6002 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -83,7 +83,7 @@ MPTTester::MPTTester(Env& env, Account const& issuer, MPTInit const& arg) } void -MPTTester::create(const MPTCreate& arg) +MPTTester::create(MPTCreate const& arg) { if (id_) Throw("MPT can't be reused"); @@ -109,7 +109,29 @@ MPTTester::create(const MPTCreate& arg) id_.reset(); } else + { env_.require(mptflags(*this, arg.flags.value_or(0))); + if (arg.authorize) + { + auto authAndPay = [&](auto const& accts, auto const&& getAcct) { + for (auto const& it : accts) + { + authorize({.account = getAcct(it)}); + if (arg.pay && arg.pay->first.empty()) + pay(issuer_, getAcct(it), arg.pay->second); + } + if (arg.pay) + { + for (auto const& p : arg.pay->first) + pay(issuer_, p, arg.pay->second); + } + }; + if (arg.authorize->empty()) + authAndPay(holders_, [](auto const& it) { return it.second; }); + else + authAndPay(*arg.authorize, [](auto const& it) { return it; }); + } + } } void diff --git a/src/test/jtx/impl/paths.cpp b/src/test/jtx/impl/paths.cpp index 393e36e9d61..dd3ab722377 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, @@ -57,6 +57,11 @@ paths::operator()(Env& env, JTx& jt) const //------------------------------------------------------------------------------ +path::path(STPath const& p) +{ + jv_ = p.getJson(JsonOptions::none); +} + Json::Value& path::create() { @@ -80,16 +85,21 @@ void path::append_one(IOU const& iou) { auto& jv = create(); - jv["currency"] = to_string(iou.issue().currency); - jv["account"] = toBase58(iou.issue().account); + jv["currency"] = to_string(iou.currency); + jv["account"] = toBase58(iou.account); } void path::append_one(BookSpec const& book) { auto& jv = create(); - jv["currency"] = to_string(book.currency); - jv["issuer"] = toBase58(book.account); + if (book.asset.holds()) + { + jv["currency"] = to_string(book.asset.get().currency); + jv["issuer"] = toBase58(book.asset.getIssuer()); + } + else + jv["mpt_issuance_id"] = to_string(book.asset); } void diff --git a/src/test/jtx/impl/xchain_bridge.cpp b/src/test/jtx/impl/xchain_bridge.cpp index 43b0e7c2f96..d4f4838190e 100644 --- a/src/test/jtx/impl/xchain_bridge.cpp +++ b/src/test/jtx/impl/xchain_bridge.cpp @@ -448,25 +448,27 @@ XChainBridgeObjects::XChainBridgeObjects() }()) , quorum(UT_XCHAIN_DEFAULT_QUORUM) , reward(XRP(1)) - , split_reward_quorum( - divide(reward, STAmount(UT_XCHAIN_DEFAULT_QUORUM), reward.issue())) + , split_reward_quorum(divide( + reward, + STAmount(UT_XCHAIN_DEFAULT_QUORUM), + reward.get())) , split_reward_everyone(divide( reward, STAmount(UT_XCHAIN_DEFAULT_NUM_SIGNERS), - reward.issue())) + reward.get())) , tiny_reward(drops(37)) , tiny_reward_split((divide( tiny_reward, STAmount(UT_XCHAIN_DEFAULT_QUORUM), - tiny_reward.issue()))) + tiny_reward.get()))) , tiny_reward_remainder( tiny_reward - multiply( tiny_reward_split, STAmount(UT_XCHAIN_DEFAULT_QUORUM), - tiny_reward.issue())) + tiny_reward.get())) , one_xrp(XRP(1)) - , xrp_dust(divide(one_xrp, STAmount(10000), one_xrp.issue())) + , xrp_dust(divide(one_xrp, STAmount(10000), one_xrp.get())) { } diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 12b9d74d27c..0ebd697f934 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -96,12 +96,20 @@ struct MPTInit struct MPTCreate { + static inline std::vector AllHolders = {}; std::optional maxAmt = std::nullopt; std::optional assetScale = std::nullopt; std::optional transferFee = std::nullopt; std::optional metadata = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; + // authorize if seated + // if empty vector then authorize all holders + std::optional> authorize = std::nullopt; + // pay if seated and if authorize is seated + // if empty vector then pay to either authorize or all holders + std::optional, std::uint64_t>> pay = + std::nullopt; bool fund = true; std::optional flags = {0}; std::optional err = std::nullopt; diff --git a/src/test/jtx/paths.h b/src/test/jtx/paths.h index cc9caf3af72..63fb28896dc 100644 --- a/src/test/jtx/paths.h +++ b/src/test/jtx/paths.h @@ -25,6 +25,7 @@ #include namespace ripple { +class STPath; namespace test { namespace jtx { @@ -63,6 +64,8 @@ class path template explicit path(T const& t, Args const&... args); + path(STPath const& p); + void operator()(Env&, JTx& jt) const; diff --git a/src/test/ledger/BookDirs_test.cpp b/src/test/ledger/BookDirs_test.cpp index b50f4381f48..49980c0e111 100644 --- a/src/test/ledger/BookDirs_test.cpp +++ b/src/test/ledger/BookDirs_test.cpp @@ -34,7 +34,7 @@ struct BookDirs_test : public beast::unit_test::suite env.fund(XRP(1000000), "alice", "bob", "gw"); { - Book book(xrpIssue(), USD.issue()); + Book book(xrpIssue(), USD); { auto d = BookDirs(*env.current(), book); BEAST_EXPECT(std::begin(d) == std::end(d)); @@ -49,15 +49,13 @@ struct BookDirs_test : public beast::unit_test::suite { env(offer("alice", Account("alice")["USD"](50), XRP(10))); auto d = BookDirs( - *env.current(), - Book(Account("alice")["USD"].issue(), xrpIssue())); + *env.current(), Book(Account("alice")["USD"], xrpIssue())); BEAST_EXPECT(std::distance(d.begin(), d.end()) == 1); } { env(offer("alice", gw["CNY"](50), XRP(10))); - auto d = - BookDirs(*env.current(), Book(gw["CNY"].issue(), xrpIssue())); + auto d = BookDirs(*env.current(), Book(gw["CNY"], xrpIssue())); BEAST_EXPECT(std::distance(d.begin(), d.end()) == 1); } @@ -65,9 +63,7 @@ struct BookDirs_test : public beast::unit_test::suite env.trust(Account("bob")["CNY"](10), "alice"); env(pay("bob", "alice", Account("bob")["CNY"](10))); env(offer("alice", USD(50), Account("bob")["CNY"](10))); - auto d = BookDirs( - *env.current(), - Book(USD.issue(), Account("bob")["CNY"].issue())); + auto d = BookDirs(*env.current(), Book(USD, Account("bob")["CNY"])); BEAST_EXPECT(std::distance(d.begin(), d.end()) == 1); } @@ -77,7 +73,7 @@ struct BookDirs_test : public beast::unit_test::suite for (auto k = 0; k < 80; ++k) env(offer("alice", AUD(i), XRP(j))); - auto d = BookDirs(*env.current(), Book(AUD.issue(), xrpIssue())); + auto d = BookDirs(*env.current(), Book(AUD, xrpIssue())); BEAST_EXPECT(std::distance(d.begin(), d.end()) == 240); auto i = 1, j = 3, k = 0; for (auto const& e : d) @@ -98,7 +94,6 @@ struct BookDirs_test : public beast::unit_test::suite { using namespace jtx; auto const sa = supported_amendments(); - test_bookdir(sa - featureFlowCross); test_bookdir(sa); } }; diff --git a/src/test/ledger/Directory_test.cpp b/src/test/ledger/Directory_test.cpp index bea394f2f36..85a6e308017 100644 --- a/src/test/ledger/Directory_test.cpp +++ b/src/test/ledger/Directory_test.cpp @@ -129,7 +129,7 @@ struct Directory_test : public beast::unit_test::suite // Now check the orderbook: it should be in the order we placed // the offers. - auto book = BookDirs(*env.current(), Book({xrpIssue(), USD.issue()})); + auto book = BookDirs(*env.current(), Book({xrpIssue(), USD})); int count = 1; for (auto const& offer : book) @@ -288,7 +288,7 @@ struct Directory_test : public beast::unit_test::suite // should have no entries and be empty: { Sandbox sb(env.closed().get(), tapNONE); - uint256 const bookBase = getBookBase({xrpIssue(), USD.issue()}); + uint256 const bookBase = getBookBase({xrpIssue(), USD}); BEAST_EXPECT(dirIsEmpty(sb, keylet::page(bookBase))); BEAST_EXPECT(!sb.succ(bookBase, getQualityNext(bookBase))); diff --git a/src/test/ledger/PaymentSandbox_test.cpp b/src/test/ledger/PaymentSandbox_test.cpp index dd9b5c5d88b..818dfb4df74 100644 --- a/src/test/ledger/PaymentSandbox_test.cpp +++ b/src/test/ledger/PaymentSandbox_test.cpp @@ -122,7 +122,7 @@ class PaymentSandbox_test : public beast::unit_test::suite // accountSend, no deferredCredits ApplyViewImpl av(&*env.current(), tapNONE); - auto const iss = USD_gw1.issue(); + auto const iss = USD_gw1; auto const startingAmount = accountHolds( av, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j); { @@ -147,7 +147,7 @@ class PaymentSandbox_test : public beast::unit_test::suite // rippleCredit, no deferredCredits ApplyViewImpl av(&*env.current(), tapNONE); - auto const iss = USD_gw1.issue(); + auto const iss = USD_gw1; auto const startingAmount = accountHolds( av, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j); @@ -169,7 +169,7 @@ class PaymentSandbox_test : public beast::unit_test::suite ApplyViewImpl av(&*env.current(), tapNONE); PaymentSandbox pv(&av); - auto const iss = USD_gw1.issue(); + auto const iss = USD_gw1; auto const startingAmount = accountHolds( pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j); @@ -197,7 +197,7 @@ class PaymentSandbox_test : public beast::unit_test::suite ApplyViewImpl av(&*env.current(), tapNONE); PaymentSandbox pv(&av); - auto const iss = USD_gw1.issue(); + auto const iss = USD_gw1; auto const startingAmount = accountHolds( pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j); @@ -213,7 +213,7 @@ class PaymentSandbox_test : public beast::unit_test::suite ApplyViewImpl av(&*env.current(), tapNONE); PaymentSandbox pv(&av); - auto const iss = USD_gw1.issue(); + auto const iss = USD_gw1; auto const startingAmount = accountHolds( pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j); @@ -229,7 +229,7 @@ class PaymentSandbox_test : public beast::unit_test::suite ApplyViewImpl av(&*env.current(), tapNONE); PaymentSandbox pv(&av); - auto const iss = USD_gw1.issue(); + auto const iss = USD_gw1; auto const startingAmount = accountHolds( pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j); @@ -245,7 +245,7 @@ class PaymentSandbox_test : public beast::unit_test::suite ApplyViewImpl av(&*env.current(), tapNONE); PaymentSandbox pv(&av); - auto const iss = USD_gw1.issue(); + auto const iss = USD_gw1; auto const startingAmount = accountHolds( pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j); @@ -310,7 +310,7 @@ class PaymentSandbox_test : public beast::unit_test::suite Account const alice("alice"); auto const USD = gw["USD"]; - auto const issue = USD.issue(); + auto const issue = USD; STAmount tinyAmt( issue, STAmount::cMinValue, @@ -395,7 +395,7 @@ class PaymentSandbox_test : public beast::unit_test::suite // The currency we pass for the last argument mimics the currency that // is typically passed to creditHook, since it comes from a trust line. Issue tlIssue = noIssue(); - tlIssue.currency = USD.issue().currency; + tlIssue.currency = USD.currency; sb.creditHook(gw.id(), alice.id(), {USD, 400}, {tlIssue, 600}); sb.creditHook(gw.id(), alice.id(), {USD, 100}, {tlIssue, 600}); @@ -403,7 +403,7 @@ class PaymentSandbox_test : public beast::unit_test::suite // Expect that the STAmount issuer returned by balanceHook() is correct. STAmount const balance = sb.balanceHook(gw.id(), alice.id(), {USD, 600}); - BEAST_EXPECT(balance.getIssuer() == USD.issue().account); + BEAST_EXPECT(balance.getIssuer() == USD.account.id()); } public: @@ -419,7 +419,6 @@ class PaymentSandbox_test : public beast::unit_test::suite }; using namespace jtx; auto const sa = supported_amendments(); - testAll(sa - featureFlowCross); testAll(sa); } }; diff --git a/src/test/protocol/STAmount_test.cpp b/src/test/protocol/STAmount_test.cpp index b512c42a643..98185de080d 100644 --- a/src/test/protocol/STAmount_test.cpp +++ b/src/test/protocol/STAmount_test.cpp @@ -53,13 +53,13 @@ class STAmount_test : public beast::unit_test::suite if (mantissa < STAmount::cMinValue) return { - amount.issue(), + amount.asset(), mantissa, amount.exponent(), amount.negative()}; return { - amount.issue(), + amount.asset(), mantissa, amount.exponent(), amount.negative(), @@ -72,13 +72,13 @@ class STAmount_test : public beast::unit_test::suite if (mantissa > STAmount::cMaxValue) return { - amount.issue(), + amount.asset(), mantissa, amount.exponent(), amount.negative()}; return { - amount.issue(), + amount.asset(), mantissa, amount.exponent(), amount.negative(), @@ -104,7 +104,7 @@ class STAmount_test : public beast::unit_test::suite BEAST_EXPECT(!cmp.native()); - BEAST_EXPECT(cmp.issue().currency == res.issue().currency); + BEAST_EXPECT(cmp.get().currency == res.get().currency); if (res != cmp) { diff --git a/src/test/protocol/STNumber_test.cpp b/src/test/protocol/STNumber_test.cpp index ed255e32f1c..b46bcc4c7c7 100644 --- a/src/test/protocol/STNumber_test.cpp +++ b/src/test/protocol/STNumber_test.cpp @@ -75,7 +75,7 @@ struct STNumber_test : public beast::unit_test::suite STNumber const factor{sfNumber, 100}; auto const iouValue = strikePrice.iou(); IOUAmount totalValue{iouValue * factor}; - STAmount const totalAmount{totalValue, strikePrice.issue()}; + STAmount const totalAmount{totalValue, strikePrice.get()}; BEAST_EXPECT(totalAmount == Number{10'000}); } } diff --git a/src/test/rpc/AMMInfo_test.cpp b/src/test/rpc/AMMInfo_test.cpp index c1e059a3ead..a4a6fb085e4 100644 --- a/src/test/rpc/AMMInfo_test.cpp +++ b/src/test/rpc/AMMInfo_test.cpp @@ -54,8 +54,7 @@ class AMMInfo_test : public jtx::AMMTestBase testAMM([&](AMM& ammAlice, Env&) { Account const gw("gw"); auto const USD = gw["USD"]; - auto const jv = - ammAlice.ammRpcInfo({}, {}, USD.issue(), USD.issue()); + auto const jv = ammAlice.ammRpcInfo({}, {}, USD, USD); BEAST_EXPECT(jv[jss::error_message] == "Account not found."); }); @@ -71,10 +70,10 @@ class AMMInfo_test : public jtx::AMMTestBase TestAccount, bool>> const invalidParams = { {xrpIssue(), std::nullopt, None, false}, - {std::nullopt, USD.issue(), None, false}, + {std::nullopt, USD, None, false}, {xrpIssue(), std::nullopt, Alice, false}, - {std::nullopt, USD.issue(), Alice, false}, - {xrpIssue(), USD.issue(), Alice, false}, + {std::nullopt, USD, Alice, false}, + {xrpIssue(), USD, Alice, false}, {std::nullopt, std::nullopt, None, true}}; // Invalid parameters @@ -140,10 +139,10 @@ class AMMInfo_test : public jtx::AMMTestBase TestAccount, bool>> const invalidParamsBadAccount = { {xrpIssue(), std::nullopt, None, false}, - {std::nullopt, USD.issue(), None, false}, + {std::nullopt, USD, None, false}, {xrpIssue(), std::nullopt, Bogie, false}, - {std::nullopt, USD.issue(), Bogie, false}, - {xrpIssue(), USD.issue(), Bogie, false}, + {std::nullopt, USD, Bogie, false}, + {xrpIssue(), USD, Bogie, false}, {std::nullopt, std::nullopt, None, true}}; // Invalid parameters *and* invalid AMM account, default API version @@ -190,8 +189,6 @@ class AMMInfo_test : public jtx::AMMTestBase using namespace jtx; testAMM([&](AMM& ammAlice, Env&) { - BEAST_EXPECT(ammAlice.expectAmmRpcInfo( - XRP(10000), USD(10000), IOUAmount{10000000, 0})); BEAST_EXPECT(ammAlice.expectAmmRpcInfo( XRP(10000), USD(10000), @@ -200,6 +197,33 @@ class AMMInfo_test : public jtx::AMMTestBase std::nullopt, ammAlice.ammAccount())); }); + + { + Env env{*this}; + env.fund(XRP(1'000), gw); + MPTTester mpt(env, gw, {.fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + MPTTester mpt1(env, gw, {.fund = false}); + mpt1.create({.flags = tfMPTCanTransfer | tfMPTCanTrade}); + auto const MPT = mpt["MPT"]; + auto const MPT1 = mpt1["MPT"]; + std::vector> + pools = { + {XRP(100), MPT(100), IOUAmount{100'000}}, + {USD(100), MPT(100), IOUAmount{100}}, + {MPT(100), MPT1(100), IOUAmount{100}}}; + for (auto& pool : pools) + { + AMM amm(env, gw, std::get<0>(pool), std::get<1>(pool)); + BEAST_EXPECT(amm.expectAmmRpcInfo( + std::get<0>(pool), + std::get<1>(pool), + std::get<2>(pool), + std::nullopt, + std::nullopt, + amm.ammAccount())); + } + } } void diff --git a/src/test/rpc/Book_test.cpp b/src/test/rpc/Book_test.cpp index 6bcc0c20809..c3c0e2b1098 100644 --- a/src/test/rpc/Book_test.cpp +++ b/src/test/rpc/Book_test.cpp @@ -1047,8 +1047,7 @@ class Book_test : public beast::unit_test::suite auto const jrOffer = jrr[jss::offers][0u]; BEAST_EXPECT(jrOffer[sfAccount.fieldName] == alice.human()); BEAST_EXPECT( - jrOffer[sfBookDirectory.fieldName] == - getBookDir(env, XRP, USD.issue())); + jrOffer[sfBookDirectory.fieldName] == getBookDir(env, XRP, USD)); BEAST_EXPECT(jrOffer[sfBookNode.fieldName] == "0"); BEAST_EXPECT(jrOffer[jss::Flags] == 0); BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer); @@ -1104,7 +1103,7 @@ class Book_test : public beast::unit_test::suite BEAST_EXPECT(jrNextOffer[sfAccount.fieldName] == bob.human()); BEAST_EXPECT( jrNextOffer[sfBookDirectory.fieldName] == - getBookDir(env, XRP, USD.issue())); + getBookDir(env, XRP, USD)); BEAST_EXPECT(jrNextOffer[sfBookNode.fieldName] == "0"); BEAST_EXPECT(jrNextOffer[jss::Flags] == 0); BEAST_EXPECT(jrNextOffer[sfLedgerEntryType.fieldName] == jss::Offer); diff --git a/src/test/rpc/GatewayBalances_test.cpp b/src/test/rpc/GatewayBalances_test.cpp index 4e847c8dbdd..55bc9eadc78 100644 --- a/src/test/rpc/GatewayBalances_test.cpp +++ b/src/test/rpc/GatewayBalances_test.cpp @@ -198,8 +198,7 @@ class GatewayBalances_test : public beast::unit_test::suite auto USD = alice["USD"]; // The largest valid STAmount of USD: - STAmount const maxUSD( - USD.issue(), STAmount::cMaxValue, STAmount::cMaxOffset); + STAmount const maxUSD(USD, STAmount::cMaxValue, STAmount::cMaxOffset); // Create a hotwallet Account const hw{"hw"}; @@ -243,11 +242,8 @@ class GatewayBalances_test : public beast::unit_test::suite { using namespace jtx; auto const sa = supported_amendments(); - for (auto feature : {sa - featureFlowCross, sa}) - { - testGWB(feature); - testGWBApiVersions(feature); - } + testGWB(sa); + testGWBApiVersions(sa); testGWBOverflow(); } diff --git a/src/test/rpc/NoRipple_test.cpp b/src/test/rpc/NoRipple_test.cpp index aa5e6e1efef..3abbd6dd7a8 100644 --- a/src/test/rpc/NoRipple_test.cpp +++ b/src/test/rpc/NoRipple_test.cpp @@ -292,7 +292,6 @@ class NoRipple_test : public beast::unit_test::suite }; using namespace jtx; auto const sa = supported_amendments(); - withFeatsTests(sa - featureFlowCross); withFeatsTests(sa); } }; diff --git a/src/xrpld/app/ledger/AcceptedLedgerTx.cpp b/src/xrpld/app/ledger/AcceptedLedgerTx.cpp index 6bdb602fd83..1097986267d 100644 --- a/src/xrpld/app/ledger/AcceptedLedgerTx.cpp +++ b/src/xrpld/app/ledger/AcceptedLedgerTx.cpp @@ -62,13 +62,14 @@ AcceptedLedgerTx::AcceptedLedgerTx( auto const amount = mTxn->getFieldAmount(sfTakerGets); // If the offer create is not self funded then add the owner balance - if (account != amount.issue().account) + if (account != amount.getIssuer()) { auto const ownerFunds = accountFunds( *ledger, account, amount, fhIGNORE_FREEZE, + ahIGNORE_AUTH, beast::Journal{beast::Journal::getNullSink()}); mJson[jss::transaction][jss::owner_funds] = ownerFunds.getText(); } diff --git a/src/xrpld/app/ledger/OrderBookDB.cpp b/src/xrpld/app/ledger/OrderBookDB.cpp index 265e0b62905..9b19b285821 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 issue; + issue.currency = sle->getFieldH160(sfTakerPaysCurrency); + issue.account = sle->getFieldH160(sfTakerPaysIssuer); + book.in = issue; + } + else + { + book.in = sle->getFieldH192(sfTakerPaysMPT); + } + if (sle->isFieldPresent(sfTakerGetsCurrency)) + { + Issue issue; + issue.currency = sle->getFieldH160(sfTakerGetsCurrency); + issue.account = sle->getFieldH160(sfTakerGetsIssuer); + book.out = issue; + } + else + { + book.out = sle->getFieldH192(sfTakerGetsMPT); + } allBooks[book.in].insert(book.out); @@ -129,9 +147,9 @@ OrderBookDB::update(std::shared_ptr const& ledger) } else if (sle->getType() == ltAMM) { - auto const issue1 = (*sle)[sfAsset].get(); - auto const issue2 = (*sle)[sfAsset2].get(); - auto addBook = [&](Issue const& in, Issue const& out) { + auto const asset1 = (*sle)[sfAsset]; + auto const asset2 = (*sle)[sfAsset2]; + auto addBook = [&](Asset const& in, Asset const& out) { allBooks[in].insert(out); if (isXRP(out)) @@ -139,8 +157,8 @@ OrderBookDB::update(std::shared_ptr const& ledger) ++cnt; }; - addBook(issue1, issue2); - addBook(issue2, issue1); + addBook(asset1, asset2); + addBook(asset2, asset1); } } } @@ -179,19 +197,19 @@ OrderBookDB::addOrderBook(Book const& book) // return list of all orderbooks that want this issuerID and currencyID std::vector -OrderBookDB::getBooksByTakerPays(Issue const& issue) +OrderBookDB::getBooksByTakerPays(Asset const& asset) { std::vector ret; { std::lock_guard sl(mLock); - if (auto it = allBooks_.find(issue); it != allBooks_.end()) + if (auto it = allBooks_.find(asset); it != allBooks_.end()) { ret.reserve(it->second.size()); for (auto const& gets : it->second) - ret.push_back(Book(issue, gets)); + ret.push_back(Book(asset, gets)); } } @@ -199,19 +217,19 @@ OrderBookDB::getBooksByTakerPays(Issue const& issue) } int -OrderBookDB::getBookSize(Issue const& issue) +OrderBookDB::getBookSize(Asset const& asset) { std::lock_guard sl(mLock); - if (auto it = allBooks_.find(issue); it != allBooks_.end()) + if (auto it = allBooks_.find(asset); it != allBooks_.end()) return static_cast(it->second.size()); return 0; } bool -OrderBookDB::isBookToXRP(Issue const& issue) +OrderBookDB::isBookToXRP(Asset const& asset) { std::lock_guard sl(mLock); - return xrpBooks_.count(issue) > 0; + return xrpBooks_.count(asset) > 0; } BookListeners::pointer @@ -276,8 +294,8 @@ OrderBookDB::processTxn( data->isFieldPresent(sfTakerGets)) { auto listeners = getBookListeners( - {data->getFieldAmount(sfTakerGets).issue(), - data->getFieldAmount(sfTakerPays).issue()}); + {data->getFieldAmount(sfTakerGets).asset(), + data->getFieldAmount(sfTakerPays).asset()}); if (listeners) listeners->publish(jvObj, havePublished); } diff --git a/src/xrpld/app/ledger/OrderBookDB.h b/src/xrpld/app/ledger/OrderBookDB.h index ce0d9f0fafe..cbff92613bc 100644 --- a/src/xrpld/app/ledger/OrderBookDB.h +++ b/src/xrpld/app/ledger/OrderBookDB.h @@ -45,15 +45,15 @@ class OrderBookDB /** @return a list of all orderbooks that want this issuerID and currencyID. */ std::vector - getBooksByTakerPays(Issue const&); + getBooksByTakerPays(Asset const&); /** @return a count of all orderbooks that want this issuerID and currencyID. */ int - getBookSize(Issue const&); + getBookSize(Asset const&); bool - isBookToXRP(Issue const&); + isBookToXRP(Asset const&); BookListeners::pointer getBookListeners(Book const&); @@ -71,10 +71,10 @@ class OrderBookDB Application& app_; // Maps order books by "issue in" to "issue out": - hardened_hash_map> allBooks_; + hardened_hash_map> allBooks_; // does an order book to XRP exist - hash_set xrpBooks_; + hash_set xrpBooks_; std::recursive_mutex mLock; diff --git a/src/xrpld/app/misc/AMMHelpers.h b/src/xrpld/app/misc/AMMHelpers.h index c6c0c808bfe..8447af87747 100644 --- a/src/xrpld/app/misc/AMMHelpers.h +++ b/src/xrpld/app/misc/AMMHelpers.h @@ -60,7 +60,7 @@ STAmount ammLPTokens( STAmount const& asset1, STAmount const& asset2, - Issue const& lptIssue); + Asset const& lptIssue); /** Calculate LP Tokens given asset's deposit amount. * @param asset1Balance current AMM asset1 balance @@ -148,11 +148,11 @@ withinRelativeDistance( * @param dist requested relative distance * @return true if within dist, false otherwise */ -// clang-format off template requires( std::is_same_v || std::is_same_v || - std::is_same_v || std::is_same_v) + std::is_same_v || std::is_same_v || + std::is_same_v) bool withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist) { @@ -161,7 +161,6 @@ withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist) auto const [min, max] = std::minmax(calc, req); return ((max - min) / max) < dist; } -// clang-format on /** Solve quadratic equation to find takerGets or takerPays. Round * to minimize the amount in order to maximize the quality. @@ -226,7 +225,7 @@ getAMMOfferStartWithTakerGets( // Round downward to minimize the offer and to maximize the quality. // This has the most impact when takerGets is XRP. auto const takerGets = toAmount( - getIssue(pool.out), nTakerGetsProposed, Number::downward); + getAsset(pool.out), nTakerGetsProposed, Number::downward); return TAmounts{ swapAssetOut(pool, takerGets, tfee), takerGets}; }; @@ -297,7 +296,7 @@ getAMMOfferStartWithTakerPays( // Round downward to minimize the offer and to maximize the quality. // This has the most impact when takerPays is XRP. auto const takerPays = toAmount( - getIssue(pool.in), nTakerPaysProposed, Number::downward); + getAsset(pool.in), nTakerPaysProposed, Number::downward); return TAmounts{ takerPays, swapAssetIn(pool, takerPays, tfee)}; }; @@ -374,7 +373,7 @@ changeSpotPriceQuality( return std::nullopt; } auto const takerPays = - toAmount(getIssue(pool.in), nTakerPays, Number::upward); + toAmount(getAsset(pool.in), nTakerPays, Number::upward); // should not fail if (auto const amounts = TAmounts{ @@ -385,9 +384,9 @@ changeSpotPriceQuality( { JLOG(j.error()) << "changeSpotPriceQuality failed: " << to_string(pool.in) - << " " << to_string(pool.out) << " " << " " << quality - << " " << tfee << " " << to_string(amounts.in) << " " - << to_string(amounts.out); + << " " << to_string(pool.out) << " " + << " " << quality << " " << tfee << " " + << to_string(amounts.in) << " " << to_string(amounts.out); Throw("changeSpotPriceQuality failed"); } else @@ -409,7 +408,7 @@ changeSpotPriceQuality( // Generate the offer starting with XRP side. Return seated offer amounts // if the offer can be generated, otherwise nullopt. auto const amounts = [&]() { - if (isXRP(getIssue(pool.out))) + if (isXRP(getAsset(pool.out))) return getAMMOfferStartWithTakerGets(pool, quality, tfee); return getAMMOfferStartWithTakerPays(pool, quality, tfee); }(); @@ -501,7 +500,7 @@ swapAssetIn( auto const denom = pool.in + assetIn * (1 - fee); if (denom.signum() <= 0) - return toAmount(getIssue(pool.out), 0); + return toAmount(getAsset(pool.out), 0); Number::setround(Number::upward); auto const ratio = numerator / denom; @@ -510,14 +509,14 @@ swapAssetIn( auto const swapOut = pool.out - ratio; if (swapOut.signum() < 0) - return toAmount(getIssue(pool.out), 0); + return toAmount(getAsset(pool.out), 0); - return toAmount(getIssue(pool.out), swapOut, Number::downward); + return toAmount(getAsset(pool.out), swapOut, Number::downward); } else { return toAmount( - getIssue(pool.out), + getAsset(pool.out), pool.out - (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)), Number::downward); @@ -569,7 +568,7 @@ swapAssetOut( auto const denom = pool.out - assetOut; if (denom.signum() <= 0) { - return toMaxAmount(getIssue(pool.in)); + return toMaxAmount(getAsset(pool.in)); } Number::setround(Number::upward); @@ -583,14 +582,14 @@ swapAssetOut( Number::setround(Number::upward); auto const swapIn = numerator2 / feeMult; if (swapIn.signum() < 0) - return toAmount(getIssue(pool.in), 0); + return toAmount(getAsset(pool.in), 0); - return toAmount(getIssue(pool.in), swapIn, Number::upward); + return toAmount(getAsset(pool.in), swapIn, Number::upward); } else { return toAmount( - getIssue(pool.in), + getAsset(pool.in), ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) / feeMult(tfee), Number::upward); diff --git a/src/xrpld/app/misc/AMMUtils.h b/src/xrpld/app/misc/AMMUtils.h index 52fe819a28e..f60e85cbf64 100644 --- a/src/xrpld/app/misc/AMMUtils.h +++ b/src/xrpld/app/misc/AMMUtils.h @@ -39,9 +39,10 @@ std::pair ammPoolHolds( ReadView const& view, AccountID const& ammAccountID, - Issue const& issue1, - Issue const& issue2, + Asset const& asset1, + Asset const& asset2, FreezeHandling freezeHandling, + AuthHandling authHandling, beast::Journal const j); /** Get AMM pool and LP token balances. If both optIssue are @@ -52,9 +53,10 @@ Expected, TER> ammHolds( ReadView const& view, SLE const& ammSle, - std::optional const& optIssue1, - std::optional const& optIssue2, + std::optional const& optAsset1, + std::optional const& optAsset2, FreezeHandling freezeHandling, + AuthHandling authHandling, beast::Journal const j); /** Get the balance of LP tokens. @@ -62,8 +64,8 @@ ammHolds( STAmount ammLPHolds( ReadView const& view, - Currency const& cur1, - Currency const& cur2, + Asset const& asset1, + Asset const& asset2, AccountID const& ammAccount, AccountID const& lpAccount, beast::Journal const j); @@ -91,7 +93,7 @@ STAmount ammAccountHolds( ReadView const& view, AccountID const& ammAccountID, - Issue const& issue); + Asset const& asset); /** Delete trustlines to AMM. If all trustlines are deleted then * AMM object and account are deleted. Otherwise tecIMPCOMPLETE is returned. @@ -99,8 +101,8 @@ ammAccountHolds( TER deleteAMMAccount( Sandbox& view, - Issue const& asset, - Issue const& asset2, + Asset const& asset, + Asset const& asset2, beast::Journal j); /** Initialize Auction and Voting slots and set the trading/discounted fee. @@ -110,7 +112,7 @@ initializeFeeAuctionVote( ApplyView& view, std::shared_ptr& ammSle, AccountID const& account, - Issue const& lptIssue, + Asset const& lptIssue, std::uint16_t tfee); /** Return true if the Liquidity Provider is the only AMM provider, false diff --git a/src/xrpld/app/misc/MPTUtils.h b/src/xrpld/app/misc/MPTUtils.h new file mode 100644 index 00000000000..55235073dec --- /dev/null +++ b/src/xrpld/app/misc/MPTUtils.h @@ -0,0 +1,52 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_MISC_MPTUTILS_H_INLCUDED +#define RIPPLE_APP_MISC_MPTUTILS_H_INLCUDED + +#include +#include +#include + +namespace ripple { + +class Asset; +class ReadView; + +/* Return true if a transaction is allowed for the specified MPT/account. The + * function checks MPTokenIssuance and MPToken objects flags to determine if the + * transaction is allowed. + */ +TER +isMPTTxAllowed( + ReadView const& v, + TxType tx, + Asset const& asset, + AccountID const& accountID); + +TER +isMPTDEXAllowed( + ReadView const& view, + Asset const& issuanceID, + AccountID const& srcAccount, + AccountID const& destAccount); + +} // namespace ripple + +#endif // RIPPLE_APP_MISC_MPTUTILS_H_INLCUDED diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 996a1fdf748..b6abf43013b 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -3032,13 +3032,14 @@ NetworkOPsImp::transJson( auto const amount = transaction->getFieldAmount(sfTakerGets); // If the offer create is not self funded then add the owner balance - if (account != amount.issue().account) + if (account != amount.getIssuer()) { auto const ownerFunds = accountFunds( *ledger, account, amount, fhIGNORE_FREEZE, + ahIGNORE_AUTH, app_.journal("View")); jvObj[jss::transaction][jss::owner_funds] = ownerFunds.getText(); } @@ -4164,8 +4165,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; @@ -4175,7 +4176,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) @@ -4223,7 +4224,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. @@ -4252,9 +4253,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) @@ -4274,9 +4275,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. @@ -4299,7 +4300,7 @@ NetworkOPsImp::getBookPage( std::min( saTakerPays, multiply( - saTakerGetsFunded, saDirRate, saTakerPays.issue())) + saTakerGetsFunded, saDirRate, saTakerPays.asset())) .setJson(jvOffer[jss::taker_pays_funded]); } @@ -4450,7 +4451,7 @@ NetworkOPsImp::getBookPage( // going on here? std::min( saTakerPays, - multiply(saTakerGetsFunded, saDirRate, saTakerPays.issue())) + multiply(saTakerGetsFunded, saDirRate, saTakerPays.asset())) .setJson(jvOffer[jss::taker_pays_funded]); } diff --git a/src/xrpld/app/misc/detail/AMMHelpers.cpp b/src/xrpld/app/misc/detail/AMMHelpers.cpp index 8724c413a68..7d23e9bf27f 100644 --- a/src/xrpld/app/misc/detail/AMMHelpers.cpp +++ b/src/xrpld/app/misc/detail/AMMHelpers.cpp @@ -25,7 +25,7 @@ STAmount ammLPTokens( STAmount const& asset1, STAmount const& asset2, - Issue const& lptIssue) + Asset const& lptIssue) { auto const tokens = root2(asset1 * asset2); return toSTAmount(lptIssue, tokens); @@ -49,7 +49,7 @@ lpTokensIn( Number const r = asset1Deposit / asset1Balance; auto const c = root2(f2 * f2 + r / f1) - f2; auto const t = lptAMMBalance * (r - c) / (1 + c); - return toSTAmount(lptAMMBalance.issue(), t); + return toSTAmount(lptAMMBalance.asset(), t); } /* Equation 4 solves equation 3 for b: @@ -79,7 +79,7 @@ ammAssetIn( auto const b = 2 * d / t2 - 1 / f1; auto const c = d * d - f2 * f2; return toSTAmount( - asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c)); + asset1Balance.asset(), asset1Balance * solveQuadraticEq(a, b, c)); } /* Equation 7: @@ -97,7 +97,7 @@ lpTokensOut( auto const f1 = getFee(tfee); auto const c = fr * f1 + 2 - f1; auto const t = lptAMMBalance * (c - root2(c * c - 4 * fr)) / 2; - return toSTAmount(lptAMMBalance.issue(), t); + return toSTAmount(lptAMMBalance.asset(), t); } /* Equation 8 solves equation 7 for b: @@ -120,7 +120,7 @@ withdrawByTokens( auto const f = getFee(tfee); Number const t1 = lpTokens / lptAMMBalance; auto const b = assetBalance * (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); - return toSTAmount(assetBalance.issue(), b); + return toSTAmount(assetBalance.asset(), b); } Number @@ -176,9 +176,9 @@ adjustAmountsByLPTokens( if (amount2) { Number const fr = lpTokensActual / lpTokens; - auto const amountActual = toSTAmount(amount.issue(), fr * amount); + auto const amountActual = toSTAmount(amount.asset(), fr * amount); auto const amount2Actual = - toSTAmount(amount2->issue(), fr * *amount2); + toSTAmount(amount2->asset(), fr * *amount2); if (!ammRoundingEnabled) return std::make_tuple( amountActual < amount ? amountActual : amount, diff --git a/src/xrpld/app/misc/detail/AMMUtils.cpp b/src/xrpld/app/misc/detail/AMMUtils.cpp index 0b83afc6d39..9aaea45eb09 100644 --- a/src/xrpld/app/misc/detail/AMMUtils.cpp +++ b/src/xrpld/app/misc/detail/AMMUtils.cpp @@ -29,15 +29,16 @@ std::pair ammPoolHolds( ReadView const& view, AccountID const& ammAccountID, - Issue const& issue1, - Issue const& issue2, + Asset const& asset1, + Asset const& asset2, FreezeHandling freezeHandling, + AuthHandling authHandling, beast::Journal const j) { - auto const assetInBalance = - accountHolds(view, ammAccountID, issue1, freezeHandling, j); - auto const assetOutBalance = - accountHolds(view, ammAccountID, issue2, freezeHandling, j); + auto const assetInBalance = accountHolds( + view, ammAccountID, asset1, freezeHandling, authHandling, j); + auto const assetOutBalance = accountHolds( + view, ammAccountID, asset2, freezeHandling, authHandling, j); return std::make_pair(assetInBalance, assetOutBalance); } @@ -45,38 +46,39 @@ Expected, TER> ammHolds( ReadView const& view, SLE const& ammSle, - std::optional const& optIssue1, - std::optional const& optIssue2, + std::optional const& optAsset1, + std::optional const& optAsset2, FreezeHandling freezeHandling, + AuthHandling authHandling, beast::Journal const j) { - auto const issues = [&]() -> std::optional> { - auto const issue1 = ammSle[sfAsset].get(); - auto const issue2 = ammSle[sfAsset2].get(); - if (optIssue1 && optIssue2) + auto const assets = [&]() -> std::optional> { + auto const asset1 = ammSle[sfAsset]; + auto const asset2 = ammSle[sfAsset2]; + if (optAsset1 && optAsset2) { if (invalidAMMAssetPair( - *optIssue1, - *optIssue2, - std::make_optional(std::make_pair(issue1, issue2)))) + *optAsset1, + *optAsset2, + std::make_optional(std::make_pair(asset1, asset2)))) { // This error can only be hit if the AMM is corrupted // LCOV_EXCL_START - JLOG(j.debug()) << "ammHolds: Invalid optIssue1 or optIssue2 " - << *optIssue1 << " " << *optIssue2; + JLOG(j.debug()) << "ammHolds: Invalid optAsset1 or optAsset2 " + << *optAsset1 << " " << *optAsset2; return std::nullopt; // LCOV_EXCL_STOP } - return std::make_optional(std::make_pair(*optIssue1, *optIssue2)); + return std::make_optional(std::make_pair(*optAsset1, *optAsset2)); } - auto const singleIssue = - [&issue1, &issue2, &j]( - Issue checkIssue, - const char* label) -> std::optional> { - if (checkIssue == issue1) - return std::make_optional(std::make_pair(issue1, issue2)); - else if (checkIssue == issue2) - return std::make_optional(std::make_pair(issue2, issue1)); + auto const singleAsset = + [&asset1, &asset2, &j]( + Asset checkIssue, + const char* label) -> std::optional> { + if (checkIssue == asset1) + return std::make_optional(std::make_pair(asset1, asset2)); + else if (checkIssue == asset2) + return std::make_optional(std::make_pair(asset2, asset1)); // Unreachable unless AMM corrupted. // LCOV_EXCL_START JLOG(j.debug()) @@ -84,34 +86,35 @@ ammHolds( return std::nullopt; // LCOV_EXCL_STOP }; - if (optIssue1) + if (optAsset1) { - return singleIssue(*optIssue1, "optIssue1"); + return singleAsset(*optAsset1, "optAsset1"); } - else if (optIssue2) + else if (optAsset2) { // Cannot have Amount2 without Amount. - return singleIssue(*optIssue2, "optIssue2"); // LCOV_EXCL_LINE + return singleAsset(*optAsset2, "optAsset2"); // LCOV_EXCL_LINE } - return std::make_optional(std::make_pair(issue1, issue2)); + return std::make_optional(std::make_pair(asset1, asset2)); }(); - if (!issues) + if (!assets) return Unexpected(tecAMM_INVALID_TOKENS); - auto const [asset1, asset2] = ammPoolHolds( + auto const [amount1, amount2] = ammPoolHolds( view, ammSle.getAccountID(sfAccount), - issues->first, - issues->second, + assets->first, + assets->second, freezeHandling, + authHandling, j); - return std::make_tuple(asset1, asset2, ammSle[sfLPTokenBalance]); + return std::make_tuple(amount1, amount2, ammSle[sfLPTokenBalance]); } STAmount ammLPHolds( ReadView const& view, - Currency const& cur1, - Currency const& cur2, + Asset const& asset1, + Asset const& asset2, AccountID const& ammAccount, AccountID const& lpAccount, beast::Journal const j) @@ -121,7 +124,7 @@ ammLPHolds( // checks if the underlying assets of LPToken are frozen with the // fixFrozenLPTokenTransfer amendment - auto const currency = ammLPTCurrency(cur1, cur2); + auto const currency = ammLPTCurrency(asset1, asset2); STAmount amount; auto const sle = view.read(keylet::line(lpAccount, ammAccount, currency)); @@ -166,8 +169,8 @@ ammLPHolds( { return ammLPHolds( view, - ammSle[sfAsset].get().currency, - ammSle[sfAsset2].get().currency, + ammSle[sfAsset], + ammSle[sfAsset2], ammSle[sfAccount], lpAccount, j); @@ -209,8 +212,17 @@ STAmount ammAccountHolds( ReadView const& view, AccountID const& ammAccountID, - Issue const& issue) + Asset const& asset) { + if (asset.holds()) + return accountHolds( + view, + ammAccountID, + asset.get(), + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + beast::Journal(beast::Journal::getNullSink())); + Issue const& issue = asset.get(); if (isXRP(issue)) { if (auto const sle = view.read(keylet::account(ammAccountID))) @@ -228,11 +240,11 @@ ammAccountHolds( return amount; } - return STAmount{issue}; + return STAmount{asset}; } static TER -deleteAMMTrustLines( +deleteAMMObjects( Sandbox& sb, AccountID const& ammAccountID, std::uint16_t maxTrustlinesToDelete, @@ -247,31 +259,46 @@ deleteAMMTrustLines( // Skip AMM if (nodeType == LedgerEntryType::ltAMM) return {tesSUCCESS, SkipEntry::Yes}; - // Should only have the trustlines - if (nodeType != LedgerEntryType::ltRIPPLE_STATE) - { - // LCOV_EXCL_START - JLOG(j.error()) - << "deleteAMMTrustLines: deleting non-trustline " - << nodeType; - return {tecINTERNAL, SkipEntry::No}; - // LCOV_EXCL_STOP - } - // Trustlines must have zero balance - if (sleItem->getFieldAmount(sfBalance) != beast::zero) + if (nodeType == ltMPTOKEN) { - // LCOV_EXCL_START - JLOG(j.error()) - << "deleteAMMTrustLines: deleting trustline with " - "non-zero balance."; - return {tecINTERNAL, SkipEntry::No}; - // LCOV_EXCL_STOP + // MPT must have zero balance + if (sleItem->getFieldU64(sfMPTAmount) != 0) + { + // LCOV_EXCL_START + JLOG(j.error()) << "deleteAMMObjects: deleting MPT with " + "non-zero balance."; + return {tecINTERNAL, SkipEntry::No}; + // LCOV_EXCL_STOP + } + + return { + deleteAMMMPToken(sb, sleItem, ammAccountID, j), + SkipEntry::No}; } + else if (nodeType == LedgerEntryType::ltRIPPLE_STATE) + { + // Trustlines must have zero balance + if (sleItem->getFieldAmount(sfBalance) != beast::zero) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "deleteAMMObjects: deleting trustline with " + "non-zero balance."; + return {tecINTERNAL, SkipEntry::No}; + // LCOV_EXCL_STOP + } - return { - deleteAMMTrustLine(sb, sleItem, ammAccountID, j), - SkipEntry::No}; + return { + deleteAMMTrustLine(sb, sleItem, ammAccountID, j), + SkipEntry::No}; + } + // LCOV_EXCL_START + JLOG(j.error()) + << "deleteAMMObjects: deleting non-trustline or non-MPT " + << nodeType; + return {tecINTERNAL, SkipEntry::No}; + // LCOV_EXCL_STOP }, j, maxTrustlinesToDelete); @@ -280,8 +307,8 @@ deleteAMMTrustLines( TER deleteAMMAccount( Sandbox& sb, - Issue const& asset, - Issue const& asset2, + Asset const& asset, + Asset const& asset2, beast::Journal j) { auto ammSle = sb.peek(keylet::amm(asset, asset2)); @@ -306,7 +333,7 @@ deleteAMMAccount( } if (auto const ter = - deleteAMMTrustLines(sb, ammAccountID, maxDeletableAMMTrustLines, j); + deleteAMMObjects(sb, ammAccountID, maxDeletableAMMTrustLines, j); ter != tesSUCCESS) return ter; @@ -339,7 +366,7 @@ initializeFeeAuctionVote( ApplyView& view, std::shared_ptr& ammSle, AccountID const& account, - Issue const& lptIssue, + Asset const& lptIssue, std::uint16_t tfee) { auto const& rules = view.rules(); @@ -389,17 +416,23 @@ isOnlyLiquidityProvider( { // Liquidity Provider (LP) must have one LPToken trustline std::uint8_t nLPTokenTrustLines = 0; - // There are at most two IOU trustlines. One or both could be to the LP - // if LP is the issuer, or a different account if LP is not an issuer. - // For instance, if AMM has two tokens USD and EUR and LP is not the issuer - // of the tokens then the trustlines are between AMM account and the issuer. + // AMM account has at most two IOU (pool tokens, not LPToken) trustlines. + // One or both trustlines could be to the LP if LP is the issuer, + // or a different account if LP is not an issuer. For instance, + // if AMM has two tokens USD and EUR and LP is not the issuer of the tokens + // then the trustlines are between AMM account and the issuer. + // There is one LPToken trustline for each LP. Only remaining LP has + // exactly one LPToken trustlines and at most two IOU trustline for each + // pool token. One or both tokens could be MPT. std::uint8_t nIOUTrustLines = 0; + // There are at most two MPT objects, one for each side of the pool. + std::uint8_t nMPT = 0; // There is only one AMM object bool hasAMM = false; - // AMM LP has at most three trustlines and only one AMM object must exist. - // If there are more than five objects then it's either an error or - // there are more than one LP. Ten pages should be sufficient to include - // five objects. + // AMM LP has at most three trustlines, at most two MPTs, and only one + // AMM object must exist. If there are more than four objects then + // it's either an error or there are more than one LP. Ten pages should + // be sufficient to include four objects. std::uint8_t limit = 10; auto const root = keylet::ownerDir(ammIssue.account); auto currentIndex = root; @@ -415,22 +448,28 @@ isOnlyLiquidityProvider( auto const sle = view.read(keylet::child(key)); if (!sle) return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE + auto const entryType = sle->getFieldU16(sfLedgerEntryType); // Only one AMM object - if (sle->getFieldU16(sfLedgerEntryType) == ltAMM) + if (entryType == ltAMM) { if (hasAMM) return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE hasAMM = true; continue; } - if (sle->getFieldU16(sfLedgerEntryType) != ltRIPPLE_STATE) + if (entryType == ltMPTOKEN) + { + ++nMPT; + continue; + } + if (entryType != ltRIPPLE_STATE) return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE auto const lowLimit = sle->getFieldAmount(sfLowLimit); auto const highLimit = sle->getFieldAmount(sfHighLimit); auto const isLPTrustline = lowLimit.getIssuer() == lpAccount || highLimit.getIssuer() == lpAccount; auto const isLPTokenTrustline = - lowLimit.issue() == ammIssue || highLimit.issue() == ammIssue; + lowLimit.asset() == ammIssue || highLimit.asset() == ammIssue; // Liquidity Provider trustline if (isLPTrustline) @@ -438,23 +477,26 @@ isOnlyLiquidityProvider( // LPToken trustline if (isLPTokenTrustline) { + // LP has exactly one LPToken trustline if (++nLPTokenTrustLines > 1) return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE } + // AMM account has at most two IOU trustlines else if (++nIOUTrustLines > 2) return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE } // Another Liquidity Provider LPToken trustline else if (isLPTokenTrustline) return false; + // AMM account has at most two IOU trustlines else if (++nIOUTrustLines > 2) return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE } auto const uNodeNext = ownerDir->getFieldU64(sfIndexNext); if (uNodeNext == 0) { - if (nLPTokenTrustLines != 1 || nIOUTrustLines == 0 || - nIOUTrustLines > 2) + if (nLPTokenTrustLines != 1 || (nIOUTrustLines == 0 && nMPT == 0) || + (nIOUTrustLines > 2 || nMPT > 2)) return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE return true; } diff --git a/src/xrpld/app/misc/detail/MPTUtils.cpp b/src/xrpld/app/misc/detail/MPTUtils.cpp new file mode 100644 index 00000000000..a4a9eeaac83 --- /dev/null +++ b/src/xrpld/app/misc/detail/MPTUtils.cpp @@ -0,0 +1,105 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include + +namespace ripple { + +static TER +isMPTAllowed( + ReadView const& view, + TxType txType, + Asset const& asset, + AccountID const& accountID, + std::optional const& destAccount) +{ + if (!asset.holds()) + return tesSUCCESS; + + auto const& issuanceID = asset.get().getMptID(); + auto const isDEX = txType == ttPAYMENT && destAccount; + XRPL_ASSERT( + txType == ttAMM_CREATE || txType == ttAMM_DEPOSIT || + txType == ttAMM_WITHDRAW || txType == ttOFFER_CREATE || + txType == ttCHECK_CREATE || txType == ttCHECK_CASH || + txType == ttPAYMENT || isDEX, + "ripple::isMPTAllowed : all MPT tx or DEX"); + + auto const issuanceKey = keylet::mptIssuance(issuanceID); + auto const issuanceSle = view.read(issuanceKey); + if (!issuanceSle) + return tecOBJECT_NOT_FOUND; + + auto const& issuer = asset.getIssuer(); + auto const flags = issuanceSle->getFlags(); + + if (flags & lsfMPTLocked) + return tecNO_PERMISSION; + // Offer crossing and Payment + if ((flags & lsfMPTCanTrade) == 0 && isDEX) + return tecNO_PERMISSION; + if ((flags & lsfMPTCanClawback) && txType == ttAMM_CREATE) + return tecNO_PERMISSION; + + if (accountID != issuer) + { + if ((flags & lsfMPTCanTransfer) == 0) + return tecNO_PERMISSION; + + auto const mptSle = + view.read(keylet::mptoken(issuanceKey.key, accountID)); + // Allow to succeed since some tx create MPToken if it doesn't exist. + // Tx's have their own check for missing MPToken. + if (!mptSle) + return tesSUCCESS; + + if ((mptSle->getFlags() & lsfMPTLocked) && destAccount != issuer) + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +TER +isMPTTxAllowed( + ReadView const& view, + TxType txType, + Asset const& asset, + AccountID const& accountID) +{ + // use isDEXAllowed for payment/offer crossing + assert(txType != ttPAYMENT); + return isMPTAllowed(view, txType, asset, accountID, std::nullopt); +} + +TER +isMPTDEXAllowed( + ReadView const& view, + Asset const& asset, + AccountID const& accountID, + AccountID const& dest) +{ + // use ttPAYMENT for both offer crossing and payment + return isMPTAllowed(view, ttPAYMENT, asset, accountID, dest); +} + +} // namespace ripple diff --git a/src/xrpld/app/paths/AMMLiquidity.h b/src/xrpld/app/paths/AMMLiquidity.h index fe60d39262f..1c2efb1c7c5 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..770dfd38c50 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/AccountAssets.cpp similarity index 66% rename from src/xrpld/app/paths/AccountCurrencies.cpp rename to src/xrpld/app/paths/AccountAssets.cpp index 8646b46939a..91a2a386d90 100644 --- a/src/xrpld/app/paths/AccountCurrencies.cpp +++ b/src/xrpld/app/paths/AccountAssets.cpp @@ -17,21 +17,21 @@ */ //============================================================================== -#include +#include namespace ripple { -hash_set -accountSourceCurrencies( +hash_set +accountSourceAssets( AccountID const& account, - std::shared_ptr const& lrCache, + std::shared_ptr const& lrCache, bool includeXRP) { - hash_set currencies; + hash_set assets; // YYY Only bother if they are above reserve if (includeXRP) - currencies.insert(xrpCurrency()); + assets.insert(xrpCurrency()); if (auto const lines = lrCache->getRippleLines(account, LineDirection::outgoing)) @@ -48,25 +48,35 @@ accountSourceCurrencies( // Peer extends credit. && ((-saBalance) < rspEntry.getLimitPeer()))) // Credit left. { - currencies.insert(saBalance.getCurrency()); + assets.insert(saBalance.get().currency); } } } - currencies.erase(badCurrency()); - return currencies; + assets.erase(badCurrency()); + + if (auto const mpts = lrCache->getMPTs(account)) + { + for (auto const& rspEntry : *mpts) + { + if (!rspEntry.isZeroBalance() && !rspEntry.isMaxedOut()) + assets.insert(rspEntry.getMptID()); + } + } + + return assets; } -hash_set -accountDestCurrencies( +hash_set +accountDestAssets( AccountID const& account, - std::shared_ptr const& lrCache, + std::shared_ptr const& lrCache, bool includeXRP) { - hash_set currencies; + hash_set assets; if (includeXRP) - currencies.insert(xrpCurrency()); + assets.insert(xrpCurrency()); // Even if account doesn't exist if (auto const lines = @@ -77,12 +87,22 @@ accountDestCurrencies( auto& saBalance = rspEntry.getBalance(); if (saBalance < rspEntry.getLimit()) // Can take more - currencies.insert(saBalance.getCurrency()); + assets.insert(saBalance.get().currency); + } + } + + assets.erase(badCurrency()); + + if (auto const mpts = lrCache->getMPTs(account)) + { + for (auto const& rspEntry : *mpts) + { + if (rspEntry.isZeroBalance() && !rspEntry.isMaxedOut()) + assets.insert(rspEntry.getMptID()); } } - currencies.erase(badCurrency()); - return currencies; + return assets; } } // namespace ripple diff --git a/src/xrpld/app/paths/AccountCurrencies.h b/src/xrpld/app/paths/AccountAssets.h similarity index 85% rename from src/xrpld/app/paths/AccountCurrencies.h rename to src/xrpld/app/paths/AccountAssets.h index 26282e742c3..c30e6c747e4 100644 --- a/src/xrpld/app/paths/AccountCurrencies.h +++ b/src/xrpld/app/paths/AccountAssets.h @@ -20,21 +20,21 @@ #ifndef RIPPLE_APP_PATHS_ACCOUNTCURRENCIES_H_INCLUDED #define RIPPLE_APP_PATHS_ACCOUNTCURRENCIES_H_INCLUDED -#include +#include #include namespace ripple { -hash_set -accountDestCurrencies( +hash_set +accountDestAssets( AccountID const& account, - std::shared_ptr const& cache, + std::shared_ptr const& cache, bool includeXRP); -hash_set -accountSourceCurrencies( +hash_set +accountSourceAssets( 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 74% rename from src/xrpld/app/paths/RippleLineCache.cpp rename to src/xrpld/app/paths/AssetCache.cpp index 38b630eeb7e..a9ed2d2638a 100644 --- a/src/xrpld/app/paths/RippleLineCache.cpp +++ b/src/xrpld/app/paths/AssetCache.cpp @@ -17,13 +17,13 @@ */ //============================================================================== -#include +#include #include #include namespace ripple { -RippleLineCache::RippleLineCache( +AssetCache::AssetCache( std::shared_ptr const& ledger, beast::Journal j) : ledger_(ledger), journal_(j) @@ -31,7 +31,7 @@ RippleLineCache::RippleLineCache( JLOG(journal_.debug()) << "created for ledger " << ledger_->info().seq; } -RippleLineCache::~RippleLineCache() +AssetCache::~AssetCache() { JLOG(journal_.debug()) << "destroyed for ledger " << ledger_->info().seq << " with " << lines_.size() << " accounts and " @@ -39,9 +39,7 @@ RippleLineCache::~RippleLineCache() } std::shared_ptr> -RippleLineCache::getRippleLines( - AccountID const& accountID, - LineDirection direction) +AssetCache::getRippleLines(AccountID const& accountID, LineDirection direction) { auto const hash = hasher_(accountID); AccountKey key(accountID, direction, hash); @@ -131,4 +129,53 @@ 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) + { + auto const mptID = makeMptID(sle->getFieldU32(sfSequence), account); + auto const maxAmount = + (*sle)[~sfMaximumAmount].value_or(maxMPTokenAmount); + bool const maxedOut = sle->at(sfOutstandingAmount) == maxAmount; + mpts.emplace_back(mptID, false, maxedOut); + } + else if (sle->getType() == ltMPTOKEN) + { + auto const mptID = sle->getFieldH192(sfMPTokenIssuanceID); + bool const zeroBalance = sle->at(sfMPTAmount) == 0; + bool const maxedOut = [&] { + if (auto const sleIssuance = + ledger_->read(keylet::mptIssuance(mptID))) + { + auto const maxAmount = + (*sleIssuance)[~sfMaximumAmount].value_or( + maxMPTokenAmount); + return sleIssuance->at(sfOutstandingAmount) == maxAmount; + } + return true; + }(); + + mpts.emplace_back(mptID, zeroBalance, maxedOut); + } + }); + + 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..1baf3d81a17 100644 --- a/src/xrpld/app/paths/RippleLineCache.h +++ b/src/xrpld/app/paths/AssetCache.h @@ -21,6 +21,7 @@ #define RIPPLE_APP_PATHS_RIPPLELINECACHE_H_INCLUDED #include +#include #include #include #include @@ -33,13 +34,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 +63,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 +129,7 @@ class RippleLineCache final : public CountedObject AccountKey::Hash> lines_; std::size_t totalLineCount_ = 0; + hash_map>> mpts_; }; } // namespace ripple diff --git a/src/xrpld/app/paths/Credit.cpp b/src/xrpld/app/paths/Credit.cpp index e027949e761..2d7a13f0e92 100644 --- a/src/xrpld/app/paths/Credit.cpp +++ b/src/xrpld/app/paths/Credit.cpp @@ -46,7 +46,7 @@ creditLimit( result.getIssuer() == account, "ripple::creditLimit : result issuer match"); XRPL_ASSERT( - result.getCurrency() == currency, + result.get().currency == currency, "ripple::creditLimit : result currency match"); return result; } @@ -84,7 +84,7 @@ creditBalance( result.getIssuer() == account, "ripple::creditBalance : result issuer match"); XRPL_ASSERT( - result.getCurrency() == currency, + result.get().currency == currency, "ripple::creditBalance : result currency match"); return result; } diff --git a/src/xrpld/app/paths/Flow.cpp b/src/xrpld/app/paths/Flow.cpp index 3df8f6f9992..d952f38c9af 100644 --- a/src/xrpld/app/paths/Flow.cpp +++ b/src/xrpld/app/paths/Flow.cpp @@ -38,8 +38,8 @@ template static auto finishFlow( PaymentSandbox& sb, - Issue const& srcIssue, - Issue const& dstIssue, + Asset const& srcAsset, + Asset const& dstAsset, FlowResult&& f) { path::RippleCalc::Output result; @@ -49,8 +49,8 @@ finishFlow( result.removableOffers = std::move(f.removableOffers); result.setResult(f.ter); - result.actualAmountIn = toSTAmount(f.in, srcIssue); - result.actualAmountOut = toSTAmount(f.out, dstIssue); + result.actualAmountIn = toSTAmount(f.in, srcAsset); + result.actualAmountOut = toSTAmount(f.out, dstAsset); return result; }; @@ -71,19 +71,21 @@ flow( beast::Journal j, path::detail::FlowDebugInfo* flowDebugInfo) { - Issue const srcIssue = [&] { + Asset const srcAsset = [&]() -> Asset { if (sendMax) - return sendMax->issue(); - if (!isXRP(deliver.issue().currency)) - return Issue(deliver.issue().currency, src); - return xrpIssue(); + return sendMax->asset(); + if (isXRP(deliver)) + return xrpIssue(); + if (deliver.holds()) + return Issue(deliver.get().currency, src); + return deliver.asset(); }(); - Issue const dstIssue = deliver.issue(); + Asset const dstAsset = deliver.asset(); - std::optional sendMaxIssue; + std::optional sendMaxAsset; if (sendMax) - sendMaxIssue = sendMax->issue(); + sendMaxAsset = sendMax->asset(); AMMContext ammContext(src, false); @@ -94,9 +96,9 @@ flow( sb, src, dst, - dstIssue, + dstAsset, limitQuality, - sendMaxIssue, + sendMaxAsset, paths, defaultPaths, ownerPaysTransferFee, @@ -116,7 +118,7 @@ flow( if (j.trace()) { j.trace() << "\nsrc: " << src << "\ndst: " << dst - << "\nsrcIssue: " << srcIssue << "\ndstIssue: " << dstIssue; + << "\nsrcAsset: " << srcAsset << "\ndstAsset: " << dstAsset; j.trace() << "\nNumStrands: " << strands.size(); for (auto const& curStrand : strands) { @@ -128,87 +130,33 @@ flow( } } - const bool srcIsXRP = isXRP(srcIssue.currency); - const bool dstIsXRP = isXRP(dstIssue.currency); - - auto const asDeliver = toAmountSpec(deliver); - - // The src account may send either xrp or iou. The dst account may receive - // either xrp or iou. Since XRP and IOU amounts are represented by different - // types, use templates to tell `flow` about the amount types. - if (srcIsXRP && dstIsXRP) - { - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( - sb, - strands, - asDeliver.xrp, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); - } - - if (srcIsXRP && !dstIsXRP) - { - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( + // The src account may send either xrp,iou,or mpt. The dst account may + // receive either xrp,iou, or mpt. Since XRP, IOU, and MPT amounts are + // represented by different types, use templates to tell `flow` about the + // amount types. + return std::visit( + [&, &strands_ = strands]( + TIn const&, TOut const&) { + using TIn_ = typename TIn::amount_type; + using TOut_ = typename TOut::amount_type; + return finishFlow( sb, - strands, - asDeliver.iou, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); - } - - if (!srcIsXRP && dstIsXRP) - { - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( - sb, - strands, - asDeliver.xrp, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); - } - - XRPL_ASSERT(!srcIsXRP && !dstIsXRP, "ripple::flow : neither is XRP"); - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( - sb, - strands, - asDeliver.iou, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); + srcAsset, + dstAsset, + flow( + sb, + strands_, + get(deliver), + partialPayment, + offerCrossing, + limitQuality, + sendMax, + j, + ammContext, + flowDebugInfo)); + }, + srcAsset.getAmountType(), + dstAsset.getAmountType()); } } // namespace ripple diff --git a/src/xrpld/app/paths/MPT.h b/src/xrpld/app/paths/MPT.h new file mode 100644 index 00000000000..2f466baba39 --- /dev/null +++ b/src/xrpld/app/paths/MPT.h @@ -0,0 +1,68 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 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_PATHS_MPT_H_INCLUDED +#define RIPPLE_APP_PATHS_MPT_H_INCLUDED + +#include + +namespace ripple { + +class PathFindMPT final +{ +private: + MPTID const mptID_; + // If true then holder's balance is 0, always false for issuer + bool const zeroBalance_; + // OutstandingAmount is equal to MaximumAmount + bool const maxedOut_; + +public: + PathFindMPT(MPTID const& mptID) + : mptID_(mptID), zeroBalance_(false), maxedOut_(false) + { + } + PathFindMPT(MPTID const& mptID, bool zeroBalance, bool maxedOut) + : mptID_(mptID), zeroBalance_(zeroBalance), maxedOut_(maxedOut) + { + } + operator MPTID const&() const + { + return mptID_; + } + MPTID const& + getMptID() const + { + return mptID_; + } + bool + isZeroBalance() const + { + return zeroBalance_; + } + bool + isMaxedOut() const + { + return maxedOut_; + } +}; + +} // namespace ripple + +#endif // RIPPLE_APP_PATHS_MPT_H_INCLUDED diff --git a/src/xrpld/app/paths/PathRequest.cpp b/src/xrpld/app/paths/PathRequest.cpp index 643923320a2..4a8a85c96e9 100644 --- a/src/xrpld/app/paths/PathRequest.cpp +++ b/src/xrpld/app/paths/PathRequest.cpp @@ -20,7 +20,7 @@ #include #include #include -#include +#include #include #include #include @@ -170,7 +170,7 @@ PathRequest::updateComplete() } bool -PathRequest::isValid(std::shared_ptr const& crCache) +PathRequest::isValid(std::shared_ptr const& crCache) { if (!raSrcAccount || !raDstAccount) return false; @@ -218,11 +218,12 @@ PathRequest::isValid(std::shared_ptr const& crCache) { bool const disallowXRP(sleDest->getFlags() & lsfDisallowXRP); - auto usDestCurrID = - accountDestCurrencies(*raDstAccount, crCache, !disallowXRP); + auto const destAssets = + accountDestAssets(*raDstAccount, crCache, !disallowXRP); + + for (auto const& asset : destAssets) + jvDestCur.append(to_string(asset)); - for (auto const& currency : usDestCurrID) - jvDestCur.append(to_string(currency)); jvStatus[jss::destination_tag] = (sleDest->getFlags() & lsfRequireDestTag); } @@ -243,7 +244,7 @@ PathRequest::isValid(std::shared_ptr const& crCache) */ std::pair PathRequest::doCreate( - std::shared_ptr const& cache, + std::shared_ptr const& cache, Json::Value const& value) { bool valid = false; @@ -314,11 +315,9 @@ PathRequest::parseJson(Json::Value const& jvParams) return PFR_PJ_INVALID; } - convert_all_ = saDstAmount == STAmount(saDstAmount.issue(), 1u, 0, true); + convert_all_ = saDstAmount == STAmount(saDstAmount.asset(), 1u, 0, true); - if ((saDstAmount.getCurrency().isZero() && - saDstAmount.getIssuer().isNonZero()) || - (saDstAmount.getCurrency() == badCurrency()) || + if (!validAsset(saDstAmount.asset()) || (!convert_all_ && saDstAmount <= beast::zero)) { jvStatus = rpcError(rpcDST_AMT_MALFORMED); @@ -336,11 +335,9 @@ PathRequest::parseJson(Json::Value const& jvParams) saSendMax.emplace(); if (!amountFromJsonNoThrow(*saSendMax, jvParams[jss::send_max]) || - (saSendMax->getCurrency().isZero() && - saSendMax->getIssuer().isNonZero()) || - (saSendMax->getCurrency() == badCurrency()) || + !validAsset(saSendMax->asset()) || (*saSendMax <= beast::zero && - *saSendMax != STAmount(saSendMax->issue(), 1u, 0, true))) + *saSendMax != STAmount(saSendMax->asset(), 1u, 0, true))) { jvStatus = rpcError(rpcSENDMAX_MALFORMED); return PFR_PJ_INVALID; @@ -357,47 +354,72 @@ PathRequest::parseJson(Json::Value const& jvParams) return PFR_PJ_INVALID; } - sciSourceCurrencies.clear(); + sciSourceAssets.clear(); for (auto const& c : jvSrcCurrencies) { - // Mandatory currency - Currency srcCurrencyID; - if (!c.isObject() || !c.isMember(jss::currency) || - !c[jss::currency].isString() || - !to_currency(srcCurrencyID, c[jss::currency].asString())) + // Mandatory currency or MPT + if (!validJSONAsset(c) || !c.isObject()) { jvStatus = rpcError(rpcSRC_CUR_MALFORMED); return PFR_PJ_INVALID; } + PathAsset srcPathAsset; + if (c.isMember(jss::currency)) + { + Currency currency; + if (!c[jss::currency].isString() || + !to_currency(currency, c[jss::currency].asString())) + { + jvStatus = rpcError(rpcSRC_CUR_MALFORMED); + return PFR_PJ_INVALID; + } + srcPathAsset = currency; + } + else + { + uint192 u; + if (!c[jss::mpt_issuance_id].isString() || + !u.parseHex(c[jss::mpt_issuance_id].asString())) + { + jvStatus = rpcError(rpcSRC_CUR_MALFORMED); + return PFR_PJ_INVALID; + } + srcPathAsset = u; + } + // Optional issuer AccountID srcIssuerID; if (c.isMember(jss::issuer) && - (!c[jss::issuer].isString() || + (c.isMember(jss::mpt_issuance_id) || + !c[jss::issuer].isString() || !to_issuer(srcIssuerID, c[jss::issuer].asString()))) { jvStatus = rpcError(rpcSRC_ISR_MALFORMED); return PFR_PJ_INVALID; } - if (srcCurrencyID.isZero()) + if (srcPathAsset.holds()) { - if (srcIssuerID.isNonZero()) + if (srcPathAsset.get().isZero()) { - jvStatus = rpcError(rpcSRC_CUR_MALFORMED); - return PFR_PJ_INVALID; + if (srcIssuerID.isNonZero()) + { + jvStatus = rpcError(rpcSRC_CUR_MALFORMED); + return PFR_PJ_INVALID; + } + } + else if (srcIssuerID.isZero()) + { + srcIssuerID = *raSrcAccount; } - } - else if (srcIssuerID.isZero()) - { - srcIssuerID = *raSrcAccount; } if (saSendMax) { - // If the currencies don't match, ignore the source currency. - if (srcCurrencyID == saSendMax->getCurrency()) + // If the assets don't match, ignore the source asset. + if (srcPathAsset == saSendMax->asset()) { // If neither is the source and they are not equal, then the // source issuer is illegal. @@ -411,26 +433,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) - { - sciSourceCurrencies.insert( - {srcCurrencyID, srcIssuerID}); - } - else if (saSendMax->getIssuer() != *raSrcAccount) + if (srcPathAsset.holds()) { - sciSourceCurrencies.insert( - {srcCurrencyID, saSendMax->getIssuer()}); + if (srcIssuerID != *raSrcAccount) + { + sciSourceAssets.insert(Issue{ + srcPathAsset.get(), srcIssuerID}); + } + else if (saSendMax->getIssuer() != *raSrcAccount) + { + sciSourceAssets.insert(Issue{ + srcPathAsset.get(), + saSendMax->getIssuer()}); + } + else + { + sciSourceAssets.insert(Issue{ + srcPathAsset.get(), *raSrcAccount}); + } } else - { - sciSourceCurrencies.insert( - {srcCurrencyID, *raSrcAccount}); - } + sciSourceAssets.insert(srcPathAsset.get()); } } + else if (srcPathAsset.holds()) + { + sciSourceAssets.insert( + Issue{srcPathAsset.get(), srcIssuerID}); + } else { - sciSourceCurrencies.insert({srcCurrencyID, srcIssuerID}); + sciSourceAssets.insert(MPTIssue{srcPathAsset.get()}); } } } @@ -466,21 +499,21 @@ PathRequest::doAborting() const std::unique_ptr const& PathRequest::getPathFinder( - std::shared_ptr const& cache, - hash_map>& currency_map, - Currency const& currency, + std::shared_ptr const& cache, + hash_map>& pathasset_map, + PathAsset const& asset, STAmount const& dst_amount, int const level, std::function const& continueCallback) { - auto i = currency_map.find(currency); - if (i != currency_map.end()) + auto i = pathasset_map.find(asset); + if (i != pathasset_map.end()) return i->second; auto pathfinder = std::make_unique( cache, *raSrcAccount, *raDstAccount, - currency, + asset, std::nullopt, dst_amount, saSendMax, @@ -489,54 +522,62 @@ 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); + auto assets = accountSourceAssets(*raSrcAccount, cache, true); bool const sameAccount = *raSrcAccount == *raDstAccount; - for (auto const& c : currencies) + for (auto const& asset : assets) { - if (!sameAccount || c != saDstAmount.getCurrency()) + if (!std::visit( + [&](TAsset const& a) { + if (!sameAccount || a != saDstAmount.asset()) + { + if (sourceAssets.size() >= + RPC::Tuning::max_auto_src_cur) + return false; + if constexpr (std::is_same_v) + sourceAssets.insert(Issue{ + a, + a.isZero() ? xrpAccount() : *raSrcAccount}); + else + sourceAssets.insert(MPTIssue{a}); + } + return true; + }, + asset.value())) { - if (sourceCurrencies.size() >= RPC::Tuning::max_auto_src_cur) - return false; - sourceCurrencies.insert( - {c, c.isZero() ? xrpAccount() : *raSrcAccount}); + return false; } } } auto const dst_amount = convertAmount(saDstAmount, convert_all_); - hash_map> currency_map; - for (auto const& issue : sourceCurrencies) + hash_map> pathasset_map; + for (auto const& asset : sourceAssets) { if (continueCallback && !continueCallback()) break; JLOG(m_journal.debug()) << iIdentifier - << " Trying to find paths: " << STAmount(issue, 1).getFullText(); + << " Trying to find paths: " << STAmount(asset, 1).getFullText(); auto& pathfinder = getPathFinder( - cache, - currency_map, - issue.currency, - dst_amount, - level, - continueCallback); + cache, pathasset_map, asset, dst_amount, level, continueCallback); if (!pathfinder) { JLOG(m_journal.debug()) << iIdentifier << " No paths found"; @@ -547,23 +588,32 @@ PathRequest::findPaths( auto ps = pathfinder->getBestPaths( max_paths_, fullLiquidityPath, - mContext[issue], - issue.account, + mContext[asset], + asset.getIssuer(), continueCallback); - mContext[issue] = ps; + mContext[asset] = ps; auto const& sourceAccount = [&] { - if (!isXRP(issue.account)) - return issue.account; + if (!isXRP(asset.getIssuer())) + return asset.getIssuer(); - if (isXRP(issue.currency)) + if (isXRP(asset)) return xrpAccount(); return *raSrcAccount; }(); - STAmount saMaxAmount = saSendMax.value_or( - STAmount(Issue{issue.currency, sourceAccount}, 1u, 0, true)); + STAmount saMaxAmount = [&]() { + if (saSendMax) + return *saSendMax; + if (asset.holds()) + return STAmount( + Issue{asset.get().currency, sourceAccount}, + 1u, + 0, + true); + return STAmount(asset.get(), 1u, 0, true); + }(); JLOG(m_journal.debug()) << iIdentifier << " Paths found, calling rippleCalc"; @@ -620,7 +670,8 @@ PathRequest::findPaths( if (rc.result() == tesSUCCESS) { Json::Value jvEntry(Json::objectValue); - rc.actualAmountIn.setIssuer(sourceAccount); + if (rc.actualAmountIn.holds()) + rc.actualAmountIn.setIssuer(sourceAccount); jvEntry[jss::source_amount] = rc.actualAmountIn.getJson(JsonOptions::none); jvEntry[jss::paths_computed] = ps.getJson(JsonOptions::none); @@ -648,14 +699,14 @@ PathRequest::findPaths( The minimum cost is 50 and the maximum is 400. The cost increases after four source currencies, 50 - (4 * 4) = 34. */ - int const size = sourceCurrencies.size(); + int const size = sourceAssets.size(); consumer_.charge({std::clamp(size * size + 34, 50, 400), "path update"}); return true; } Json::Value PathRequest::doUpdate( - std::shared_ptr const& cache, + std::shared_ptr const& cache, bool fast, std::function const& continueCallback) { @@ -675,11 +726,11 @@ 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 const assets = accountDestAssets(*raDstAccount, cache, true); + for (auto const& asset : assets) + destAssets.append(to_string(asset)); } newStatus[jss::source_account] = toBase58(*raSrcAccount); diff --git a/src/xrpld/app/paths/PathRequest.h b/src/xrpld/app/paths/PathRequest.h index 21f10d066ba..534b1392946 100644 --- a/src/xrpld/app/paths/PathRequest.h +++ b/src/xrpld/app/paths/PathRequest.h @@ -21,10 +21,11 @@ #define RIPPLE_APP_PATHS_PATHREQUEST_H_INCLUDED #include +#include #include -#include #include #include +#include #include #include #include @@ -37,7 +38,7 @@ namespace ripple { // A pathfinding request submitted by a client // The request issuer must maintain a strong pointer -class RippleLineCache; +class AssetCache; class PathRequests; // Return values from parseJson <0 = invalid, >0 = valid @@ -86,7 +87,7 @@ class PathRequest final : public InfoSubRequest, updateComplete(); std::pair - doCreate(std::shared_ptr const&, Json::Value const&); + doCreate(std::shared_ptr const&, Json::Value const&); Json::Value doClose() override; @@ -98,7 +99,7 @@ class PathRequest final : public InfoSubRequest, // update jvStatus Json::Value doUpdate( - std::shared_ptr const&, + std::shared_ptr const&, bool fast, std::function const& continueCallback = {}); InfoSub::pointer @@ -108,13 +109,13 @@ class PathRequest final : public InfoSubRequest, private: bool - isValid(std::shared_ptr const& crCache); + isValid(std::shared_ptr const& crCache); std::unique_ptr const& getPathFinder( - std::shared_ptr const&, - hash_map>&, - Currency const&, + std::shared_ptr const&, + hash_map>&, + PathAsset const&, STAmount const&, int const, std::function const&); @@ -124,7 +125,7 @@ class PathRequest final : public InfoSubRequest, */ bool findPaths( - std::shared_ptr const&, + std::shared_ptr const&, int const, Json::Value&, std::function const&); @@ -152,8 +153,8 @@ class PathRequest final : public InfoSubRequest, STAmount saDstAmount; std::optional saSendMax; - std::set sciSourceCurrencies; - std::map mContext; + std::set sciSourceAssets; + std::map mContext; bool convert_all_; diff --git a/src/xrpld/app/paths/PathRequests.cpp b/src/xrpld/app/paths/PathRequests.cpp index 86560445ec7..8888096ff04 100644 --- a/src/xrpld/app/paths/PathRequests.cpp +++ b/src/xrpld/app/paths/PathRequests.cpp @@ -30,21 +30,22 @@ namespace ripple { -/** Get the current RippleLineCache, updating it if necessary. +/** Get the current AssetCache, updating it if necessary. Get the correct ledger to use. */ -std::shared_ptr -PathRequests::getLineCache( +std::shared_ptr +PathRequests::getAssetCache( std::shared_ptr const& ledger, bool authoritative) { std::lock_guard sl(mLock); - auto lineCache = lineCache_.lock(); + auto assetCache = assetCache_.lock(); - std::uint32_t const lineSeq = lineCache ? lineCache->getLedger()->seq() : 0; + std::uint32_t const lineSeq = + assetCache ? assetCache->getLedger()->seq() : 0; std::uint32_t const lgrSeq = ledger->seq(); - JLOG(mJournal.debug()) << "getLineCache has cache for " << lineSeq + JLOG(mJournal.debug()) << "getAssetCache has cache for " << lineSeq << ", considering " << lgrSeq; if ((lineSeq == 0) || // no ledger @@ -54,14 +55,14 @@ PathRequests::getLineCache( (lgrSeq > (lineSeq + 8))) // we jumped way forward for some reason { JLOG(mJournal.debug()) - << "getLineCache creating new cache for " << lgrSeq; + << "getAssetCache creating new cache for " << lgrSeq; // Assign to the local before the member, because the member is a // weak_ptr, and will immediately discard it if there are no other // references. - lineCache_ = lineCache = std::make_shared( - ledger, app_.journal("RippleLineCache")); + assetCache_ = assetCache = + std::make_shared(ledger, app_.journal("AssetCache")); } - return lineCache; + return assetCache; } void @@ -71,13 +72,13 @@ PathRequests::updateAll(std::shared_ptr const& inLedger) app_.getJobQueue().makeLoadEvent(jtPATH_FIND, "PathRequest::updateAll"); std::vector requests; - std::shared_ptr cache; + std::shared_ptr cache; // Get the ledger and cache we should be using { std::lock_guard sl(mLock); requests = requests_; - cache = getLineCache(inLedger, true); + cache = getAssetCache(inLedger, true); } bool newRequests = app_.getLedgerMaster().isNewPathRequest(); @@ -202,7 +203,7 @@ PathRequests::updateAll(std::shared_ptr const& inLedger) // Hold on to the line cache until after the lock is released, so it can // be destroyed outside of the lock - std::shared_ptr lastCache; + std::shared_ptr lastCache; { // Get the latest requests, cache, and ledger for next pass std::lock_guard sl(mLock); @@ -211,7 +212,7 @@ PathRequests::updateAll(std::shared_ptr const& inLedger) break; requests = requests_; lastCache = cache; - cache = getLineCache(cache->getLedger(), false); + cache = getAssetCache(cache->getLedger(), false); } } while (!app_.getJobQueue().isStopping()); @@ -255,7 +256,7 @@ PathRequests::makePathRequest( app_, subscriber, ++mLastIdentifier, *this, mJournal); auto [valid, jvRes] = - req->doCreate(getLineCache(inLedger, false), requestJson); + req->doCreate(getAssetCache(inLedger, false), requestJson); if (valid) { @@ -280,7 +281,8 @@ PathRequests::makeLegacyPathRequest( req = std::make_shared( app_, completion, consumer, ++mLastIdentifier, *this, mJournal); - auto [valid, jvRes] = req->doCreate(getLineCache(inLedger, false), request); + auto [valid, jvRes] = + req->doCreate(getAssetCache(inLedger, false), request); if (!valid) { @@ -306,8 +308,8 @@ PathRequests::doLegacyPathRequest( std::shared_ptr const& inLedger, Json::Value const& request) { - auto cache = std::make_shared( - inLedger, app_.journal("RippleLineCache")); + auto cache = + std::make_shared(inLedger, app_.journal("AssetCache")); auto req = std::make_shared( app_, [] {}, consumer, ++mLastIdentifier, *this, mJournal); diff --git a/src/xrpld/app/paths/PathRequests.h b/src/xrpld/app/paths/PathRequests.h index 670790518a1..e5760d64ea2 100644 --- a/src/xrpld/app/paths/PathRequests.h +++ b/src/xrpld/app/paths/PathRequests.h @@ -21,8 +21,8 @@ #define RIPPLE_APP_PATHS_PATHREQUESTS_H_INCLUDED #include +#include #include -#include #include #include #include @@ -54,8 +54,8 @@ class PathRequests bool requestsPending() const; - std::shared_ptr - getLineCache( + std::shared_ptr + getAssetCache( std::shared_ptr const& ledger, bool authoritative); @@ -111,8 +111,8 @@ class PathRequests // Track all requests std::vector requests_; - // Use a RippleLineCache - std::weak_ptr lineCache_; + // Use a AssetCache + std::weak_ptr assetCache_; std::atomic mLastIdentifier; diff --git a/src/xrpld/app/paths/Pathfinder.cpp b/src/xrpld/app/paths/Pathfinder.cpp index 5864357aec4..7d7c4e8ef67 100644 --- a/src/xrpld/app/paths/Pathfinder.cpp +++ b/src/xrpld/app/paths/Pathfinder.cpp @@ -19,9 +19,9 @@ #include #include +#include #include #include -#include #include #include #include @@ -154,15 +154,49 @@ pathTypeToString(Pathfinder::PathType const& type) STAmount smallestUsefulAmount(STAmount const& amount, int maxPaths) { - return divide(amount, STAmount(maxPaths + 2), amount.issue()); + return divide(amount, STAmount(maxPaths + 2), amount.asset()); } + +STAmount +amountFromPathAsset( + PathAsset const& pathAsset, + std::optional const& srcIssuer, + AccountID const& srcAccount) +{ + return std::visit( + [&](T const& el) { + if constexpr (std::is_same_v) + { + auto const& account = + srcIssuer.value_or(isXRP(el) ? xrpAccount() : srcAccount); + return STAmount(Issue{el, account}, 1u, 0, true); + } + else + return STAmount(el, 1u, 0, true); + }, + pathAsset.value()); +} + +Asset +assetFromPathAsset(PathAsset const& pathAsset, AccountID const& account) +{ + return std::visit( + [&](T const& el) { + if constexpr (std::is_same_v) + return Asset{Issue{el, account}}; + else + return Asset{el}; + }, + pathAsset.value()); +} + } // namespace Pathfinder::Pathfinder( - std::shared_ptr const& cache, + std::shared_ptr const& cache, AccountID const& uSrcAccount, AccountID const& uDstAccount, - Currency const& uSrcCurrency, + PathAsset const& uSrcPathAsset, std::optional const& uSrcIssuer, STAmount const& saDstAmount, std::optional const& srcAmount, @@ -173,24 +207,17 @@ Pathfinder::Pathfinder( isXRP(saDstAmount.getIssuer()) ? uDstAccount : saDstAmount.getIssuer()) , mDstAmount(saDstAmount) - , mSrcCurrency(uSrcCurrency) + , mSrcPathAsset(uSrcPathAsset) , mSrcIssuer(uSrcIssuer) - , mSrcAmount(srcAmount.value_or(STAmount( - Issue{ - uSrcCurrency, - uSrcIssuer.value_or( - isXRP(uSrcCurrency) ? xrpAccount() : uSrcAccount)}, - 1u, - 0, - true))) + , mSrcAmount(amountFromPathAsset(uSrcPathAsset, uSrcIssuer, uSrcAccount)) , convert_all_(convertAllCheck(mDstAmount)) , mLedger(cache->getLedger()) - , mRLCache(cache) + , mAssetCache(cache) , app_(app) , j_(app.journal("Pathfinder")) { XRPL_ASSERT( - !uSrcIssuer || isXRP(uSrcCurrency) == isXRP(uSrcIssuer.value()), + !uSrcIssuer || uSrcPathAsset.isXRP() == isXRP(uSrcIssuer.value()), "ripple::Pathfinder::Pathfinder : valid inputs"); } @@ -212,7 +239,7 @@ Pathfinder::findPaths( } if (mSrcAccount == mDstAccount && mDstAccount == mEffectiveDst && - mSrcCurrency == mDstAmount.getCurrency()) + mSrcPathAsset == mDstAmount.asset()) { // No need to send to same account with same currency. JLOG(j_.debug()) << "Tried to send to same issuer"; @@ -220,26 +247,26 @@ Pathfinder::findPaths( return false; } - if (mSrcAccount == mEffectiveDst && - mSrcCurrency == mDstAmount.getCurrency()) + if (mSrcAccount == mEffectiveDst && mSrcPathAsset == mDstAmount.asset()) { // Default path might work, but any path would loop return true; } m_loadEvent = app_.getJobQueue().makeLoadEvent(jtPATH_FIND, "FindPath"); - auto currencyIsXRP = isXRP(mSrcCurrency); + auto currencyIsXRP = isXRP(mSrcPathAsset); bool useIssuerAccount = mSrcIssuer && !currencyIsXRP && !isXRP(*mSrcIssuer); auto& account = useIssuerAccount ? *mSrcIssuer : mSrcAccount; auto issuer = currencyIsXRP ? AccountID() : account; - mSource = STPathElement(account, mSrcCurrency, issuer); + mSource = STPathElement(account, mSrcPathAsset, issuer); auto issuerString = mSrcIssuer ? to_string(*mSrcIssuer) : std::string("none"); - JLOG(j_.trace()) << "findPaths>" << " mSrcAccount=" << mSrcAccount + JLOG(j_.trace()) << "findPaths>" + << " mSrcAccount=" << mSrcAccount << " mDstAccount=" << mDstAccount << " mDstAmount=" << mDstAmount.getFullText() - << " mSrcCurrency=" << mSrcCurrency + << " mSrcPathAsset=" << mSrcPathAsset << " mSrcIssuer=" << issuerString; if (!mLedger) @@ -248,8 +275,8 @@ Pathfinder::findPaths( return false; } - bool bSrcXrp = isXRP(mSrcCurrency); - bool bDstXrp = isXRP(mDstAmount.getCurrency()); + bool bSrcXrp = isXRP(mSrcPathAsset); + bool bDstXrp = isXRP(mDstAmount.asset()); if (!mLedger->exists(keylet::account(mSrcAccount))) { @@ -306,7 +333,7 @@ Pathfinder::findPaths( JLOG(j_.debug()) << "non-XRP to XRP payment"; paymentType = pt_nonXRP_to_XRP; } - else if (mSrcCurrency == mDstAmount.getCurrency()) + else if (mSrcPathAsset == mDstAmount.asset()) { // non-XRP -> non-XRP - Same currency JLOG(j_.debug()) << "non-XRP to non-XRP - same currency"; @@ -583,7 +610,7 @@ Pathfinder::getBestPaths( fullLiquidityPath.empty(), "ripple::Pathfinder::getBestPaths : first empty path result"); const bool issuerIsSender = - isXRP(mSrcCurrency) || (srcIssuer == mSrcAccount); + isXRP(mSrcPathAsset) || (srcIssuer == mSrcAccount); std::vector extraPathRanks; rankPaths(maxPaths, extraPaths, extraPathRanks, continueCallback); @@ -700,28 +727,28 @@ Pathfinder::getBestPaths( } bool -Pathfinder::issueMatchesOrigin(Issue const& issue) +Pathfinder::issueMatchesOrigin(Asset const& asset) { - bool matchingCurrency = (issue.currency == mSrcCurrency); - bool matchingAccount = isXRP(issue.currency) || - (mSrcIssuer && issue.account == mSrcIssuer) || - issue.account == mSrcAccount; + bool matchingAsset = (asset == mSrcPathAsset); + bool matchingAccount = isXRP(asset) || + (mSrcIssuer && asset.getIssuer() == mSrcIssuer) || + asset.getIssuer() == mSrcAccount; - return matchingCurrency && matchingAccount; + return matchingAsset && matchingAccount; } int Pathfinder::getPathsOut( - Currency const& currency, + PathAsset const& pathAsset, AccountID const& account, LineDirection direction, - bool isDstCurrency, + bool isDstAsset, AccountID const& dstAccount, std::function const& continueCallback) { - Issue const issue(currency, account); + Asset const asset = assetFromPathAsset(pathAsset, account); - auto [it, inserted] = mPathsOutCountMap.emplace(issue, 0); + auto [it, inserted] = mPathsOutCountMap.emplace(asset, 0); // If it was already present, return the stored number of paths if (!inserted) @@ -733,41 +760,82 @@ 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()) + { + if (auto const lines = + mAssetCache->getRippleLines(account, direction)) + { + for (auto const& rspEntry : *lines) + { + if (pathAsset.get() != + rspEntry.getLimit().get().currency) + { + } + 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 (auto const mpts = mAssetCache->getMPTs(account)) { - for (auto const& rspEntry : *lines) + for (auto const& mpt : *mpts) { - if (currency != rspEntry.getLimit().getCurrency()) + if (pathAsset.get() != mpt.getMptID()) { } - else if ( - rspEntry.getBalance() <= beast::zero && - (!rspEntry.getLimitPeer() || - -rspEntry.getBalance() >= rspEntry.getLimitPeer() || - (bAuthRequired && !rspEntry.getAuth()))) + else if (mpt.isZeroBalance() || mpt.isMaxedOut()) { } - else if ( - isDstCurrency && dstAccount == rspEntry.getAccountIDPeer()) + else if (bAuthRequired) { - 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 + count += 10000; } - else if (rspEntry.getFreezePeer()) + else if (bFrozen) { - // Not a useful path out } else { @@ -925,7 +993,8 @@ Pathfinder::isNoRippleOut(STPath const& currentPath) ? mSrcAccount : (currentPath.end() - 2)->getAccountID(); auto const& toAccount = endElement.getAccountID(); - return isNoRipple(fromAccount, toAccount, endElement.getCurrency()); + return endElement.hasCurrency() && + isNoRipple(fromAccount, toAccount, endElement.getCurrency()); } void @@ -949,10 +1018,10 @@ Pathfinder::addLink( std::function const& continueCallback) { auto const& pathEnd = currentPath.empty() ? mSource : currentPath.back(); - auto const& uEndCurrency = pathEnd.getCurrency(); + auto const& uEndPathAsset = pathEnd.getPathAsset(); auto const& uEndIssuer = pathEnd.getIssuerID(); auto const& uEndAccount = pathEnd.getAccountID(); - bool const bOnXRP = uEndCurrency.isZero(); + bool const bOnXRP = isXRP(uEndPathAsset); // Does pathfinding really need to get this to // a gateway (the issuer of the destination amount) @@ -984,27 +1053,40 @@ Pathfinder::addLink( { bool const bRequireAuth( sleEnd->getFieldU32(sfFlags) & lsfRequireAuth); - bool const bIsEndCurrency( - uEndCurrency == mDstAmount.getCurrency()); + bool const bIsEndAsset(uEndPathAsset == mDstAmount.asset()); bool const bIsNoRippleOut(isNoRippleOut(currentPath)); bool const bDestOnly(addFlags & afAC_LAST); - if (auto const lines = mRLCache->getRippleLines( - uEndAccount, - bIsNoRippleOut ? LineDirection::incoming - : LineDirection::outgoing)) - { - auto& rippleLines = *lines; + AccountCandidates candidates; + + auto forAssets = [&]( + AssetType const& assets) { + candidates.reserve(assets.size()); - AccountCandidates candidates; - candidates.reserve(rippleLines.size()); + static bool constexpr isLine = std:: + is_same_v>; + static bool constexpr isMPT = + std::is_same_v>; - for (auto const& rs : rippleLines) + for (auto const& asset : assets) { if (continueCallback && !continueCallback()) return; - auto const& acct = rs.getAccountIDPeer(); - LineDirection const direction = rs.getDirectionPeer(); + auto const& acct = [&]() constexpr { + if constexpr (isLine) + return asset.getAccountIDPeer(); + // Unlike trustline, MPT is not bidirectional + if constexpr (isMPT) + return getMPTIssuer(asset); + }(); + auto const direction = + [&]() constexpr -> LineDirection { + if constexpr (isLine) + return asset.getDirectionPeer(); + // incoming for MPT since MPT doesn't support + // rippling (see LineDirection comments) + return LineDirection::incoming; + }(); if (hasEffectiveDestination && (acct == mDstAccount)) { @@ -1019,26 +1101,49 @@ Pathfinder::addLink( continue; } - if ((uEndCurrency == rs.getLimit().getCurrency()) && - !currentPath.hasSeen(acct, uEndCurrency, acct)) + auto const correctAsset = [&]() { + if constexpr (isLine) + return uEndPathAsset.get() == + asset.getLimit() + .template get() + .currency; + if constexpr (isMPT) + return uEndPathAsset.get() == + asset.getMptID(); + }(); + auto checkAsset = [&]() { + if constexpr (isLine) + { + return ( + (asset.getBalance() <= beast::zero && + (!asset.getLimitPeer() || + -asset.getBalance() >= + asset.getLimitPeer() || + (bRequireAuth && !asset.getAuth()))) || + (bIsNoRippleOut && asset.getNoRipple())); + } + if constexpr (isMPT) + { + return asset.isZeroBalance() || + asset.isMaxedOut() || + requireAuth( + *mLedger, MPTIssue{asset}, acct); + } + }; + + 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 (checkAsset()) { // Can't leave on this path } else if (bToDestination) { // destination is always worth trying - if (uEndCurrency == mDstAmount.getCurrency()) + if (uEndPathAsset == mDstAmount.asset()) { // this is a complete path if (!currentPath.empty()) @@ -1066,10 +1171,10 @@ Pathfinder::addLink( { // save this candidate int out = getPathsOut( - uEndCurrency, + uEndPathAsset, acct, direction, - bIsEndCurrency, + bIsEndAsset, mEffectiveDst, continueCallback); if (out) @@ -1077,40 +1182,54 @@ Pathfinder::addLink( } } } + }; - if (!candidates.empty()) + if (uEndPathAsset.holds()) + { + if (auto const lines = mAssetCache->getRippleLines( + uEndAccount, + bIsNoRippleOut ? LineDirection::incoming + : LineDirection::outgoing)) { - std::sort( - candidates.begin(), - candidates.end(), - std::bind( - compareAccountCandidate, - mLedger->seq(), - std::placeholders::_1, - std::placeholders::_2)); - - int count = candidates.size(); - // allow more paths from source - if ((count > 10) && (uEndAccount != mSrcAccount)) - count = 10; - else if (count > 50) - count = 50; - - auto it = candidates.begin(); - while (count-- != 0) - { - if (continueCallback && !continueCallback()) - return; - // Add accounts to incompletePaths - STPathElement pathElement( - STPathElement::typeAccount, - it->account, - uEndCurrency, - it->account); - incompletePaths.assembleAdd( - currentPath, pathElement); - ++it; - } + forAssets(*lines); + } + } + else if (auto const mpts = mAssetCache->getMPTs(uEndAccount)) + { + forAssets(*mpts); + } + + if (!candidates.empty()) + { + std::sort( + candidates.begin(), + candidates.end(), + std::bind( + compareAccountCandidate, + mLedger->seq(), + std::placeholders::_1, + std::placeholders::_2)); + + int count = candidates.size(); + // allow more paths from source + if ((count > 10) && (uEndAccount != mSrcAccount)) + count = 10; + else if (count > 50) + count = 50; + + auto it = candidates.begin(); + while (count-- != 0) + { + if (continueCallback && !continueCallback()) + return; + // Add accounts to incompletePaths + STPathElement pathElement( + STPathElement::typeAccount, + it->account, + uEndPathAsset, + it->account); + incompletePaths.assembleAdd(currentPath, pathElement); + ++it; } } } @@ -1127,7 +1246,8 @@ Pathfinder::addLink( { // to XRP only if (!bOnXRP && - app_.getOrderBookDB().isBookToXRP({uEndCurrency, uEndIssuer})) + app_.getOrderBookDB().isBookToXRP( + assetFromPathAsset(uEndPathAsset, uEndIssuer))) { STPathElement pathElement( STPathElement::typeCurrency, @@ -1141,7 +1261,7 @@ Pathfinder::addLink( { bool bDestOnly = (addFlags & afOB_LAST) != 0; auto books = app_.getOrderBookDB().getBooksByTakerPays( - {uEndCurrency, uEndIssuer}); + assetFromPathAsset(uEndPathAsset, uEndIssuer)); JLOG(j_.trace()) << books.size() << " books found from this currency/issuer"; @@ -1150,14 +1270,13 @@ Pathfinder::addLink( if (continueCallback && !continueCallback()) return; if (!currentPath.hasSeen( - xrpAccount(), book.out.currency, book.out.account) && + xrpAccount(), book.out, book.out.getIssuer()) && !issueMatchesOrigin(book.out) && - (!bDestOnly || - (book.out.currency == mDstAmount.getCurrency()))) + (!bDestOnly || equalTokens(book.out, mDstAmount.asset()))) { STPath newPath(currentPath); - if (book.out.currency.isZero()) + if (isXRP(book.out)) { // to XRP // add the order book itself @@ -1167,7 +1286,7 @@ Pathfinder::addLink( xrpCurrency(), xrpAccount()); - if (mDstAmount.getCurrency().isZero()) + if (isXRP(mDstAmount.asset())) { // destination is XRP, add account and path is // complete @@ -1180,10 +1299,13 @@ Pathfinder::addLink( incompletePaths.push_back(newPath); } else if (!currentPath.hasSeen( - book.out.account, - book.out.currency, - book.out.account)) + book.out.getIssuer(), + book.out, + book.out.getIssuer())) { + auto const assetType = book.out.holds() + ? STPathElement::typeCurrency + : STPathElement::typeMPT; // Don't want the book if we've already seen the issuer // book -> account -> book if ((newPath.size() >= 2) && @@ -1192,32 +1314,30 @@ Pathfinder::addLink( { // replace the redundant account with the order book newPath[newPath.size() - 1] = STPathElement( - STPathElement::typeCurrency | - STPathElement::typeIssuer, + assetType | STPathElement::typeIssuer, xrpAccount(), - book.out.currency, - book.out.account); + book.out, + book.out.getIssuer()); } else { // add the order book newPath.emplace_back( - STPathElement::typeCurrency | - STPathElement::typeIssuer, + assetType | STPathElement::typeIssuer, xrpAccount(), - book.out.currency, - book.out.account); + book.out, + book.out.getIssuer()); } if (hasEffectiveDestination && - book.out.account == mDstAccount && - book.out.currency == mDstAmount.getCurrency()) + book.out.getIssuer() == mDstAccount && + equalTokens(book.out, mDstAmount.asset())) { // We skipped a required issuer } else if ( - book.out.account == mEffectiveDst && - book.out.currency == mDstAmount.getCurrency()) + book.out.getIssuer() == mEffectiveDst && + equalTokens(book.out, mDstAmount.asset())) { // with the destination account, this path is // complete JLOG(j_.trace()) @@ -1232,9 +1352,9 @@ Pathfinder::addLink( 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..10d8896a8d2 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, + 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..baa677ae904 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() || + !equalTokens(saMaxAmountReq.asset(), saDstAmountReq.asset()) || saMaxAmountReq.getIssuer() != uSrcAccountID) { return saMaxAmountReq; diff --git a/src/xrpld/app/paths/detail/AMMLiquidity.cpp b/src/xrpld/app/paths/detail/AMMLiquidity.cpp index 813554ba7ff..d0e1193b5ee 100644 --- a/src/xrpld/app/paths/detail/AMMLiquidity.cpp +++ b/src/xrpld/app/paths/detail/AMMLiquidity.cpp @@ -27,15 +27,15 @@ AMMLiquidity::AMMLiquidity( ReadView const& view, AccountID const& ammAccountID, std::uint32_t tradingFee, - Issue const& in, - Issue const& out, + Asset const& in, + Asset const& out, AMMContext& ammContext, beast::Journal j) : ammContext_(ammContext) , ammAccountID_(ammAccountID) , tradingFee_(tradingFee) - , issueIn_(in) - , issueOut_(out) + , assetIn_(in) + , assetOut_(out) , initialBalances_{fetchBalances(view)} , j_(j) { @@ -45,13 +45,13 @@ template TAmounts AMMLiquidity::fetchBalances(ReadView const& view) const { - auto const assetIn = ammAccountHolds(view, ammAccountID_, issueIn_); - auto const assetOut = ammAccountHolds(view, ammAccountID_, issueOut_); + auto const amountIn = ammAccountHolds(view, ammAccountID_, assetIn_); + auto const amountOut = ammAccountHolds(view, ammAccountID_, assetOut_); // This should not happen. - if (assetIn < beast::zero || assetOut < beast::zero) + if (amountIn < beast::zero || amountOut < beast::zero) Throw("AMMLiquidity: invalid balances"); - return TAmounts{get(assetIn), get(assetOut)}; + return TAmounts{get(amountIn), get(amountOut)}; } template @@ -62,7 +62,7 @@ AMMLiquidity::generateFibSeqOffer( TAmounts cur{}; cur.in = toAmount( - getIssue(balances.in), + getAsset(balances.in), InitialFibSeqPct * initialBalances_.in, Number::rounding_mode::upward); cur.out = swapAssetIn(initialBalances_, cur.in, tradingFee_); @@ -82,7 +82,7 @@ AMMLiquidity::generateFibSeqOffer( "ripple::AMMLiquidity::generateFibSeqOffer : maximum iterations"); cur.out = toAmount( - getIssue(balances.out), + getAsset(balances.out), cur.out * fib[ammContext_.curIters() - 1], Number::rounding_mode::downward); // swapAssetOut() returns negative in this case @@ -106,14 +106,16 @@ maxAmount() return IOUAmount(STAmount::cMaxValue / 2, STAmount::cMaxOffset); else if constexpr (std::is_same_v) return STAmount(STAmount::cMaxValue / 2, STAmount::cMaxOffset); + else if constexpr (std::is_same_v) + return MPTAmount(maxMPTokenAmount); } template T -maxOut(T const& out, Issue const& iss) +maxOut(T const& out, Asset const& asset) { Number const res = out * Number{99, -2}; - return toAmount(iss, res, Number::rounding_mode::downward); + return toAmount(asset, res, Number::rounding_mode::downward); } } // namespace @@ -134,7 +136,7 @@ AMMLiquidity::maxOffer( } else { - auto const out = maxOut(balances.out, issueOut()); + auto const out = maxOut(balances.out, assetOut()); if (out <= TOut{0} || out >= balances.out) return std::nullopt; return AMMOffer( @@ -243,8 +245,8 @@ AMMLiquidity::getOffer( { JLOG(j_.trace()) << "AMMLiquidity::getOffer, created " - << to_string(offer->amount().in) << "/" << issueIn_ << " " - << to_string(offer->amount().out) << "/" << issueOut_; + << to_string(offer->amount().in) << "/" << assetIn_ << " " + << to_string(offer->amount().out) << "/" << assetOut_; return offer; } @@ -259,9 +261,13 @@ AMMLiquidity::getOffer( return std::nullopt; } -template class AMMLiquidity; template class AMMLiquidity; template class AMMLiquidity; template class AMMLiquidity; +template class AMMLiquidity; +template class AMMLiquidity; +template class AMMLiquidity; +template class AMMLiquidity; +template class AMMLiquidity; } // namespace ripple diff --git a/src/xrpld/app/paths/detail/AMMOffer.cpp b/src/xrpld/app/paths/detail/AMMOffer.cpp index 16ea8628f3b..b543faf3199 100644 --- a/src/xrpld/app/paths/detail/AMMOffer.cpp +++ b/src/xrpld/app/paths/detail/AMMOffer.cpp @@ -23,7 +23,7 @@ namespace ripple { -template +template AMMOffer::AMMOffer( AMMLiquidity const& ammLiquidity, TAmounts const& amounts, @@ -37,28 +37,35 @@ AMMOffer::AMMOffer( { } -template -Issue const& -AMMOffer::issueIn() const +template +Asset const& +AMMOffer::assetIn() const { - return ammLiquidity_.issueIn(); + return ammLiquidity_.assetIn(); } -template +template +Asset const& +AMMOffer::assetOut() const +{ + return ammLiquidity_.assetOut(); +} + +template AccountID const& AMMOffer::owner() const { return ammLiquidity_.ammAccount(); } -template +template TAmounts const& AMMOffer::amount() const { return amounts_; } -template +template void AMMOffer::consume( ApplyView& view, @@ -76,7 +83,7 @@ AMMOffer::consume( ammLiquidity_.context().setAMMUsed(); } -template +template TAmounts AMMOffer::limitOut( TAmounts const& offrAmt, @@ -106,7 +113,7 @@ AMMOffer::limitOut( return {swapAssetOut(balances_, limit, ammLiquidity_.tradingFee()), limit}; } -template +template TAmounts AMMOffer::limitIn( TAmounts const& offrAmt, @@ -125,7 +132,7 @@ AMMOffer::limitIn( return {limit, swapAssetIn(balances_, limit, ammLiquidity_.tradingFee())}; } -template +template QualityFunction AMMOffer::getQualityFunc() const { @@ -135,7 +142,7 @@ AMMOffer::getQualityFunc() const balances_, ammLiquidity_.tradingFee(), QualityFunction::AMMTag{}}; } -template +template bool AMMOffer::checkInvariant( TAmounts const& consumed, @@ -173,9 +180,13 @@ AMMOffer::checkInvariant( return false; } -template class AMMOffer; template class AMMOffer; template class AMMOffer; template class AMMOffer; +template class AMMOffer; +template class AMMOffer; +template class AMMOffer; +template class AMMOffer; +template class AMMOffer; } // namespace ripple diff --git a/src/xrpld/app/paths/detail/AmountSpec.h b/src/xrpld/app/paths/detail/AmountSpec.h index d57e9140f80..3e50732fef1 100644 --- a/src/xrpld/app/paths/detail/AmountSpec.h +++ b/src/xrpld/app/paths/detail/AmountSpec.h @@ -32,112 +32,125 @@ struct AmountSpec { explicit AmountSpec() = default; - bool native; - union - { - XRPAmount xrp; - IOUAmount iou = {}; - }; - std::optional issuer; - std::optional currency; + std::variant amount; - friend std::ostream& - operator<<(std::ostream& stream, AmountSpec const& amt) + bool + native() const { - if (amt.native) - stream << to_string(amt.xrp); - else - stream << to_string(amt.iou); - if (amt.currency) - stream << "/(" << *amt.currency << ")"; - if (amt.issuer) - stream << "/" << *amt.issuer << ""; - return stream; + 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); } }; 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& -get(EitherAmount& amt) -{ - static_assert(sizeof(T) == -1, "Must used specialized function"); - return T(0); -} - -template <> -inline IOUAmount& -get(EitherAmount& amt) -{ - XRPL_ASSERT( - !amt.native, "ripple::get(EitherAmount&) : is not XRP"); - return amt.iou; -} - -template <> -inline XRPAmount& -get(EitherAmount& amt) -{ - XRPL_ASSERT(amt.native, "ripple::get(EitherAmount&) : is XRP"); - return amt.xrp; -} - template T const& get(EitherAmount const& amt) @@ -151,9 +164,9 @@ inline IOUAmount const& get(EitherAmount const& amt) { XRPL_ASSERT( - !amt.native, + !amt.native(), "ripple::get(EitherAmount const&) : is not XRP"); - return amt.iou; + return amt.iou(); } template <> @@ -161,63 +174,17 @@ inline XRPAmount const& get(EitherAmount const& amt) { XRPL_ASSERT( - amt.native, "ripple::get(EitherAmount const&) : is XRP"); - return amt.xrp; + amt.native(), "ripple::get(EitherAmount const&) : is XRP"); + return amt.xrp(); } -inline AmountSpec -toAmountSpec(STAmount const& amt) -{ - XRPL_ASSERT( - amt.mantissa() < std::numeric_limits::max(), - "ripple::toAmountSpec(STAmount const&) : maximum mantissa"); - bool const isNeg = amt.negative(); - std::int64_t const sMant = - isNeg ? -std::int64_t(amt.mantissa()) : amt.mantissa(); - AmountSpec result; - - result.native = isXRP(amt); - if (result.native) - { - result.xrp = XRPAmount(sMant); - } - else - { - result.iou = IOUAmount(sMant, amt.exponent()); - result.issuer = amt.issue().account; - result.currency = amt.issue().currency; - } - - return result; -} - -inline EitherAmount -toEitherAmount(STAmount const& amt) -{ - if (isXRP(amt)) - return EitherAmount{amt.xrp()}; - return EitherAmount{amt.iou()}; -} - -inline AmountSpec -toAmountSpec(EitherAmount const& ea, std::optional const& c) +template <> +inline MPTAmount const& +get(EitherAmount const& amt) { - AmountSpec r; - r.native = (!c || isXRP(*c)); - r.currency = c; XRPL_ASSERT( - ea.native == r.native, - "ripple::toAmountSpec(EitherAmount const&&, std::optional) : " - "matching native"); - if (r.native) - { - r.xrp = ea.xrp; - } - else - { - r.iou = ea.iou; - } - return r; + amt.isMPT(), "ripple::get(EitherAmount const&) : is MPT"); + return amt.mpt(); } } // namespace ripple diff --git a/src/xrpld/app/paths/detail/BookStep.cpp b/src/xrpld/app/paths/detail/BookStep.cpp index 1d35f80b183..e92b28d8f90 100644 --- a/src/xrpld/app/paths/detail/BookStep.cpp +++ b/src/xrpld/app/paths/detail/BookStep.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -90,7 +91,7 @@ class BookStep : public StepImp> } public: - BookStep(StrandContext const& ctx, Issue const& in, Issue const& out) + BookStep(StrandContext const& ctx, Asset const& in, Asset const& out) : maxOffersToConsume_(getMaxOffersToConsume(ctx)) , book_(in, out) , strandSrc_(ctx.strandSrc) @@ -189,13 +190,18 @@ class BookStep : public StepImp> logStringImpl(char const* name) const { std::ostringstream ostr; - ostr << name << ": " << "\ninIss: " << book_.in.account - << "\noutIss: " << book_.out.account - << "\ninCur: " << book_.in.currency - << "\noutCur: " << book_.out.currency; + ostr << name << ": " + << "\ninIss: " << book_.in.getIssuer() + << "\noutIss: " << book_.out.getIssuer() + << "\ninCur: " << to_string(book_.in) + << "\noutCur: " << to_string(book_.out); return ostr.str(); } + Rate + rate(ReadView const& view, Asset const& asset, AccountID const& dstAccount) + const; + private: friend bool operator==(BookStep const& lhs, BookStep const& rhs) @@ -337,19 +343,14 @@ class BookPaymentStep : public BookStep> // (the old code does not charge a fee) // Calculate amount that goes to the taker and the amount charged the // offer owner - auto rate = [&](AccountID const& id) { - if (isXRP(id) || id == this->strandDst_) - return parityRate; - return transferRate(v, id); - }; - - auto const trIn = - redeems(prevStepDir) ? rate(this->book_.in.account) : parityRate; + auto const trIn = redeems(prevStepDir) + ? this->rate(v, this->book_.in, this->strandDst_) + : parityRate; // Always charge the transfer fee, even if the owner is the issuer, // unless the fee is waived auto const trOut = (this->ownerPaysTransferFee_ && waiveFee == WaiveTransferFee::No) - ? rate(this->book_.out.account) + ? this->rate(v, this->book_.out, this->strandDst_) : parityRate; Quality const q1{getRate(STAmount(trOut.value), STAmount(trIn.value))}; @@ -391,8 +392,8 @@ class BookOfferCrossingStep public: BookOfferCrossingStep( StrandContext const& ctx, - Issue const& in, - Issue const& out) + Asset const& in, + Asset const& out) : BookStep>(ctx, in, out) , defaultPath_(ctx.isDefaultPath) , qualityThreshold_(getQuality(ctx.limitQuality)) @@ -540,14 +541,9 @@ class BookOfferCrossingStep (this->ammLiquidity_ && this->ammLiquidity_->multiPath())) return ofrQ; - auto rate = [&](AccountID const& id) { - if (isXRP(id) || id == this->strandDst_) - return parityRate; - return transferRate(v, id); - }; - - auto const trIn = - redeems(prevStepDir) ? rate(this->book_.in.account) : parityRate; + auto const trIn = redeems(prevStepDir) + ? this->rate(v, this->book_.in, this->strandDst_) + : parityRate; // AMM doesn't pay the transfer fee on the out amount auto const trOut = parityRate; @@ -723,17 +719,13 @@ BookStep::forEachOffer( // (the old code does not charge a fee) // Calculate amount that goes to the taker and the amount charged the offer // owner - auto rate = [this, &sb](AccountID const& id) -> std::uint32_t { - if (isXRP(id) || id == this->strandDst_) - return QUALITY_ONE; - return transferRate(sb, id).value; - }; - - std::uint32_t const trIn = - redeems(prevStepDir) ? rate(book_.in.account) : QUALITY_ONE; + std::uint32_t const trIn = redeems(prevStepDir) + ? rate(sb, book_.in, this->strandDst_).value + : QUALITY_ONE; // Always charge the transfer fee, even if the owner is the issuer - std::uint32_t const trOut = - ownerPaysTransferFee_ ? rate(book_.out.account) : QUALITY_ONE; + std::uint32_t const trOut = ownerPaysTransferFee_ + ? rate(sb, book_.out, this->strandDst_).value + : QUALITY_ONE; typename FlowOfferStream::StepCounter counter( maxOffersToConsume_, j_); @@ -741,7 +733,6 @@ BookStep::forEachOffer( FlowOfferStream offers( sb, afView, book_, sb.parentCloseTime(), counter, j_); - bool const flowCross = afView.rules().enabled(featureFlowCross); bool offerAttempted = false; std::optional ofrQ; auto execOffer = [&](auto& offer) { @@ -756,36 +747,19 @@ BookStep::forEachOffer( strandSrc_, strandDst_, offer, ofrQ, offers, offerAttempted)) return true; - // Make sure offer owner has authorization to own IOUs from issuer. - // An account can always own XRP or their own IOUs. - if (flowCross && (!isXRP(offer.issueIn().currency)) && - (offer.owner() != offer.issueIn().account)) + // Make sure offer owner has authorization to own Assets from issuer. + // An account can always own XRP or their own Assets. + if (requireAuth(afView, offer.assetIn(), offer.owner()) != tesSUCCESS) { - auto const& issuerID = offer.issueIn().account; - auto const issuer = afView.read(keylet::account(issuerID)); - if (issuer && ((*issuer)[sfFlags] & lsfRequireAuth)) - { - // Issuer requires authorization. See if offer owner has that. - auto const& ownerID = offer.owner(); - auto const authFlag = - issuerID > ownerID ? lsfHighAuth : lsfLowAuth; - - auto const line = afView.read( - keylet::line(ownerID, issuerID, offer.issueIn().currency)); - - if (!line || (((*line)[sfFlags] & authFlag) == 0)) - { - // Offer owner not authorized to hold IOU from issuer. - // Remove this offer even if no crossing occurs. - if (auto const key = offer.key()) - offers.permRmOffer(*key); - if (!offerAttempted) - // Change quality only if no previous offers were tried. - ofrQ = std::nullopt; - // Returning true causes offers.step() to delete the offer. - return true; - } - } + // Offer owner not authorized to hold IOU/MPT from issuer. + // Remove this offer even if no crossing occurs. + if (auto const key = offer.key()) + offers.permRmOffer(*key); + if (!offerAttempted) + // Change quality only if no previous offers were tried. + ofrQ = std::nullopt; + // Returning true causes offers.step() to delete the offer. + return true; } if (!static_cast(this)->checkQualityThreshold( @@ -893,7 +867,7 @@ BookStep::consumeOffer( { auto const dr = offer.send( sb, - book_.in.account, + book_.in.getIssuer(), offer.owner(), toSTAmount(ofrAmt.in, book_.in), j_); @@ -907,7 +881,7 @@ BookStep::consumeOffer( auto const cr = offer.send( sb, offer.owner(), - book_.out.account, + book_.out.getIssuer(), toSTAmount(ownerGives, book_.out), j_); if (cr != tesSUCCESS) @@ -1354,20 +1328,21 @@ BookStep::check(StrandContext const& ctx) const // Do not allow two books to output the same issue. This may cause offers on // one step to unfund offers in another step. if (!ctx.seenBookOuts.insert(book_.out).second || - ctx.seenDirectIssues[0].count(book_.out)) + ctx.seenDirectAssets[0].count(book_.out)) { JLOG(j_.debug()) << "BookStep: loop detected: " << *this; return temBAD_PATH_LOOP; } - if (ctx.seenDirectIssues[1].count(book_.out)) + if (ctx.seenDirectAssets[1].count(book_.out)) { JLOG(j_.debug()) << "BookStep: loop detected: " << *this; return temBAD_PATH_LOOP; } - auto issuerExists = [](ReadView const& view, Issue const& iss) -> bool { - return isXRP(iss.account) || view.read(keylet::account(iss.account)); + auto issuerExists = [](ReadView const& view, Asset const& iss) -> bool { + return isXRP(iss.getIssuer()) || + view.read(keylet::account(iss.getIssuer())); }; if (!issuerExists(ctx.view, book_.in) || !issuerExists(ctx.view, book_.out)) @@ -1381,20 +1356,53 @@ BookStep::check(StrandContext const& ctx) const if (auto const prev = ctx.prevStep->directStepSrcAcct()) { auto const& view = ctx.view; - auto const& cur = book_.in.account; - - auto sle = view.read(keylet::line(*prev, cur, book_.in.currency)); - if (!sle) - return terNO_LINE; - if ((*sle)[sfFlags] & - ((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple)) - return terNO_RIPPLE; + auto const& cur = book_.in.getIssuer(); + + if (book_.in.holds()) + { + auto sle = view.read( + keylet::line(*prev, cur, book_.in.get().currency)); + if (!sle) + return terNO_LINE; + if ((*sle)[sfFlags] & + ((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple)) + return terNO_RIPPLE; + } + else + { + auto const issuanceID = + keylet::mptIssuance(book_.in.get().getMptID()); + if (!view.exists(issuanceID)) + return tecOBJECT_NOT_FOUND; + + if (auto const ter = isMPTDEXAllowed( + view, + book_.in, + book_.in.getIssuer(), + book_.in.getIssuer()); + ter != tesSUCCESS) + return ter; + } } } return tesSUCCESS; } +template +Rate +BookStep::rate( + ReadView const& view, + Asset const& asset, + AccountID const& dstAccount) const +{ + if (isXRP(asset) || asset.getIssuer() == dstAccount) + return parityRate; + if (asset.holds()) + return transferRate(view, asset.getIssuer()); + return transferRate(view, asset.get().getMptID()); +}; + //------------------------------------------------------------------------------ namespace test { @@ -1412,29 +1420,20 @@ equalHelper(Step const& step, ripple::Book const& book) bool bookStepEqual(Step const& step, ripple::Book const& book) { - bool const inXRP = isXRP(book.in.currency); - bool const outXRP = isXRP(book.out.currency); - if (inXRP && outXRP) + if (isXRP(book.in) && isXRP(book.out)) { UNREACHABLE("ripple::test::bookStepEqual : no XRP to XRP book step"); return false; // no such thing as xrp/xrp book step } - if (inXRP && !outXRP) - return equalHelper< - XRPAmount, - IOUAmount, - BookPaymentStep>(step, book); - if (!inXRP && outXRP) - return equalHelper< - IOUAmount, - XRPAmount, - BookPaymentStep>(step, book); - if (!inXRP && !outXRP) - return equalHelper< - IOUAmount, - IOUAmount, - BookPaymentStep>(step, book); - return false; + return std::visit( + [&](TIn const&, TOut const&) { + using TIn_ = typename TIn::amount_type; + using TOut_ = typename TOut::amount_type; + return equalHelper>( + step, book); + }, + book.in.getAmountType(), + book.out.getAmountType()); } } // namespace test @@ -1442,7 +1441,7 @@ bookStepEqual(Step const& step, ripple::Book const& book) template static std::pair> -make_BookStepHelper(StrandContext const& ctx, Issue const& in, Issue const& out) +make_BookStepHelper(StrandContext const& ctx, Asset const& in, Asset const& out) { TER ter = tefINTERNAL; std::unique_ptr r; @@ -1484,4 +1483,38 @@ make_BookStepXI(StrandContext const& ctx, Issue const& out) return make_BookStepHelper(ctx, xrpIssue(), out); } +// MPT's +std::pair> +make_BookStepMM( + StrandContext const& ctx, + MPTIssue const& in, + MPTIssue const& out) +{ + return make_BookStepHelper(ctx, in, out); +} + +std::pair> +make_BookStepMI(StrandContext const& ctx, MPTIssue const& in, Issue const& out) +{ + return make_BookStepHelper(ctx, in, out); +} + +std::pair> +make_BookStepIM(StrandContext const& ctx, Issue const& in, MPTIssue const& out) +{ + return make_BookStepHelper(ctx, in, out); +} + +std::pair> +make_BookStepMX(StrandContext const& ctx, MPTIssue const& in) +{ + return make_BookStepHelper(ctx, in, xrpIssue()); +} + +std::pair> +make_BookStepXM(StrandContext const& ctx, MPTIssue const& out) +{ + return make_BookStepHelper(ctx, xrpIssue(), out); +} + } // namespace ripple diff --git a/src/xrpld/app/paths/detail/DirectStep.cpp b/src/xrpld/app/paths/detail/DirectStep.cpp index 46aa129ac71..697cef3bab9 100644 --- a/src/xrpld/app/paths/detail/DirectStep.cpp +++ b/src/xrpld/app/paths/detail/DirectStep.cpp @@ -723,7 +723,8 @@ DirectStepI::validFwd( auto const savCache = *cache_; - XRPL_ASSERT(!in.native, "ripple::DirectStepI::validFwd : input is not XRP"); + XRPL_ASSERT( + !in.native(), "ripple::DirectStepI::validFwd : input is not XRP"); auto const [maxSrcToDst, srcDebtDir] = static_cast(this)->maxFlow(sb, cache_->srcToDst); @@ -732,7 +733,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&) { @@ -940,13 +941,13 @@ DirectStepI::check(StrandContext const& ctx) const // issue if (auto book = ctx.prevStep->bookStepBook()) { - if (book->out != srcIssue) + if (book->out.get() != srcIssue) return temBAD_PATH_LOOP; } } - if (!ctx.seenDirectIssues[0].insert(srcIssue).second || - !ctx.seenDirectIssues[1].insert(dstIssue).second) + if (!ctx.seenDirectAssets[0].insert(srcIssue).second || + !ctx.seenDirectAssets[1].insert(dstIssue).second) { JLOG(j_.debug()) << "DirectStepI: loop detected: Index: " << ctx.strandSize diff --git a/src/xrpld/app/paths/detail/FlowDebugInfo.h b/src/xrpld/app/paths/detail/FlowDebugInfo.h index 4c3ea5faf1b..8223f31ca5d 100644 --- a/src/xrpld/app/paths/detail/FlowDebugInfo.h +++ b/src/xrpld/app/paths/detail/FlowDebugInfo.h @@ -238,7 +238,7 @@ struct FlowDebugInfo std::vector const& amts, char delim = ';') { auto get_val = [](EitherAmount const& a) -> std::string { - return ripple::to_string(a.xrp); + return ripple::to_string(a.xrp()); }; write_list(amts, get_val, delim); }; @@ -246,7 +246,7 @@ struct FlowDebugInfo std::vector const& amts, char delim = ';') { auto get_val = [](EitherAmount const& a) -> std::string { - return ripple::to_string(a.iou); + return ripple::to_string(a.iou()); }; write_list(amts, get_val, delim); }; diff --git a/src/xrpld/app/paths/detail/MPTEndpointStep.cpp b/src/xrpld/app/paths/detail/MPTEndpointStep.cpp new file mode 100644 index 00000000000..c30e6c86811 --- /dev/null +++ b/src/xrpld/app/paths/detail/MPTEndpointStep.cpp @@ -0,0 +1,987 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 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 +#include + +namespace ripple { + +template +class MPTEndpointStep + : public StepImp> +{ +protected: + AccountID const src_; + AccountID const dst_; + MPTIssue const mptIssue_; + + // Charge transfer fees when the prev step redeems + Step const* const prevStep_ = nullptr; + bool const isLast_; + // Direct payment between the holders + // Used by maxFlow's last step. + bool const isDirectBetweenHolders_; + beast::Journal const j_; + + struct Cache + { + MPTAmount in; + MPTAmount srcToDst; + MPTAmount out; + DebtDirection srcDebtDir; + + Cache( + MPTAmount const& in_, + MPTAmount const& srcToDst_, + MPTAmount const& out_, + DebtDirection srcDebtDir_) + : in(in_), srcToDst(srcToDst_), out(out_), srcDebtDir(srcDebtDir_) + { + } + }; + + std::optional cache_; + + // Compute the maximum value that can flow from src->dst at + // the best available quality. + // return: first element is max amount that can flow, + // second is the debt direction of the source w.r.t. the dst + std::pair + maxPaymentFlow(ReadView const& sb) const; + + // Compute srcQOut and dstQIn when the source redeems. + std::pair + qualitiesSrcRedeems(ReadView const& sb) const; + + // Compute srcQOut and dstQIn when the source issues. + std::pair + qualitiesSrcIssues(ReadView const& sb, DebtDirection prevStepDebtDirection) + const; + + // Returns srcQOut, dstQIn + std::pair + qualities( + ReadView const& sb, + DebtDirection srcDebtDir, + StrandDirection strandDir) const; + +public: + MPTEndpointStep( + StrandContext const& ctx, + AccountID const& src, + AccountID const& dst, + MPTID const& mpt) + : src_(src) + , dst_(dst) + , mptIssue_(mpt) + , prevStep_(ctx.prevStep) + , isLast_(ctx.isLast) + , isDirectBetweenHolders_( + mptIssue_ == ctx.strandDeliver && + ctx.strandSrc != mptIssue_.getIssuer() && + ctx.strandDst != mptIssue_.getIssuer() && + (ctx.isFirst || (ctx.prevStep && !ctx.prevStep->bookStepBook()))) + , j_(ctx.j) + { + } + + AccountID const& + src() const + { + return src_; + } + AccountID const& + dst() const + { + return dst_; + } + MPTID const& + mptID() const + { + return mptIssue_.getMptID(); + } + + std::optional + cachedIn() const override + { + if (!cache_) + return std::nullopt; + return EitherAmount(cache_->in); + } + + std::optional + cachedOut() const override + { + if (!cache_) + return std::nullopt; + return EitherAmount(cache_->out); + } + + std::optional + directStepSrcAcct() const override + { + return src_; + } + + std::optional> + directStepAccts() const override + { + return std::make_pair(src_, dst_); + } + + DebtDirection + debtDirection(ReadView const& sb, StrandDirection dir) const override; + + std::uint32_t + lineQualityIn(ReadView const& v) const override; + + std::pair, DebtDirection> + qualityUpperBound(ReadView const& v, DebtDirection dir) const override; + + std::pair + revImp( + PaymentSandbox& sb, + ApplyView& afView, + boost::container::flat_set& ofrsToRm, + MPTAmount const& out); + + std::pair + fwdImp( + PaymentSandbox& sb, + ApplyView& afView, + boost::container::flat_set& ofrsToRm, + MPTAmount const& in); + + std::pair + validFwd(PaymentSandbox& sb, ApplyView& afView, EitherAmount const& in) + override; + + // Check for error, existing liquidity, and violations of auth/frozen + // constraints. + TER + check(StrandContext const& ctx) const; + + void + setCacheLimiting( + MPTAmount const& fwdIn, + MPTAmount const& fwdSrcToDst, + MPTAmount const& fwdOut, + DebtDirection srcDebtDir); + + friend bool + operator==(MPTEndpointStep const& lhs, MPTEndpointStep const& rhs) + { + return lhs.src_ == rhs.src_ && lhs.dst_ == rhs.dst_ && + lhs.mptIssue_ == rhs.mptIssue_; + } + + friend bool + operator!=(MPTEndpointStep const& lhs, MPTEndpointStep const& rhs) + { + return !(lhs == rhs); + } + +protected: + std::string + logStringImpl(char const* name) const + { + std::ostringstream ostr; + ostr << name << ": " + << "\nSrc: " << src_ << "\nDst: " << dst_; + return ostr.str(); + } + +private: + bool + equal(Step const& rhs) const override + { + if (auto ds = dynamic_cast(&rhs)) + { + return *this == *ds; + } + return false; + } +}; + +//------------------------------------------------------------------------------ + +// Flow is used in two different circumstances for transferring funds: +// o Payments, and +// o Offer crossing. +// The rules for handling funds in these two cases are almost, but not +// quite, the same. + +// Payment DirectStep class (not offer crossing). +class DirectMPTPaymentStep : public MPTEndpointStep +{ +public: + using MPTEndpointStep::MPTEndpointStep; + using MPTEndpointStep::check; + + bool + verifyPrevStepDebtDirection(DebtDirection) const + { + // A payment doesn't care regardless of prevStepRedeems. + return true; + } + + bool + verifyDstQualityIn(std::uint32_t dstQIn) const + { + // Payments have no particular expectations for what dstQIn will be. + return true; + } + + std::uint32_t + quality(ReadView const& sb, QualityDirection qDir) const; + + // Compute the maximum value that can flow from src->dst at + // the best available quality. + // return: first element is max amount that can flow, + // second is the debt direction w.r.t. the source account + std::pair + maxFlow(ReadView const& sb, MPTAmount const& desired) const; + + // Verify the consistency of the step. These checks are specific to + // payments and assume that general checks were already performed. + TER + check(StrandContext const& ctx, std::shared_ptr const& sleSrc) + const; + + std::string + logString() const override + { + return logStringImpl("DirectMPTPaymentStep"); + } +}; + +// Offer crossing DirectStep class (not a payment). +class DirectMPTOfferCrossingStep + : public MPTEndpointStep +{ +public: + using MPTEndpointStep::MPTEndpointStep; + using MPTEndpointStep::check; + + bool + verifyPrevStepDebtDirection(DebtDirection prevStepDir) const + { + // During offer crossing we rely on the fact that prevStepRedeems + // will *always* issue. That's because: + // o If there's a prevStep_, it will always be a BookStep. + // o BookStep::debtDirection() always returns `issues` when offer + // crossing. + // An assert based on this return value will tell us if that + // behavior changes. + return issues(prevStepDir); + } + + bool + verifyDstQualityIn(std::uint32_t dstQIn) const + { + // Due to a couple of factors dstQIn is always QUALITY_ONE for + // offer crossing. If that changes we need to know. + return dstQIn == QUALITY_ONE; + } + + std::uint32_t + quality(ReadView const& sb, QualityDirection qDir) const; + + // Compute the maximum value that can flow from src->dst at + // the best available quality. + // return: first element is max amount that can flow, + // second is the debt direction w.r.t the source + std::pair + maxFlow(ReadView const& sb, MPTAmount const& desired) const; + + // Verify the consistency of the step. These checks are specific to + // offer crossing and assume that general checks were already performed. + TER + check(StrandContext const& ctx, std::shared_ptr const& sleSrc) + const; + + std::string + logString() const override + { + return logStringImpl("DirectMPTOfferCrossingStep"); + } +}; + +//------------------------------------------------------------------------------ + +std::uint32_t +DirectMPTPaymentStep::quality(ReadView const& sb, QualityDirection qDir) const +{ + // MPT doesn't have Quality fields like trust line + return QUALITY_ONE; +} + +std::uint32_t +DirectMPTOfferCrossingStep::quality(ReadView const&, QualityDirection qDir) + const +{ + // MPT doesn't have Quality fields like trust line + 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 +{ + // Unlike IOU, MPT can't exceed the limit (MaximumAmount). + // See DirectIOfferCrossingStep::maxFlow(). + return maxPaymentFlow(sb); +} + +TER +DirectMPTPaymentStep::check( + StrandContext const& ctx, + std::shared_ptr const& sleSrc) const +{ + // Since this is a payment, MPToken must be present. Perform all + // MPToken related checks. + + // requireAuth checks if MPTIssuance exist. Note that issuer to issuer + // payment is invalid + auto const& issuer = mptIssue_.getIssuer(); + if (src_ != issuer) + { + if (auto const ter = requireAuth(ctx.view, mptIssue_, src_); + ter != tesSUCCESS) + return ter; + } + + if (dst_ != issuer) + { + if (auto const ter = requireAuth(ctx.view, mptIssue_, dst_); + ter != tesSUCCESS) + return ter; + } + + // Direct MPT payment, no DEX + if (mptIssue_ == ctx.strandDeliver && + (ctx.isFirst || (ctx.prevStep && !ctx.prevStep->bookStepBook()))) + { + // Between holders + if (isDirectBetweenHolders_) + { + auto const& holder = ctx.isFirst ? src_ : dst_; + // Payment between the holders + if (isFrozen(ctx.view, holder, mptIssue_)) + return tecLOCKED; + + if (auto const ter = + canTransfer(ctx.view, mptIssue_, holder, ctx.strandDst); + ter != tesSUCCESS) + return ter; + } + // Don't need to check if a payment is between issuer and holder + // in either direction + } + // Cross-token MPT payment via DEX + else + { + auto const account = ctx.isFirst ? src_ : dst_; + if (auto const ter = isMPTDEXAllowed( + ctx.view, mptIssue_, account, mptIssue_.getIssuer()); + ter != tesSUCCESS) + return ter; + } + + return tesSUCCESS; +} + +TER +DirectMPTOfferCrossingStep::check( + StrandContext const& ctx, + std::shared_ptr const&) const +{ + auto const& holder = ctx.isFirst ? src_ : dst_; + auto const& issuer = mptIssue_.getIssuer(); + if (holder != issuer) + { + if (auto const ter = + isMPTDEXAllowed(ctx.view, mptIssue_, holder, issuer); + ter != tesSUCCESS) + return ter; + } + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +template +std::pair +MPTEndpointStep::maxPaymentFlow(ReadView const& sb) const +{ + // From a non-issuer source to a destination + if (src_ != mptIssue_.getIssuer()) + return { + toAmount(accountHolds( + sb, src_, mptIssue_, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_)), + DebtDirection::redeems}; + + // From an issuer source to a destination + if (auto const sle = sb.read(keylet::mptIssuance(mptIssue_.getMptID()))) + { + std::uint64_t const maximumAmount = [&] { + auto const max = sle->getFieldU64(sfMaximumAmount); + return max > 0 ? max : maxMPTokenAmount; + }(); + std::int64_t const maxFlow = + maximumAmount - sle->getFieldU64(sfOutstandingAmount); + + // Direct issue, an issuer pays to a holder + if (!prevStep_) + return {MPTAmount{maxFlow}, DebtDirection::issues}; + + // Direct payment between holders or a cross currency payment. + // If maxFlow returned in this case then a valid payment will fail. + // Consider MaximumAmount is 100, alice is issuer. Then the last + // payment fails: pay(alice, bob, 100), pay(bob, carol, 100). + // Another use case with a cross-currency payment, which would + // also fail has maxFlow been returned. Last payment fails. + // MPT MaximumAmount is 1000, gw is issuer. pay(gw, alice, 1000), + // pay(gw, carol, 100), offer(alice, MPT1(10), MPT(10)), + // pay(carol, bob, MPT(10), sendmax(MPT1(10)), path(~MPT)). + // In case of a cross-currency payment, the previous step limits + // the output amount. + return {MPTAmount(maximumAmount), DebtDirection::issues}; + } + + return {MPTAmount{0}, DebtDirection::issues}; +} + +template +DebtDirection +MPTEndpointStep::debtDirection( + ReadView const& sb, + StrandDirection dir) const +{ + if (dir == StrandDirection::forward && cache_) + return cache_->srcDebtDir; + + return (src_ == mptIssue_.getIssuer()) ? DebtDirection::issues + : DebtDirection::redeems; +} + +template +std::pair +MPTEndpointStep::revImp( + PaymentSandbox& sb, + ApplyView& /*afView*/, + boost::container::flat_set& /*ofrsToRm*/, + MPTAmount const& out) +{ + cache_.reset(); + + auto const [maxSrcToDst, srcDebtDir] = + static_cast(this)->maxFlow(sb, out); + + auto const [srcQOut, dstQIn] = + qualities(sb, srcDebtDir, StrandDirection::reverse); + XRPL_ASSERT( + static_cast(this)->verifyDstQualityIn(dstQIn), + "MPTEndpointStep::revImp : verify dst quaity in"); + + MPTIssue const srcToDstIss(mptIssue_); + + JLOG(j_.trace()) << "MPTEndpointStep::rev" + << " srcRedeems: " << redeems(srcDebtDir) + << " outReq: " << to_string(out) + << " maxSrcToDst: " << to_string(maxSrcToDst) + << " srcQOut: " << srcQOut << " dstQIn: " << dstQIn; + + if (maxSrcToDst.signum() <= 0) + { + JLOG(j_.trace()) << "MPTEndpointStep::rev: dry"; + cache_.emplace( + MPTAmount(beast::zero), + MPTAmount(beast::zero), + MPTAmount(beast::zero), + srcDebtDir); + return {beast::zero, beast::zero}; + } + + MPTAmount const srcToDst = + mulRatio(out, QUALITY_ONE, dstQIn, /*roundUp*/ true); + + if (srcToDst <= maxSrcToDst) + { + MPTAmount const in = + mulRatio(srcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true); + cache_.emplace(in, srcToDst, srcToDst, srcDebtDir); + auto const ter = rippleCredit( + sb, + src_, + dst_, + toSTAmount(srcToDst, srcToDstIss), + /*checkIssuer*/ false, + j_); + (void)ter; + JLOG(j_.trace()) << "MPTEndpointStep::rev: Non-limiting" + << " srcRedeems: " << redeems(srcDebtDir) + << " in: " << to_string(in) + << " srcToDst: " << to_string(srcToDst) + << " out: " << to_string(out); + return {in, out}; + } + + // limiting node + MPTAmount const in = + mulRatio(maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true); + MPTAmount const actualOut = + mulRatio(maxSrcToDst, dstQIn, QUALITY_ONE, /*roundUp*/ false); + cache_.emplace(in, maxSrcToDst, actualOut, srcDebtDir); + + auto const ter = rippleCredit( + sb, + src_, + dst_, + toSTAmount(maxSrcToDst, srcToDstIss), + /*checkIssuer*/ false, + j_); + (void)ter; + JLOG(j_.trace()) << "MPTEndpointStep::rev: Limiting" + << " srcRedeems: " << redeems(srcDebtDir) + << " in: " << to_string(in) + << " srcToDst: " << to_string(maxSrcToDst) + << " out: " << to_string(out); + return {in, actualOut}; +} + +// The forward pass should never have more liquidity than the reverse +// pass. But sometimes rounding differences cause the forward pass to +// deliver more liquidity. Use the cached values from the reverse pass +// to prevent this. +template +void +MPTEndpointStep::setCacheLimiting( + MPTAmount const& fwdIn, + MPTAmount const& fwdSrcToDst, + MPTAmount const& fwdOut, + DebtDirection srcDebtDir) +{ + if (cache_->in < fwdIn) + { + MPTAmount const smallDiff(1); + auto const diff = fwdIn - cache_->in; + if (diff > smallDiff) + { + if (!cache_->in.value() || + (Number(fwdIn.value()) / Number(cache_->in.value())) > + Number(101, -2)) + { + // Detect large diffs on forward pass so they may be + // investigated + JLOG(j_.warn()) + << "MPTEndpointStep::fwd: setCacheLimiting" + << " fwdIn: " << to_string(fwdIn) + << " cacheIn: " << to_string(cache_->in) + << " fwdSrcToDst: " << to_string(fwdSrcToDst) + << " cacheSrcToDst: " << to_string(cache_->srcToDst) + << " fwdOut: " << to_string(fwdOut) + << " cacheOut: " << to_string(cache_->out); + cache_.emplace(fwdIn, fwdSrcToDst, fwdOut, srcDebtDir); + return; + } + } + } + cache_->in = fwdIn; + if (fwdSrcToDst < cache_->srcToDst) + cache_->srcToDst = fwdSrcToDst; + if (fwdOut < cache_->out) + cache_->out = fwdOut; + cache_->srcDebtDir = srcDebtDir; +}; + +template +std::pair +MPTEndpointStep::fwdImp( + PaymentSandbox& sb, + ApplyView& /*afView*/, + boost::container::flat_set& /*ofrsToRm*/, + MPTAmount const& in) +{ + XRPL_ASSERT(cache_, "MPTEndpointStep::fwdImp : valid cache"); + + auto const [maxSrcToDst, srcDebtDir] = + static_cast(this)->maxFlow(sb, cache_->srcToDst); + + auto const [srcQOut, dstQIn] = + qualities(sb, srcDebtDir, StrandDirection::forward); + + MPTIssue const srcToDstIss(mptIssue_); + + JLOG(j_.trace()) << "MPTEndpointStep::fwd" + << " srcRedeems: " << redeems(srcDebtDir) + << " inReq: " << to_string(in) + << " maxSrcToDst: " << to_string(maxSrcToDst) + << " srcQOut: " << srcQOut << " dstQIn: " << dstQIn; + + if (maxSrcToDst.signum() <= 0) + { + JLOG(j_.trace()) << "MPTEndpointStep::fwd: dry"; + cache_.emplace( + MPTAmount(beast::zero), + MPTAmount(beast::zero), + MPTAmount(beast::zero), + srcDebtDir); + return {beast::zero, beast::zero}; + } + + MPTAmount const srcToDst = + mulRatio(in, QUALITY_ONE, srcQOut, /*roundUp*/ false); + + if (srcToDst <= maxSrcToDst) + { + MPTAmount const out = + mulRatio(srcToDst, dstQIn, QUALITY_ONE, /*roundUp*/ false); + setCacheLimiting(in, srcToDst, out, srcDebtDir); + [[maybe_unused]] auto const ter = rippleCredit( + sb, + src_, + dst_, + toSTAmount(cache_->srcToDst, srcToDstIss), + /*checkIssuer*/ false, + j_); + JLOG(j_.trace()) << "MPTEndpointStep::fwd: Non-limiting" + << " srcRedeems: " << redeems(srcDebtDir) + << " in: " << to_string(in) + << " srcToDst: " << to_string(srcToDst) + << " out: " << to_string(out); + } + else + { + // limiting node + MPTAmount const actualIn = + mulRatio(maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true); + MPTAmount const out = + mulRatio(maxSrcToDst, dstQIn, QUALITY_ONE, /*roundUp*/ false); + setCacheLimiting(actualIn, maxSrcToDst, out, srcDebtDir); + auto const ter = rippleCredit( + sb, + src_, + dst_, + toSTAmount(cache_->srcToDst, srcToDstIss), + /*checkIssuer*/ false, + j_); + (void)ter; + JLOG(j_.trace()) << "MPTEndpointStep::rev: Limiting" + << " srcRedeems: " << redeems(srcDebtDir) + << " in: " << to_string(actualIn) + << " srcToDst: " << to_string(srcToDst) + << " out: " << to_string(out); + } + return {cache_->in, cache_->out}; +} + +template +std::pair +MPTEndpointStep::validFwd( + PaymentSandbox& sb, + ApplyView& afView, + EitherAmount const& in) +{ + if (!cache_) + { + JLOG(j_.trace()) << "Expected valid cache in validFwd"; + return {false, EitherAmount(MPTAmount(beast::zero))}; + } + + auto const savCache = *cache_; + + XRPL_ASSERT( + !in.native() && !in.isIOU(), + "MPTEndpoint::validFwd : not XRP or IOU"); + + auto const [maxSrcToDst, srcDebtDir] = + static_cast(this)->maxFlow(sb, cache_->srcToDst); + (void)srcDebtDir; + + try + { + boost::container::flat_set dummy; + fwdImp(sb, afView, dummy, in.mpt()); // changes cache + } + catch (FlowException const&) + { + return {false, EitherAmount(MPTAmount(beast::zero))}; + } + + if (maxSrcToDst < cache_->srcToDst) + { + JLOG(j_.warn()) << "MPTEndpointStep: Strand re-execute check failed." + << " Exceeded max src->dst limit" + << " max src->dst: " << to_string(maxSrcToDst) + << " actual src->dst: " << to_string(cache_->srcToDst); + return {false, EitherAmount(cache_->out)}; + } + + if (!(checkNear(savCache.in, cache_->in) && + checkNear(savCache.out, cache_->out))) + { + JLOG(j_.warn()) << "MPTEndpointStep: Strand re-execute check failed." + << " ExpectedIn: " << to_string(savCache.in) + << " CachedIn: " << to_string(cache_->in) + << " ExpectedOut: " << to_string(savCache.out) + << " CachedOut: " << to_string(cache_->out); + return {false, EitherAmount(cache_->out)}; + } + return {true, EitherAmount(cache_->out)}; +} + +// Returns srcQOut, dstQIn +template +std::pair +MPTEndpointStep::qualitiesSrcRedeems(ReadView const& sb) const +{ + if (!prevStep_) + return {QUALITY_ONE, QUALITY_ONE}; + + auto const prevStepQIn = prevStep_->lineQualityIn(sb); + auto srcQOut = + static_cast(this)->quality(sb, QualityDirection::out); + + if (prevStepQIn > srcQOut) + srcQOut = prevStepQIn; + return {srcQOut, QUALITY_ONE}; +} + +// Returns srcQOut, dstQIn +template +std::pair +MPTEndpointStep::qualitiesSrcIssues( + ReadView const& sb, + DebtDirection prevStepDebtDirection) const +{ + // Charge a transfer rate when issuing and previous step redeems + + XRPL_ASSERT( + static_cast(this)->verifyPrevStepDebtDirection( + prevStepDebtDirection), + "MPTEndpointStep::qualitiesSrcIssues : verify prev step debt " + "direction"); + + std::uint32_t const srcQOut = redeems(prevStepDebtDirection) + ? transferRate(sb, mptIssue_.getMptID()).value + : QUALITY_ONE; + auto dstQIn = + static_cast(this)->quality(sb, QualityDirection::in); + + if (isLast_ && dstQIn > QUALITY_ONE) + dstQIn = QUALITY_ONE; + return {srcQOut, dstQIn}; +} + +// Returns srcQOut, dstQIn +template +std::pair +MPTEndpointStep::qualities( + ReadView const& sb, + DebtDirection srcDebtDir, + StrandDirection strandDir) const +{ + if (redeems(srcDebtDir)) + { + return qualitiesSrcRedeems(sb); + } + else + { + auto const prevStepDebtDirection = [&] { + if (prevStep_) + return prevStep_->debtDirection(sb, strandDir); + return DebtDirection::issues; + }(); + return qualitiesSrcIssues(sb, prevStepDebtDirection); + } +} + +template +std::uint32_t +MPTEndpointStep::lineQualityIn(ReadView const& v) const +{ + // dst quality in + return static_cast(this)->quality(v, QualityDirection::in); +} + +template +std::pair, DebtDirection> +MPTEndpointStep::qualityUpperBound( + ReadView const& v, + DebtDirection prevStepDir) const +{ + auto const dir = this->debtDirection(v, StrandDirection::forward); + + if (!v.rules().enabled(fixQualityUpperBound)) + { + std::uint32_t const srcQOut = [&]() -> std::uint32_t { + if (redeems(prevStepDir) && issues(dir)) + return transferRate(v, mptIssue_.getMptID()).value; + return QUALITY_ONE; + }(); + auto dstQIn = static_cast(this)->quality( + v, QualityDirection::in); + + if (isLast_ && dstQIn > QUALITY_ONE) + dstQIn = QUALITY_ONE; + MPTIssue const iss{mptIssue_}; + return { + Quality(getRate(STAmount(iss, srcQOut), STAmount(iss, dstQIn))), + dir}; + } + + auto const [srcQOut, dstQIn] = redeems(dir) + ? qualitiesSrcRedeems(v) + : qualitiesSrcIssues(v, prevStepDir); + + MPTIssue const iss{mptIssue_}; + // Be careful not to switch the parameters to `getRate`. The + // `getRate(offerOut, offerIn)` function is usually used for offers. It + // returns offerIn/offerOut. For a direct step, the rate is srcQOut/dstQIn + // (Input*dstQIn/srcQOut = Output; So rate = srcQOut/dstQIn). Although the + // first parameter is called `offerOut`, it should take the `dstQIn` + // variable. + return { + Quality(getRate(STAmount(iss, dstQIn), STAmount(iss, srcQOut))), dir}; +} + +template +TER +MPTEndpointStep::check(StrandContext const& ctx) const +{ + // The following checks apply for both payments and offer crossing. + if (!src_ || !dst_) + { + JLOG(j_.debug()) << "MPTEndpointStep: specified bad account."; + return temBAD_PATH; + } + + if (src_ == dst_) + { + JLOG(j_.debug()) << "MPTEndpointStep: same src and dst."; + return temBAD_PATH; + } + + auto const sleSrc = ctx.view.read(keylet::account(src_)); + if (!sleSrc) + { + JLOG(j_.warn()) + << "MPTEndpointStep: can't receive MPT from non-existent issuer: " + << src_; + return terNO_ACCOUNT; + } + + // pure issue/redeem can't be frozen - can this happen? can only be an + // endpoint + if (!(ctx.isLast && ctx.isFirst)) + { + if (isFrozen(ctx.view, src_, mptIssue_) || + isFrozen(ctx.view, dst_, mptIssue_)) + return tecLOCKED; + } + + if (ctx.seenBookOuts.count(mptIssue_)) + { + if (!ctx.prevStep) + { + UNREACHABLE( + "ripple::MPTEndpointStep::check : prev seen book without a " + "prev step"); + return temBAD_PATH_LOOP; + } + + // This is OK if the previous step is a book step that outputs this + // issue + if (auto book = ctx.prevStep->bookStepBook()) + { + if (book->out.get() != mptIssue_) + return temBAD_PATH_LOOP; + } + } + + if ((ctx.isFirst && !ctx.seenDirectAssets[0].insert(mptIssue_).second) || + (ctx.isLast && !ctx.seenDirectAssets[1].insert(mptIssue_).second)) + { + JLOG(j_.debug()) << "DirectStepI: loop detected: Index: " + << ctx.strandSize << ' ' << *this; + return temBAD_PATH_LOOP; + } + + // MPT can only be an endpoint + if (!ctx.isLast && !ctx.isFirst) + { + JLOG(j_.warn()) << "MPTEndpointStep: MPT can only be an endpoint"; + return terNO_RIPPLE; + } + + return static_cast(this)->check(ctx, sleSrc); +} + +//------------------------------------------------------------------------------ + +std::pair> +make_MPTEndpointStep( + StrandContext const& ctx, + AccountID const& src, + AccountID const& dst, + MPTID const& mpt) +{ + TER ter = tefINTERNAL; + std::unique_ptr r; + if (ctx.offerCrossing) + { + auto offerCrossingStep = + std::make_unique(ctx, src, dst, mpt); + ter = offerCrossingStep->check(ctx); + r = std::move(offerCrossingStep); + } + else // payment + { + auto paymentStep = + std::make_unique(ctx, src, dst, mpt); + ter = paymentStep->check(ctx); + r = std::move(paymentStep); + } + if (ter != tesSUCCESS) + return {ter, nullptr}; + + return {tesSUCCESS, std::move(r)}; +} + +} // namespace ripple diff --git a/src/xrpld/app/paths/detail/PathfinderUtils.h b/src/xrpld/app/paths/detail/PathfinderUtils.h index b06dded75bd..5010555868e 100644 --- a/src/xrpld/app/paths/detail/PathfinderUtils.h +++ b/src/xrpld/app/paths/detail/PathfinderUtils.h @@ -30,7 +30,9 @@ largestAmount(STAmount const& amt) if (amt.native()) return INITIAL_XRP; - return STAmount(amt.issue(), STAmount::cMaxValue, STAmount::cMaxOffset); + if (amt.holds()) + return STAmount(amt.asset(), STAmount::cMaxValue, STAmount::cMaxOffset); + return STAmount(amt.asset(), maxMPTokenAmount, 0); } inline STAmount diff --git a/src/xrpld/app/paths/detail/PaySteps.cpp b/src/xrpld/app/paths/detail/PaySteps.cpp index b73b1ac8acc..4964d5a677d 100644 --- a/src/xrpld/app/paths/detail/PaySteps.cpp +++ b/src/xrpld/app/paths/detail/PaySteps.cpp @@ -57,12 +57,6 @@ checkNear(IOUAmount const& expected, IOUAmount const& actual) return r <= ratTol; }; -bool -checkNear(XRPAmount const& expected, XRPAmount const& actual) -{ - return expected == actual; -}; - static bool isXRPAccount(STPathElement const& pe) { @@ -76,13 +70,13 @@ toStep( StrandContext const& ctx, STPathElement const* e1, STPathElement const* e2, - Issue const& curIssue) + Asset const& curAsset) { auto& j = ctx.j; if (ctx.isFirst && e1->isAccount() && (e1->getNodeType() & STPathElement::typeCurrency) && - isXRP(e1->getCurrency())) + e1->getPathAsset().isXRP()) { return make_XRPEndpointStep(ctx, e1->getAccountID()); } @@ -90,10 +84,34 @@ toStep( if (ctx.isLast && isXRPAccount(*e1) && e2->isAccount()) return make_XRPEndpointStep(ctx, e2->getAccountID()); + // MPTEndpointStep is created in following cases: + // 1 Direct payment between an issuer and a holder + // e1 is issuer and e2 is holder or vise versa + // There is only one step in this case: holder->issuer or + // issuer->holder + // 2 Direct payment between the holders + // e1 is issuer and e2 is holder or vise versa + // There are two steps in this case: holder->issuer->holder1 + // 3 Cross-token payment with Amount or SendMax or both MPT + // If destination is an issuer then the last step is BookStep, + // otherwise the last step is MPTEndpointStep where e1 is + // the issuer and e2 is the holder. + // In all cases MPTEndpointStep is always first or last step, + // e1/e2 are always account types, and curAsset is always MPT. + if (e1->isAccount() && e2->isAccount()) { + if (curAsset.holds()) + return make_MPTEndpointStep( + ctx, + e1->getAccountID(), + e2->getAccountID(), + curAsset.get().getMptID()); return make_DirectStepI( - ctx, e1->getAccountID(), e2->getAccountID(), curIssue.currency); + ctx, + e1->getAccountID(), + e2->getAccountID(), + curAsset.get().currency); } if (e1->isOffer() && e2->isAccount()) @@ -106,17 +124,17 @@ toStep( } XRPL_ASSERT( - (e2->getNodeType() & STPathElement::typeCurrency) || + (e2->getNodeType() & STPathElement::typeAsset) || (e2->getNodeType() & STPathElement::typeIssuer), "ripple::toStep : currency or issuer"); - auto const outCurrency = e2->getNodeType() & STPathElement::typeCurrency - ? e2->getCurrency() - : curIssue.currency; + auto const outAsset = e2->getNodeType() & STPathElement::typeAsset + ? e2->getPathAsset() + : curAsset; auto const outIssuer = e2->getNodeType() & STPathElement::typeIssuer ? e2->getIssuerID() - : curIssue.account; + : curAsset.getIssuer(); - if (isXRP(curIssue.currency) && isXRP(outCurrency)) + if (isXRP(curAsset) && outAsset.isXRP()) { JLOG(j.info()) << "Found xrp/xrp offer payment step"; return {temBAD_PATH, std::unique_ptr{}}; @@ -124,13 +142,34 @@ toStep( XRPL_ASSERT(e2->isOffer(), "ripple::toStep : is offer"); - if (isXRP(outCurrency)) - return make_BookStepIX(ctx, curIssue); + if (outAsset.isXRP()) + { + if (curAsset.holds()) + return make_BookStepMX(ctx, curAsset.get()); + return make_BookStepIX(ctx, curAsset.get()); + } - if (isXRP(curIssue.currency)) - return make_BookStepXI(ctx, {outCurrency, outIssuer}); + if (isXRP(curAsset)) + { + if (outAsset.holds()) + return make_BookStepXM(ctx, outAsset.get()); + return make_BookStepXI(ctx, {outAsset.get(), outIssuer}); + } - return make_BookStepII(ctx, curIssue, {outCurrency, outIssuer}); + if (curAsset.holds() && outAsset.holds()) + return make_BookStepMI( + ctx, + curAsset.get(), + {outAsset.get(), outIssuer}); + if (curAsset.holds() && outAsset.holds()) + return make_BookStepIM( + ctx, curAsset.get(), outAsset.get()); + + if (curAsset.holds()) + return make_BookStepMM( + ctx, curAsset.get(), outAsset.get()); + return make_BookStepII( + ctx, curAsset.get(), {outAsset.get(), outIssuer}); } std::pair @@ -138,9 +177,9 @@ toStrand( ReadView const& view, AccountID const& src, AccountID const& dst, - Issue const& deliver, + Asset const& deliver, std::optional const& limitQuality, - std::optional const& sendMaxIssue, + std::optional const& sendMaxAsset, STPath const& path, bool ownerPaysTransferFee, OfferCrossing offerCrossing, @@ -148,16 +187,22 @@ toStrand( beast::Journal j) { if (isXRP(src) || isXRP(dst) || !isConsistent(deliver) || - (sendMaxIssue && !isConsistent(*sendMaxIssue))) + (sendMaxAsset && !isConsistent(*sendMaxAsset))) return {temBAD_PATH, Strand{}}; - if ((sendMaxIssue && sendMaxIssue->account == noAccount()) || + if ((sendMaxAsset && sendMaxAsset->getIssuer() == noAccount()) || (src == noAccount()) || (dst == noAccount()) || - (deliver.account == noAccount())) + (deliver.getIssuer() == noAccount())) + return {temBAD_PATH, Strand{}}; + + if ((deliver.holds() && deliver.getIssuer() == beast::zero) || + (sendMaxAsset && sendMaxAsset->holds() && + sendMaxAsset->getIssuer() == beast::zero)) return {temBAD_PATH, Strand{}}; - for (auto const& pe : path) + for (std::size_t i = 0; i < path.size(); ++i) { + auto const& pe = path[i]; auto const t = pe.getNodeType(); if ((t & ~STPathElement::typeAll) || !t) @@ -166,6 +211,8 @@ toStrand( bool const hasAccount = t & STPathElement::typeAccount; bool const hasIssuer = t & STPathElement::typeIssuer; bool const hasCurrency = t & STPathElement::typeCurrency; + bool const hasMPT = t & STPathElement::typeMPT; + bool const hasAsset = t & STPathElement::typeAsset; if (hasAccount && (hasIssuer || hasCurrency)) return {temBAD_PATH, Strand{}}; @@ -185,18 +232,33 @@ toStrand( if (hasAccount && (pe.getAccountID() == noAccount())) return {temBAD_PATH, Strand{}}; + + if (hasMPT && (hasCurrency || hasAccount)) + return {temBAD_PATH, Strand{}}; + + if (hasMPT && hasIssuer && + (pe.getIssuerID() != getMPTIssuer(pe.getMPTID()))) + return {temBAD_PATH, Strand{}}; + + // No rippling if MPT + if (i > 0 && path[i - 1].hasMPT() && + (hasAccount || (hasIssuer && !hasAsset))) + return {temBAD_PATH, Strand{}}; } - Issue curIssue = [&] { - auto const& currency = - sendMaxIssue ? sendMaxIssue->currency : deliver.currency; - if (isXRP(currency)) + Asset curAsset = [&]() -> Asset { + auto const& asset = sendMaxAsset ? *sendMaxAsset : deliver; + if (isXRP(asset)) return xrpIssue(); - return Issue{currency, src}; + if (asset.holds()) + return asset; + // First step ripples from the source to the issuer. + return Issue{asset.get().currency, src}; }(); - auto hasCurrency = [](STPathElement const pe) { - return pe.getNodeType() & STPathElement::typeCurrency; + // Currency or MPT + auto hasAsset = [](STPathElement const pe) { + return pe.getNodeType() & STPathElement::typeAsset; }; std::vector normPath; @@ -204,15 +266,30 @@ toStrand( // sendmax and deliver. normPath.reserve(4 + path.size()); { - normPath.emplace_back( - STPathElement::typeAll, src, curIssue.currency, curIssue.account); - - if (sendMaxIssue && sendMaxIssue->account != src && + // The first step of a path is always implied to be the sender of the + // transaction, as defined by the transaction's Account field. The Asset + // is either SendMax or Deliver. + auto const t = [&]() { + auto const t = + STPathElement::typeAccount | STPathElement::typeIssuer; + if (curAsset.holds()) + return t | STPathElement::typeMPT; + return t | STPathElement::typeCurrency; + }(); + // If MPT then the issuer is the actual issuer, it is never the source + // account. + normPath.emplace_back(t, src, curAsset, curAsset.getIssuer()); + + // If transaction includes SendMax with the issuer, which is not + // the sender of the transaction, that issuer is implied to be + // the second step of the path. Unless the path starts at an address, + // which is the issuer of SendMax. + if (sendMaxAsset && sendMaxAsset->getIssuer() != src && (path.empty() || !path[0].isAccount() || - path[0].getAccountID() != sendMaxIssue->account)) + path[0].getAccountID() != sendMaxAsset->getIssuer())) { normPath.emplace_back( - sendMaxIssue->account, std::nullopt, std::nullopt); + sendMaxAsset->getIssuer(), std::nullopt, std::nullopt); } for (auto const& i : path) @@ -220,25 +297,34 @@ toStrand( { // Note that for offer crossing (only) we do use an offer book - // even if all that is changing is the Issue.account. - STPathElement const& lastCurrency = - *std::find_if(normPath.rbegin(), normPath.rend(), hasCurrency); - if ((lastCurrency.getCurrency() != deliver.currency) || + // even if all that is changing is the Issue.account. Note + // that MPTIssue can't change the account. + STPathElement const& lastAsset = + *std::find_if(normPath.rbegin(), normPath.rend(), hasAsset); + if (lastAsset.getPathAsset() != deliver || (offerCrossing && - lastCurrency.getIssuerID() != deliver.account)) + lastAsset.getIssuerID() != deliver.getIssuer())) { normPath.emplace_back( - std::nullopt, deliver.currency, deliver.account); + std::nullopt, deliver, deliver.getIssuer()); } } + // If the Amount field of the transaction includes an issuer that is not + // the same as the Destination of the transaction, that issuer is + // implied to be the second-to-last step of the path. If normPath.back + // is an offer, which sells MPT then the added path element account is + // the MPT's issuer. if (!((normPath.back().isAccount() && - normPath.back().getAccountID() == deliver.account) || - (dst == deliver.account))) + normPath.back().getAccountID() == deliver.getIssuer()) || + (dst == deliver.getIssuer()))) { - normPath.emplace_back(deliver.account, std::nullopt, std::nullopt); + normPath.emplace_back( + deliver.getIssuer(), std::nullopt, std::nullopt); } + // Last step of a path is always implied to be the receiver of a + // transaction, as defined by the transaction's Destination field. if (!normPath.back().isAccount() || normPath.back().getAccountID() != dst) { @@ -261,11 +347,11 @@ toStrand( at most twice: once as a src and once as a dst (hence the two element array). The strandSrc and strandDst will only show up once each. */ - std::array, 2> seenDirectIssues; + std::array, 2> seenDirectAssets; // A strand may not include the same offer book more than once - boost::container::flat_set seenBookOuts; - seenDirectIssues[0].reserve(normPath.size()); - seenDirectIssues[1].reserve(normPath.size()); + boost::container::flat_set seenBookOuts; + seenDirectAssets[0].reserve(normPath.size()); + seenDirectAssets[1].reserve(normPath.size()); seenBookOuts.reserve(normPath.size()); auto ctx = [&](bool isLast = false) { return StrandContext{ @@ -279,7 +365,7 @@ toStrand( ownerPaysTransferFee, offerCrossing, isDefaultPath, - seenDirectIssues, + seenDirectAssets, seenBookOuts, ammContext, j}; @@ -298,36 +384,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_) -> std::pair> { + if (asset_.holds()) + { + JLOG(j.error()) << "MPT is invalid with rippling"; + return {temBAD_PATH, nullptr}; + } + return make_DirectStepI( + ctx(), src_, dst_, asset_.get().currency); + }; if (cur->isAccount() && next->isAccount()) { - if (!isXRP(curIssue.currency) && - curIssue.account != cur->getAccountID() && - curIssue.account != next->getAccountID()) + // This block doesn't execute + // 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/amendment excluded? + if (!isXRP(curAsset) && + curAsset.getIssuer() != cur->getAccountID() && + curAsset.getIssuer() != next->getAccountID()) { JLOG(j.trace()) << "Inserting implied account"; - auto msr = make_DirectStepI( - ctx(), - cur->getAccountID(), - curIssue.account, - curIssue.currency); + auto msr = getImpliedStep( + cur->getAccountID(), curAsset.getIssuer(), curAsset); if (msr.first != tesSUCCESS) return {msr.first, Strand{}}; result.push_back(std::move(msr.second)); impliedPE.emplace( STPathElement::typeAccount, - curIssue.account, + curAsset.getIssuer(), xrpCurrency(), xrpAccount()); cur = &*impliedPE; @@ -335,20 +446,18 @@ toStrand( } else if (cur->isAccount() && next->isOffer()) { - if (curIssue.account != cur->getAccountID()) + // Same as above, this block doesn't execute. + if (curAsset.getIssuer() != cur->getAccountID()) { JLOG(j.trace()) << "Inserting implied account before offer"; - auto msr = make_DirectStepI( - ctx(), - cur->getAccountID(), - curIssue.account, - curIssue.currency); + auto msr = getImpliedStep( + cur->getAccountID(), curAsset.getIssuer(), curAsset); if (msr.first != tesSUCCESS) return {msr.first, Strand{}}; result.push_back(std::move(msr.second)); impliedPE.emplace( STPathElement::typeAccount, - curIssue.account, + curAsset.getIssuer(), xrpCurrency(), xrpAccount()); cur = &*impliedPE; @@ -356,10 +465,13 @@ toStrand( } else if (cur->isOffer() && next->isAccount()) { - if (curIssue.account != next->getAccountID() && + // If the offer sells MPT, then next's account is always the issuer. + // See how normPath step is added for second-to-last or last + // step. Therefore, this block never executes if MPT. + if (curAsset.getIssuer() != next->getAccountID() && !isXRP(next->getAccountID())) { - if (isXRP(curIssue)) + if (isXRP(curAsset)) { if (i != normPath.size() - 2) return {temBAD_PATH, Strand{}}; @@ -376,11 +488,8 @@ toStrand( else { JLOG(j.trace()) << "Inserting implied account after offer"; - auto msr = make_DirectStepI( - ctx(), - curIssue.account, - next->getAccountID(), - curIssue.currency); + auto msr = getImpliedStep( + curAsset.getIssuer(), next->getAccountID(), curAsset); if (msr.first != tesSUCCESS) return {msr.first, Strand{}}; result.push_back(std::move(msr.second)); @@ -389,8 +498,8 @@ toStrand( continue; } - if (!next->isOffer() && next->hasCurrency() && - next->getCurrency() != curIssue.currency) + if (!next->isOffer() && next->hasAsset() && + next->getPathAsset() != curAsset) { // Should never happen UNREACHABLE("ripple::toStrand : offer currency mismatch"); @@ -398,7 +507,7 @@ toStrand( } auto s = toStep( - ctx(/*isLast*/ i == normPath.size() - 2), cur, next, curIssue); + ctx(/*isLast*/ i == normPath.size() - 2), cur, next, curAsset); if (s.first == tesSUCCESS) result.emplace_back(std::move(s.second)); else @@ -413,19 +522,20 @@ toStrand( if (auto r = s.directStepAccts()) return *r; if (auto const r = s.bookStepBook()) - return std::make_pair(r->in.account, r->out.account); + return std::make_pair(r->in.getIssuer(), r->out.getIssuer()); Throw( tefEXCEPTION, "Step should be either a direct or book step"); return std::make_pair(xrpAccount(), xrpAccount()); }; auto curAcc = src; - auto curIss = [&] { - auto& currency = - sendMaxIssue ? sendMaxIssue->currency : deliver.currency; - if (isXRP(currency)) + auto curAsset = [&]() -> Asset { + auto const& asset = sendMaxAsset ? *sendMaxAsset : deliver; + if (isXRP(asset)) return xrpIssue(); - return Issue{currency, src}; + if (asset.holds()) + return asset; + return Issue{asset.get().currency, src}; }(); for (auto const& s : result) @@ -436,22 +546,27 @@ toStrand( if (auto const b = s->bookStepBook()) { - if (curIss != b->in) + if (curAsset != b->in) return false; - curIss = b->out; + curAsset = b->out; } - else + else if (curAsset.holds()) { - curIss.account = accts.second; + curAsset.get().account = accts.second; } curAcc = accts.second; } if (curAcc != dst) return false; - if (curIss.currency != deliver.currency) + if (curAsset.holds() != deliver.holds() || + (curAsset.holds() && + curAsset.get().currency != deliver.get().currency) || + (curAsset.holds() && + curAsset.get() != deliver.get())) return false; - if (curIss.account != deliver.account && curIss.account != dst) + if (curAsset.getIssuer() != deliver.getIssuer() && + curAsset.getIssuer() != dst) return false; return true; }; @@ -471,9 +586,9 @@ toStrands( ReadView const& view, AccountID const& src, AccountID const& dst, - Issue const& deliver, + Asset const& deliver, std::optional const& limitQuality, - std::optional const& sendMax, + std::optional const& sendMax, STPathSet const& paths, bool addDefaultPath, bool ownerPaysTransferFee, @@ -586,14 +701,14 @@ StrandContext::StrandContext( // replicates the source or destination. AccountID const& strandSrc_, AccountID const& strandDst_, - Issue const& strandDeliver_, + Asset const& strandDeliver_, std::optional const& limitQuality_, bool isLast_, bool ownerPaysTransferFee_, OfferCrossing offerCrossing_, bool isDefaultPath_, - std::array, 2>& seenDirectIssues_, - boost::container::flat_set& seenBookOuts_, + std::array, 2>& seenDirectAssets_, + boost::container::flat_set& seenBookOuts_, AMMContext& ammContext_, beast::Journal j_) : view(view_) @@ -608,7 +723,7 @@ StrandContext::StrandContext( , isDefaultPath(isDefaultPath_) , strandSize(strand_.size()) , prevStep(!strand_.empty() ? strand_.back().get() : nullptr) - , seenDirectIssues(seenDirectIssues_) + , seenDirectAssets(seenDirectAssets_) , seenBookOuts(seenBookOuts_) , ammContext(ammContext_) , j(j_) @@ -635,5 +750,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/StepChecks.h b/src/xrpld/app/paths/detail/StepChecks.h index d4fda2bfe62..78131a7a7be 100644 --- a/src/xrpld/app/paths/detail/StepChecks.h +++ b/src/xrpld/app/paths/detail/StepChecks.h @@ -71,10 +71,7 @@ checkFreeze( return tecINTERNAL; // LCOV_EXCL_LINE if (isLPTokenFrozen( - view, - src, - (*sleAmm)[sfAsset].get(), - (*sleAmm)[sfAsset2].get())) + view, src, (*sleAmm)[sfAsset], (*sleAmm)[sfAsset2])) { return terNO_LINE; } diff --git a/src/xrpld/app/paths/detail/Steps.h b/src/xrpld/app/paths/detail/Steps.h index dee90f617a5..dd0524688d3 100644 --- a/src/xrpld/app/paths/detail/Steps.h +++ b/src/xrpld/app/paths/detail/Steps.h @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -31,6 +32,7 @@ #include namespace ripple { + class PaymentSandbox; class ReadView; class ApplyView; @@ -362,8 +364,8 @@ std::pair normalizePath( AccountID const& src, AccountID const& dst, - Issue const& deliver, - std::optional const& sendMaxIssue, + Asset const& deliver, + std::optional const& sendMaxAsset, STPath const& path); /** @@ -378,7 +380,7 @@ normalizePath( optimization. If, during direct offer crossing, the quality of the tip of the book drops below this value, then evaluating the strand can stop. - @param sendMaxIssue Optional asset to send. + @param sendMaxAsset Optional asset to send. @param path Liquidity sources to use for this strand of the payment. The path contains an ordered collection of the offer books to use and accounts to ripple through. @@ -394,9 +396,9 @@ toStrand( ReadView const& sb, AccountID const& src, AccountID const& dst, - Issue const& deliver, + Asset const& deliver, std::optional const& limitQuality, - std::optional const& sendMaxIssue, + std::optional const& sendMaxAsset, STPath const& path, bool ownerPaysTransferFee, OfferCrossing offerCrossing, @@ -433,9 +435,9 @@ toStrands( ReadView const& sb, AccountID const& src, AccountID const& dst, - Issue const& deliver, + Asset const& deliver, std::optional const& limitQuality, - std::optional const& sendMax, + std::optional const& sendMax, STPathSet const& paths, bool addDefaultPath, bool ownerPaysTransferFee, @@ -444,7 +446,7 @@ toStrands( beast::Journal j); /// @cond INTERNAL -template +template struct StepImp : public Step { explicit StepImp() = default; @@ -515,8 +517,13 @@ class FlowException : public std::runtime_error // Check equal with tolerance bool checkNear(IOUAmount const& expected, IOUAmount const& actual); +template + requires(std::is_same_v || std::is_same_v) bool -checkNear(XRPAmount const& expected, XRPAmount const& actual); +checkNear(T const& expected, T const& actual) +{ + return expected == actual; +} /// @endcond /** @@ -527,7 +534,7 @@ struct StrandContext ReadView const& view; ///< Current ReadView AccountID const strandSrc; ///< Strand source account AccountID const strandDst; ///< Strand destination account - Issue const strandDeliver; ///< Issue strand delivers + Asset const strandDeliver; ///< Asset strand delivers std::optional const limitQuality; ///< Worst accepted quality bool const isFirst; ///< true if Step is first in Strand bool const isLast = false; ///< true if Step is last in Strand @@ -545,11 +552,11 @@ struct StrandContext at most twice: once as a src and once as a dst (hence the two element array). The strandSrc and strandDst will only show up once each. */ - std::array, 2>& seenDirectIssues; + std::array, 2>& seenDirectAssets; /** A strand may not include an offer that output the same issue more than once */ - boost::container::flat_set& seenBookOuts; + boost::container::flat_set& seenBookOuts; AMMContext& ammContext; beast::Journal const j; @@ -561,15 +568,15 @@ struct StrandContext // replicates the source or destination. AccountID const& strandSrc_, AccountID const& strandDst_, - Issue const& strandDeliver_, + Asset const& strandDeliver_, std::optional const& limitQuality_, bool isLast_, bool ownerPaysTransferFee_, OfferCrossing offerCrossing_, bool isDefaultPath_, - std::array, 2>& - seenDirectIssues_, ///< For detecting currency loops - boost::container::flat_set& + std::array, 2>& + seenDirectAssets_, ///< For detecting currency loops + boost::container::flat_set& seenBookOuts_, ///< For detecting book loops AMMContext& ammContext_, beast::Journal j_); ///< Journal for logging @@ -599,6 +606,13 @@ make_DirectStepI( AccountID const& dst, Currency const& c); +std::pair> +make_MPTEndpointStep( + StrandContext const& ctx, + AccountID const& src, + AccountID const& dst, + MPTID const& a); + std::pair> make_BookStepII(StrandContext const& ctx, Issue const& in, Issue const& out); @@ -611,6 +625,24 @@ make_BookStepXI(StrandContext const& ctx, Issue const& out); std::pair> make_XRPEndpointStep(StrandContext const& ctx, AccountID const& acc); +std::pair> +make_BookStepMM( + StrandContext const& ctx, + MPTIssue const& in, + MPTIssue const& out); + +std::pair> +make_BookStepMX(StrandContext const& ctx, MPTIssue const& in); + +std::pair> +make_BookStepXM(StrandContext const& ctx, MPTIssue const& out); + +std::pair> +make_BookStepMI(StrandContext const& ctx, MPTIssue const& in, Issue const& out); + +std::pair> +make_BookStepIM(StrandContext const& ctx, Issue const& in, MPTIssue const& out); + template bool isDirectXrpToXrp(Strand const& strand); diff --git a/src/xrpld/app/paths/detail/StrandFlow.h b/src/xrpld/app/paths/detail/StrandFlow.h index 0e168b73cce..9cca99977dd 100644 --- a/src/xrpld/app/paths/detail/StrandFlow.h +++ b/src/xrpld/app/paths/detail/StrandFlow.h @@ -403,9 +403,11 @@ limitOut( return XRPAmount{*out}; else if constexpr (std::is_same_v) return IOUAmount{*out}; + else if constexpr (std::is_same_v) + return MPTAmount{*out}; else return STAmount{ - remainingOut.issue(), out->mantissa(), out->exponent()}; + remainingOut.asset(), out->mantissa(), out->exponent()}; }(); // A tiny difference could be due to the round off if (withinRelativeDistance(out, remainingOut, Number(1, -9))) @@ -557,7 +559,7 @@ class ActiveStrands @return Actual amount in and out from the strands, errors, and payment sandbox */ -template +template FlowResult flow( PaymentSandbox const& baseView, diff --git a/src/xrpld/app/paths/detail/XRPEndpointStep.cpp b/src/xrpld/app/paths/detail/XRPEndpointStep.cpp index ab211a7c856..80220d2e1dd 100644 --- a/src/xrpld/app/paths/detail/XRPEndpointStep.cpp +++ b/src/xrpld/app/paths/detail/XRPEndpointStep.cpp @@ -201,7 +201,8 @@ class XRPEndpointOfferCrossingStep static std::int32_t computeReserveReduction(StrandContext const& ctx, AccountID const& acc) { - if (ctx.isFirst && !ctx.view.read(keylet::line(acc, ctx.strandDeliver))) + if (ctx.isFirst && ctx.strandDeliver.holds() && + !ctx.view.read(keylet::line(acc, ctx.strandDeliver.get()))) return -1; return 0; } @@ -309,9 +310,10 @@ XRPEndpointStep::validFwd( return {false, EitherAmount(XRPAmount(beast::zero))}; } - XRPL_ASSERT(in.native, "ripple::XRPEndpointStep::validFwd : input is XRP"); + XRPL_ASSERT( + in.native(), "ripple::XRPEndpointStep::validFwd : input is XRP"); - auto const& xrpIn = in.xrp; + auto const& xrpIn = in.xrp(); auto const balance = static_cast(this)->xrpLiquid(sb); if (!isLast_ && balance < xrpIn) @@ -364,7 +366,7 @@ XRPEndpointStep::check(StrandContext const& ctx) const if (ctx.view.rules().enabled(fix1781)) { auto const issuesIndex = isLast_ ? 0 : 1; - if (!ctx.seenDirectIssues[issuesIndex].insert(xrpIssue()).second) + if (!ctx.seenDirectAssets[issuesIndex].insert(xrpIssue()).second) { JLOG(j_.debug()) << "XRPEndpointStep: loop detected: Index: " << ctx.strandSize diff --git a/src/xrpld/app/tx/detail/AMMBid.cpp b/src/xrpld/app/tx/detail/AMMBid.cpp index e8a14c14922..5bd15b0ce5a 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; @@ -124,7 +128,7 @@ AMMBid::preclaim(PreclaimContext const& ctx) if (bidMin) { - if (bidMin->issue() != lpTokens.issue()) + if (bidMin->asset() != lpTokens.asset()) { JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken."; return temBAD_AMM_TOKENS; @@ -139,7 +143,7 @@ AMMBid::preclaim(PreclaimContext const& ctx) auto const bidMax = ctx.tx[~sfBidMax]; if (bidMax) { - if (bidMax->issue() != lpTokens.issue()) + if (bidMax->asset() != lpTokens.asset()) { JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken."; return temBAD_AMM_TOKENS; @@ -225,7 +229,7 @@ applyBid( else if (auctionSlot.isFieldPresent(sfDiscountedFee)) auctionSlot.makeFieldAbsent(sfDiscountedFee); auctionSlot.setFieldAmount( - sfPrice, toSTAmount(lpTokens.issue(), minPrice)); + sfPrice, toSTAmount(lpTokens.asset(), minPrice)); if (ctx_.tx.isFieldPresent(sfAuthAccounts)) auctionSlot.setFieldArray( sfAuthAccounts, ctx_.tx.getFieldArray(sfAuthAccounts)); @@ -233,7 +237,7 @@ applyBid( auctionSlot.makeFieldAbsent(sfAuthAccounts); // Burn the remaining bid amount auto const saBurn = adjustLPTokens( - lptAMMBalance, toSTAmount(lptAMMBalance.issue(), burn), false); + lptAMMBalance, toSTAmount(lptAMMBalance.asset(), burn), false); if (saBurn >= lptAMMBalance) { // This error case should never occur. @@ -242,8 +246,8 @@ applyBid( << lptAMMBalance; return tecINTERNAL; } - auto res = - redeemIOU(sb, account_, saBurn, lpTokens.issue(), ctx_.journal); + auto res = redeemIOU( + sb, account_, saBurn, lpTokens.get(), ctx_.journal); if (res != tesSUCCESS) { JLOG(ctx_.journal.debug()) << "AMM Bid: failed to redeem."; @@ -340,7 +344,7 @@ applyBid( sb, account_, auctionSlot[sfAccount], - toSTAmount(lpTokens.issue(), refund), + toSTAmount(lpTokens.asset(), refund), ctx_.journal); if (res != tesSUCCESS) { diff --git a/src/xrpld/app/tx/detail/AMMClawback.cpp b/src/xrpld/app/tx/detail/AMMClawback.cpp index 162224ff913..6f348dd4073 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.cpp +++ b/src/xrpld/app/tx/detail/AMMClawback.cpp @@ -39,6 +39,15 @@ AMMClawback::preflight(PreflightContext const& ctx) if (!ctx.rules.enabled(featureAMMClawback)) return temDISABLED; + std::optional const clawAmount = ctx.tx[~sfAmount]; + auto const asset = ctx.tx[sfAsset]; + auto const asset2 = ctx.tx[sfAsset2]; + + if (!ctx.rules.enabled(featureMPTokensV2) && + ((clawAmount && clawAmount->holds()) || + asset.holds() || asset2.holds())) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; // LCOV_EXCL_LINE @@ -56,14 +65,10 @@ AMMClawback::preflight(PreflightContext const& ctx) return temMALFORMED; } - std::optional const clawAmount = ctx.tx[~sfAmount]; - auto const asset = ctx.tx[sfAsset].get(); - auto const asset2 = ctx.tx[sfAsset2].get(); - if (isXRP(asset)) return temMALFORMED; - if (flags & tfClawTwoAssets && asset.account != asset2.account) + if (flags & tfClawTwoAssets && asset.getIssuer() != asset2.getIssuer()) { JLOG(ctx.j.trace()) << "AMMClawback: tfClawTwoAssets can only be enabled when two " @@ -71,16 +76,16 @@ AMMClawback::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - if (asset.account != issuer) + if (asset.getIssuer() != issuer) { JLOG(ctx.j.trace()) << "AMMClawback: Asset's account does not " "match Account field."; return temMALFORMED; } - if (clawAmount && clawAmount->get() != asset) + if (clawAmount && clawAmount->asset() != asset) { - JLOG(ctx.j.trace()) << "AMMClawback: Amount's issuer/currency subfield " + JLOG(ctx.j.trace()) << "AMMClawback: Amount's asset subfield " "does not match Asset field"; return temBAD_AMOUNT; } @@ -94,8 +99,8 @@ AMMClawback::preflight(PreflightContext const& ctx) TER AMMClawback::preclaim(PreclaimContext const& ctx) { - auto const asset = ctx.tx[sfAsset].get(); - auto const asset2 = ctx.tx[sfAsset2].get(); + auto const asset = ctx.tx[sfAsset]; + auto const asset2 = ctx.tx[sfAsset2]; auto const sleIssuer = ctx.view.read(keylet::account(ctx.tx[sfAccount])); if (!sleIssuer) return terNO_ACCOUNT; // LCOV_EXCL_LINE @@ -139,8 +144,8 @@ AMMClawback::applyGuts(Sandbox& sb) std::optional const clawAmount = ctx_.tx[~sfAmount]; AccountID const issuer = ctx_.tx[sfAccount]; AccountID const holder = ctx_.tx[sfHolder]; - Issue const asset = ctx_.tx[sfAsset].get(); - Issue const asset2 = ctx_.tx[sfAsset2].get(); + Asset const asset = ctx_.tx[sfAsset]; + Asset const asset2 = ctx_.tx[sfAsset2]; auto ammSle = sb.peek(keylet::amm(asset, asset2)); if (!ammSle) @@ -157,6 +162,7 @@ AMMClawback::applyGuts(Sandbox& sb) asset, asset2, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, ctx_.journal); if (!expected) @@ -188,6 +194,7 @@ AMMClawback::applyGuts(Sandbox& sb) holdLPtokens, 0, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, WithdrawAll::Yes, mPriorBalance, ctx_.journal); @@ -251,7 +258,7 @@ AMMClawback::equalWithdrawMatchingOneAmount( auto const amount2Withdraw = amount2Balance * frac; auto const lpTokensWithdraw = - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); + toSTAmount(lptAMMBalance.asset(), lptAMMBalance * frac); if (lpTokensWithdraw > holdLPtokens) // if lptoken balance less than what the issuer intended to clawback, // clawback all the tokens. Because we are doing a two-asset withdrawal, @@ -268,6 +275,7 @@ AMMClawback::equalWithdrawMatchingOneAmount( holdLPtokens, 0, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, WithdrawAll::Yes, mPriorBalance, ctx_.journal); @@ -281,11 +289,12 @@ AMMClawback::equalWithdrawMatchingOneAmount( holder, amountBalance, amount, - toSTAmount(amount2Balance.issue(), amount2Withdraw), + toSTAmount(amount2Balance.asset(), amount2Withdraw), lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + toSTAmount(lptAMMBalance.asset(), lptAMMBalance * frac), 0, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, WithdrawAll::No, mPriorBalance, ctx_.journal); diff --git a/src/xrpld/app/tx/detail/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index 31773166d4a..d432b53b7e9 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include #include #include @@ -38,6 +40,13 @@ AMMCreate::preflight(PreflightContext const& ctx) if (!ammEnabled(ctx.rules)) return temDISABLED; + auto const amount = ctx.tx[sfAmount]; + auto const amount2 = ctx.tx[sfAmount2]; + + if (!ctx.rules.enabled(featureMPTokensV2) && + (amount.holds() || amount2.holds())) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -47,13 +56,10 @@ AMMCreate::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - 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 asset."; return temBAD_AMM_TOKENS; } @@ -93,50 +99,50 @@ AMMCreate::preclaim(PreclaimContext const& ctx) auto const amount2 = ctx.tx[sfAmount2]; // Check if AMM already exists for the token pair - if (auto const ammKeylet = keylet::amm(amount.issue(), amount2.issue()); + if (auto const ammKeylet = keylet::amm(amount.asset(), amount2.asset()); ctx.view.read(ammKeylet)) { JLOG(ctx.j.debug()) << "AMM Instance: ltAMM already exists."; return tecDUPLICATE; } - if (auto const ter = requireAuth(ctx.view, amount.issue(), accountID); + if (auto const ter = requireAuth(ctx.view, amount.asset(), accountID); ter != tesSUCCESS) { JLOG(ctx.j.debug()) - << "AMM Instance: account is not authorized, " << amount.issue(); + << "AMM Instance: account is not authorized, " << amount.asset(); return ter; } - if (auto const ter = requireAuth(ctx.view, amount2.issue(), accountID); + if (auto const ter = requireAuth(ctx.view, amount2.asset(), accountID); ter != tesSUCCESS) { JLOG(ctx.j.debug()) - << "AMM Instance: account is not authorized, " << amount2.issue(); + << "AMM Instance: account is not authorized, " << amount2.asset(); return ter; } // Globally or individually frozen - if (isFrozen(ctx.view, accountID, amount.issue()) || - isFrozen(ctx.view, accountID, amount2.issue())) + if (isFrozen(ctx.view, accountID, amount.asset()) || + isFrozen(ctx.view, accountID, amount2.asset())) { JLOG(ctx.j.debug()) << "AMM Instance: involves frozen asset."; return tecFROZEN; } - auto noDefaultRipple = [](ReadView const& view, Issue const& issue) { - if (isXRP(issue)) + auto noDefaultRipple = [](ReadView const& view, Asset const& asset) { + if (asset.holds() || isXRP(asset)) return false; if (auto const issuerAccount = - view.read(keylet::account(issue.account))) + view.read(keylet::account(asset.getIssuer()))) return (issuerAccount->getFlags() & lsfDefaultRipple) == 0; return false; }; - if (noDefaultRipple(ctx.view, amount.issue()) || - noDefaultRipple(ctx.view, amount2.issue())) + if (noDefaultRipple(ctx.view, amount.asset()) || + noDefaultRipple(ctx.view, amount2.asset())) { JLOG(ctx.j.debug()) << "AMM Instance: DefaultRipple not set"; return terNO_RIPPLE; @@ -151,16 +157,17 @@ AMMCreate::preclaim(PreclaimContext const& ctx) return tecINSUF_RESERVE_LINE; } - auto insufficientBalance = [&](STAmount const& asset) { - if (isXRP(asset)) - return xrpBalance < asset; - return accountID != asset.issue().account && + auto insufficientBalance = [&](STAmount const& amount) { + if (isXRP(amount)) + return xrpBalance < amount; + return accountID != amount.asset().getIssuer() && accountHolds( ctx.view, accountID, - asset.issue(), + amount.asset(), FreezeHandling::fhZERO_IF_FROZEN, - ctx.j) < asset; + AuthHandling::ahZERO_IF_UNAUTHORIZED, + ctx.j) < amount; }; if (insufficientBalance(amount) || insufficientBalance(amount2)) @@ -172,7 +179,7 @@ AMMCreate::preclaim(PreclaimContext const& ctx) auto isLPToken = [&](STAmount const& amount) -> bool { if (auto const sle = - ctx.view.read(keylet::account(amount.issue().account))) + ctx.view.read(keylet::account(amount.asset().getIssuer()))) return sle->isFieldPresent(sfAMMID); return false; }; @@ -191,20 +198,45 @@ AMMCreate::preclaim(PreclaimContext const& ctx) // Disallow AMM if the issuer has clawback enabled when featureAMMClawback // is not enabled - auto clawbackDisabled = [&](Issue const& issue) -> TER { - if (isXRP(issue)) + auto clawbackDisabled = [&](Asset const& asset) -> TER { + if (isXRP(asset)) return tesSUCCESS; - if (auto const sle = ctx.view.read(keylet::account(issue.account)); - !sle) - return tecINTERNAL; - else if (sle->getFlags() & lsfAllowTrustLineClawback) - return tecNO_PERMISSION; + if (asset.holds()) + { + if (auto const sle = ctx.view.read( + keylet::mptIssuance(asset.get().getMptID())); + !sle) + return tecINTERNAL; + else if (sle->getFlags() & lsfMPTCanClawback) + return tecNO_PERMISSION; + } + else + { + if (auto const sle = + ctx.view.read(keylet::account(asset.getIssuer())); + !sle) + return tecINTERNAL; + else if (sle->getFlags() & lsfAllowTrustLineClawback) + return tecNO_PERMISSION; + } return tesSUCCESS; }; - if (auto const ter = clawbackDisabled(amount.issue()); ter != tesSUCCESS) + if (auto const ter = clawbackDisabled(amount.asset()); ter != tesSUCCESS) + return ter; + if (auto const ter = clawbackDisabled(amount2.asset()); ter != tesSUCCESS) + return ter; + + if (auto const ter = + isMPTTxAllowed(ctx.view, ttAMM_CREATE, amount.asset(), accountID); + ter != tesSUCCESS) + return ter; + if (auto const ter = + isMPTTxAllowed(ctx.view, ttAMM_CREATE, amount2.asset(), accountID); + ter != tesSUCCESS) return ter; - return clawbackDisabled(amount2.issue()); + + return tesSUCCESS; } static std::pair @@ -217,7 +249,7 @@ applyCreate( auto const amount = ctx_.tx[sfAmount]; auto const amount2 = ctx_.tx[sfAmount2]; - auto const ammKeylet = keylet::amm(amount.issue(), amount2.issue()); + auto const ammKeylet = keylet::amm(amount.asset(), amount2.asset()); // Mitigate same account exists possibility auto const ammAccount = [&]() -> Expected { @@ -240,8 +272,8 @@ applyCreate( } // LP Token already exists. (should not happen) - auto const lptIss = ammLPTIssue( - amount.issue().currency, amount2.issue().currency, *ammAccount); + auto const lptIss = + ammLPTIssue(amount.asset(), amount2.asset(), *ammAccount); if (sb.read(keylet::line(*ammAccount, lptIss))) { JLOG(j_.error()) << "AMM Instance: LP Token already exists."; @@ -279,9 +311,9 @@ applyCreate( auto ammSle = std::make_shared(ammKeylet); ammSle->setAccountID(sfAccount, *ammAccount); ammSle->setFieldAmount(sfLPTokenBalance, lpTokens); - auto const& [issue1, issue2] = std::minmax(amount.issue(), amount2.issue()); - ammSle->setFieldIssue(sfAsset, STIssue{sfAsset, issue1}); - ammSle->setFieldIssue(sfAsset2, STIssue{sfAsset2, issue2}); + auto const& [asset1, asset2] = std::minmax(amount.asset(), amount2.asset()); + ammSle->setFieldIssue(sfAsset, STIssue{sfAsset, asset1}); + ammSle->setFieldIssue(sfAsset2, STIssue{sfAsset2, asset2}); // AMM creator gets the auction slot and the voting slot. initializeFeeAuctionVote( ctx_.view(), ammSle, account_, lptIss, ctx_.tx[sfTradingFee]); @@ -309,7 +341,24 @@ applyCreate( return {res, false}; } - auto sendAndTrustSet = [&](STAmount const& amount) -> TER { + auto sendAndInitTrustOrMPT = [&](STAmount const& amount) -> TER { + // Authorize MPT + if (amount.holds()) + { + auto const& mptIssue = amount.get(); + if (auto const err = requireAuth( + ctx_.view(), mptIssue, *ammAccount, MPTAuthType::WeakAuth); + err != tesSUCCESS) + return err; + + if (auto const err = MPTokenAuthorize::createMPToken( + sb, mptIssue.getMptID(), *ammAccount, lsfMPTAMM); + err != tesSUCCESS) + return err; + // Don't adjust AMM owner count. + // It's irrelevant for pseudo-account like AMM. + } + if (auto const res = accountSend( sb, account_, @@ -318,11 +367,12 @@ 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())); + sb.peek(keylet::line(*ammAccount, amount.get())); !sleRippleState) return tecINTERNAL; else @@ -332,11 +382,12 @@ applyCreate( sb.update(sleRippleState); } } + return tesSUCCESS; }; // Send asset1. - res = sendAndTrustSet(amount); + res = sendAndInitTrustOrMPT(amount); if (res != tesSUCCESS) { JLOG(j_.debug()) << "AMM Instance: failed to send " << amount; @@ -344,7 +395,7 @@ applyCreate( } // Send asset2. - res = sendAndTrustSet(amount2); + res = sendAndInitTrustOrMPT(amount2); if (res != tesSUCCESS) { JLOG(j_.debug()) << "AMM Instance: failed to send " << amount2; @@ -355,15 +406,15 @@ applyCreate( << ammKeylet.key << " " << lpTokens << " " << amount << " " << amount2; auto addOrderBook = - [&](Issue const& issueIn, Issue const& issueOut, std::uint64_t uRate) { - Book const book{issueIn, issueOut}; + [&](Asset const& assetIn, Asset const& assetOut, std::uint64_t uRate) { + Book const book{assetIn, assetOut}; auto const dir = keylet::quality(keylet::book(book), uRate); if (auto const bookExisted = static_cast(sb.read(dir)); !bookExisted) ctx_.app.getOrderBookDB().addOrderBook(book); }; - addOrderBook(amount.issue(), amount2.issue(), getRate(amount2, amount)); - addOrderBook(amount2.issue(), amount.issue(), getRate(amount, amount2)); + addOrderBook(amount.asset(), amount2.asset(), getRate(amount2, amount)); + addOrderBook(amount2.asset(), amount.asset(), getRate(amount, amount2)); return {res, res == tesSUCCESS}; } diff --git a/src/xrpld/app/tx/detail/AMMDelete.cpp b/src/xrpld/app/tx/detail/AMMDelete.cpp index 430ac17e87b..4592bbedf2c 100644 --- a/src/xrpld/app/tx/detail/AMMDelete.cpp +++ b/src/xrpld/app/tx/detail/AMMDelete.cpp @@ -35,6 +35,11 @@ AMMDelete::preflight(PreflightContext const& ctx) if (!ammEnabled(ctx.rules)) return temDISABLED; + if (!ctx.rules.enabled(featureMPTokensV2) && + (ctx.tx[sfAsset].holds() || + ctx.tx[sfAsset2].holds())) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -72,8 +77,8 @@ AMMDelete::doApply() // as we go on processing transactions. Sandbox sb(&ctx_.view()); - auto const ter = deleteAMMAccount( - sb, ctx_.tx[sfAsset].get(), ctx_.tx[sfAsset2].get(), j_); + auto const ter = + deleteAMMAccount(sb, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], j_); if (ter == tesSUCCESS || ter == tecINCOMPLETE) sb.apply(ctx_.rawView()); diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 675f560098c..e2602fdef44 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -38,6 +39,16 @@ AMMDeposit::preflight(PreflightContext const& ctx) if (!ammEnabled(ctx.rules)) return temDISABLED; + auto const amount = ctx.tx[~sfAmount]; + auto const amount2 = ctx.tx[~sfAmount2]; + + if (!ctx.rules.enabled(featureMPTokensV2) && + (ctx.tx[sfAsset].holds() || + ctx.tx[sfAsset2].holds() || + (amount && amount->holds()) || + (amount2 && amount2->holds()))) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -48,8 +59,6 @@ AMMDeposit::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - auto const amount = ctx.tx[~sfAmount]; - auto const amount2 = ctx.tx[~sfAmount2]; auto const ePrice = ctx.tx[~sfEPrice]; auto const lpTokens = ctx.tx[~sfLPTokenOut]; auto const tradingFee = ctx.tx[~sfTradingFee]; @@ -100,18 +109,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 +158,7 @@ AMMDeposit::preflight(PreflightContext const& ctx) if (auto const res = invalidAMMAmount( *ePrice, std::make_optional( - std::make_pair(amount->issue(), amount->issue())))) + std::make_pair(amount->asset(), amount->asset())))) { JLOG(ctx.j.debug()) << "AMM Deposit: invalid EPrice"; return res; @@ -184,6 +193,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) std::nullopt, std::nullopt, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, ctx.j); if (!expected) return expected.error(); // LCOV_EXCL_LINE @@ -223,7 +233,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) auto balance = [&](auto const& deposit) -> TER { if (isXRP(deposit)) { - auto const lpIssue = (*ammSle)[sfLPTokenBalance].issue(); + auto const lpIssue = (*ammSle)[sfLPTokenBalance].get(); // Adjust the reserve if LP doesn't have LPToken trustline auto const sle = ctx.view.read( keylet::line(accountID, lpIssue.account, lpIssue.currency)); @@ -233,12 +243,13 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) return tecUNFUNDED_AMM; return tecINSUF_RESERVE_LINE; } - return (accountID == deposit.issue().account || + return (accountID == deposit.asset().getIssuer() || accountHolds( ctx.view, accountID, - deposit.issue(), + deposit.asset(), FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, ctx.j) >= deposit) ? TER(tesSUCCESS) : tecUNFUNDED_AMM; @@ -248,8 +259,12 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) { // Check if either of the assets is frozen, AMMDeposit is not allowed // if either asset is frozen - auto checkAsset = [&](Issue const& asset) -> TER { - if (auto const ter = requireAuth(ctx.view, asset, accountID)) + auto checkAsset = [&](Asset const& asset) -> TER { + // WeakAuth - don't need to check if MPT object exists as might be + // depositing into non-MPT pool. It'll fail on send if MPT doesn't + // exist. + if (auto const ter = requireAuth( + ctx.view, asset, accountID, MPTAuthType::WeakAuth)) { JLOG(ctx.j.debug()) << "AMM Deposit: account is not authorized, " << asset; @@ -260,7 +275,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) { JLOG(ctx.j.debug()) << "AMM Deposit: account or currency is frozen, " - << to_string(accountID) << " " << to_string(asset.currency); + << to_string(accountID) << " " << to_string(asset); return tecFROZEN; } @@ -268,10 +283,10 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) return tesSUCCESS; }; - if (auto const ter = checkAsset(ctx.tx[sfAsset].get())) + if (auto const ter = checkAsset(ctx.tx[sfAsset])) return ter; - if (auto const ter = checkAsset(ctx.tx[sfAsset2].get())) + if (auto const ter = checkAsset(ctx.tx[sfAsset2])) return ter; } @@ -285,19 +300,19 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) { // This normally should not happen. // Account is not authorized to hold the assets it's depositing, - // or it doesn't even have a trust line for them + // or it doesn't even have a trust line or MPT for them. if (auto const ter = - requireAuth(ctx.view, amount->issue(), accountID)) + requireAuth(ctx.view, amount->asset(), accountID)) { // LCOV_EXCL_START JLOG(ctx.j.debug()) << "AMM Deposit: account is not authorized, " - << amount->issue(); + << amount->asset(); return ter; // LCOV_EXCL_STOP } // AMM account or currency frozen - if (isFrozen(ctx.view, ammAccountID, amount->issue())) + if (isFrozen(ctx.view, ammAccountID, amount->asset())) { JLOG(ctx.j.debug()) << "AMM Deposit: AMM account or currency is frozen, " @@ -305,11 +320,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) @@ -345,7 +360,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) // Equal deposit lp tokens if (auto const lpTokens = ctx.tx[~sfLPTokenOut]; - lpTokens && lpTokens->issue() != lptAMMBalance.issue()) + lpTokens && lpTokens->asset() != lptAMMBalance.asset()) { JLOG(ctx.j.debug()) << "AMM Deposit: invalid LPTokens."; return temBAD_AMM_TOKENS; @@ -364,6 +379,15 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) } } + if (auto const ter = + isMPTTxAllowed(ctx.view, ttAMM_DEPOSIT, ctx.tx[sfAsset], accountID); + ter != tesSUCCESS) + return ter; + if (auto const ter = isMPTTxAllowed( + ctx.view, ttAMM_DEPOSIT, ctx.tx[sfAsset2], accountID); + ter != tesSUCCESS) + return ter; + return tesSUCCESS; } @@ -382,9 +406,10 @@ AMMDeposit::applyGuts(Sandbox& sb) auto const expected = ammHolds( sb, *ammSle, - amount ? amount->issue() : std::optional{}, - amount2 ? amount2->issue() : std::optional{}, + amount ? amount->asset() : std::optional{}, + amount2 ? amount2->asset() : std::optional{}, FreezeHandling::fhZERO_IF_FROZEN, + AuthHandling::ahIGNORE_AUTH, ctx_.journal); if (!expected) return {expected.error(), false}; // LCOV_EXCL_LINE @@ -455,7 +480,7 @@ AMMDeposit::applyGuts(Sandbox& sb) ammAccountID, *amount, *amount2, - lptAMMBalance.issue(), + lptAMMBalance.asset(), tfee); // should not happen. // LCOV_EXCL_START @@ -474,7 +499,7 @@ AMMDeposit::applyGuts(Sandbox& sb) // and the voting if (lptAMMBalance == beast::zero) initializeFeeAuctionVote( - sb, ammSle, account_, lptAMMBalance.issue(), tfee); + sb, ammSle, account_, lptAMMBalance.asset(), tfee); sb.update(ammSle); } @@ -517,7 +542,7 @@ AMMDeposit::deposit( return temBAD_AMOUNT; if (isXRP(depositAmount)) { - auto const& lpIssue = lpTokensDeposit.issue(); + auto const& lpIssue = lpTokensDeposit.get(); // Adjust the reserve if LP doesn't have LPToken trustline auto const sle = view.read( keylet::line(account_, lpIssue.account, lpIssue.currency)); @@ -525,12 +550,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; @@ -646,13 +672,13 @@ AMMDeposit::equalDepositTokens( try { auto const frac = - divide(lpTokensDeposit, lptAMMBalance, lptAMMBalance.issue()); + divide(lpTokensDeposit, lptAMMBalance, lptAMMBalance.asset()); return deposit( view, ammAccount, amountBalance, - multiply(amountBalance, frac, amountBalance.issue()), - multiply(amount2Balance, frac, amount2Balance.issue()), + multiply(amountBalance, frac, amountBalance.asset()), + multiply(amount2Balance, frac, amount2Balance.asset()), lptAMMBalance, lpTokensDeposit, depositMin, @@ -711,7 +737,7 @@ AMMDeposit::equalDepositLimit( std::uint16_t tfee) { auto frac = Number{amount} / amountBalance; - auto tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); + auto tokens = toSTAmount(lptAMMBalance.asset(), lptAMMBalance * frac); if (tokens == beast::zero) return {tecAMM_FAILED, STAmount{}}; auto const amount2Deposit = amount2Balance * frac; @@ -721,7 +747,7 @@ AMMDeposit::equalDepositLimit( ammAccount, amountBalance, amount, - toSTAmount(amount2Balance.issue(), amount2Deposit), + toSTAmount(amount2Balance.asset(), amount2Deposit), lptAMMBalance, tokens, std::nullopt, @@ -729,7 +755,7 @@ AMMDeposit::equalDepositLimit( lpTokensDepositMin, tfee); frac = Number{amount2} / amount2Balance; - tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); + tokens = toSTAmount(lptAMMBalance.asset(), lptAMMBalance * frac); if (tokens == beast::zero) return {tecAMM_FAILED, STAmount{}}; auto const amountDeposit = amountBalance * frac; @@ -738,7 +764,7 @@ AMMDeposit::equalDepositLimit( view, ammAccount, amountBalance, - toSTAmount(amountBalance.issue(), amountDeposit), + toSTAmount(amountBalance.asset(), amountDeposit), amount2, lptAMMBalance, tokens, @@ -901,12 +927,12 @@ 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{}}; auto const tokens = - toSTAmount(lptAMMBalance.issue(), amountDeposit / ePrice); + toSTAmount(lptAMMBalance.asset(), amountDeposit / ePrice); return deposit( view, ammAccount, @@ -927,7 +953,7 @@ AMMDeposit::equalDepositInEmptyState( AccountID const& ammAccount, STAmount const& amount, STAmount const& amount2, - Issue const& lptIssue, + Asset const& lptIssue, std::uint16_t tfee) { return deposit( diff --git a/src/xrpld/app/tx/detail/AMMDeposit.h b/src/xrpld/app/tx/detail/AMMDeposit.h index 0acb1dd9ab3..09d1748b9cf 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.h +++ b/src/xrpld/app/tx/detail/AMMDeposit.h @@ -238,7 +238,7 @@ class AMMDeposit : public Transactor AccountID const& ammAccount, STAmount const& amount, STAmount const& amount2, - Issue const& lptIssue, + Asset const& lptIssue, std::uint16_t tfee); }; diff --git a/src/xrpld/app/tx/detail/AMMVote.cpp b/src/xrpld/app/tx/detail/AMMVote.cpp index 1b8b91e518a..c57198162aa 100644 --- a/src/xrpld/app/tx/detail/AMMVote.cpp +++ b/src/xrpld/app/tx/detail/AMMVote.cpp @@ -35,11 +35,15 @@ AMMVote::preflight(PreflightContext const& ctx) if (!ammEnabled(ctx.rules)) return temDISABLED; + if (!ctx.rules.enabled(featureMPTokensV2) && + (ctx.tx[sfAsset].holds() || + ctx.tx[sfAsset2].holds())) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; - if (auto const res = invalidAMMAssetPair( - ctx.tx[sfAsset].get(), ctx.tx[sfAsset2].get())) + if (auto const res = invalidAMMAssetPair(ctx.tx[sfAsset], ctx.tx[sfAsset2])) { JLOG(ctx.j.debug()) << "AMM Vote: invalid asset pair."; return res; diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 23e8529cfc9..3f67f574098 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -21,7 +21,10 @@ #include #include +#include +#include #include +#include #include #include #include @@ -37,6 +40,16 @@ AMMWithdraw::preflight(PreflightContext const& ctx) if (!ammEnabled(ctx.rules)) return temDISABLED; + auto const amount = ctx.tx[~sfAmount]; + auto const amount2 = ctx.tx[~sfAmount2]; + + if (!ctx.rules.enabled(featureMPTokensV2) && + (ctx.tx[sfAsset].holds() || + ctx.tx[sfAsset2].holds() || + (amount && amount->holds()) || + (amount2 && amount2->holds()))) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -47,8 +60,6 @@ AMMWithdraw::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - auto const amount = ctx.tx[~sfAmount]; - auto const amount2 = ctx.tx[~sfAmount2]; auto const ePrice = ctx.tx[~sfEPrice]; auto const lpTokens = ctx.tx[~sfLPTokenIn]; // Valid combinations are: @@ -100,18 +111,18 @@ AMMWithdraw::preflight(PreflightContext const& ctx) return temMALFORMED; } - auto const asset = ctx.tx[sfAsset].get(); - auto const asset2 = ctx.tx[sfAsset2].get(); + auto const asset = ctx.tx[sfAsset]; + auto const asset2 = ctx.tx[sfAsset2]; if (auto const res = invalidAMMAssetPair(asset, asset2)) { JLOG(ctx.j.debug()) << "AMM Withdraw: Invalid asset pair."; return res; } - if (amount && amount2 && amount->issue() == amount2->issue()) + if (amount && amount2 && amount->asset() == amount2->asset()) { JLOG(ctx.j.debug()) << "AMM Withdraw: invalid tokens, same issue." - << amount->issue() << " " << amount2->issue(); + << amount->asset() << " " << amount2->asset(); return temBAD_AMM_TOKENS; } @@ -186,9 +197,10 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) auto const expected = ammHolds( ctx.view, *ammSle, - amount ? amount->issue() : std::optional{}, - amount2 ? amount2->issue() : std::optional{}, + amount ? amount->asset() : std::optional{}, + amount2 ? amount2->asset() : std::optional{}, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, ctx.j); if (!expected) return expected.error(); @@ -216,16 +228,20 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) << *amount; return tecAMM_BALANCE; } - if (auto const ter = - requireAuth(ctx.view, amount->issue(), accountID)) + // WeakAuth - MPToken is created if it doesn't exist. + if (auto const ter = requireAuth( + ctx.view, + amount->asset(), + accountID, + MPTAuthType::WeakAuth)) { JLOG(ctx.j.debug()) << "AMM Withdraw: account is not authorized, " - << amount->issue(); + << amount->asset(); return ter; } // AMM account or currency frozen - if (isFrozen(ctx.view, ammAccountID, amount->issue())) + if (isFrozen(ctx.view, ammAccountID, amount->asset())) { JLOG(ctx.j.debug()) << "AMM Withdraw: AMM account or currency is frozen, " @@ -233,11 +249,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; } } @@ -261,7 +277,7 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) return tecAMM_BALANCE; } - if (lpTokensWithdraw && lpTokensWithdraw->issue() != lpTokens.issue()) + if (lpTokensWithdraw && lpTokensWithdraw->asset() != lpTokens.asset()) { JLOG(ctx.j.debug()) << "AMM Withdraw: invalid LPTokens."; return temBAD_AMM_TOKENS; @@ -274,7 +290,7 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) } if (auto const ePrice = ctx.tx[~sfEPrice]; - ePrice && ePrice->issue() != lpTokens.issue()) + ePrice && ePrice->asset() != lpTokens.asset()) { JLOG(ctx.j.debug()) << "AMM Withdraw: invalid EPrice."; return temBAD_AMM_TOKENS; @@ -288,6 +304,15 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) return ter; } + if (auto const ter = isMPTTxAllowed( + ctx.view, ttAMM_WITHDRAW, ctx.tx[sfAsset], accountID); + ter != tesSUCCESS) + return ter; + if (auto const ter = isMPTTxAllowed( + ctx.view, ttAMM_WITHDRAW, ctx.tx[sfAsset2], accountID); + ter != tesSUCCESS) + return ter; + return tesSUCCESS; } @@ -314,7 +339,7 @@ AMMWithdraw::applyGuts(Sandbox& sb) if (sb.rules().enabled(fixAMMv1_1)) { if (auto const res = - isOnlyLiquidityProvider(sb, lpTokens.issue(), account_); + isOnlyLiquidityProvider(sb, lpTokens.get(), account_); !res) return {res.error(), false}; else if (res.value()) @@ -339,9 +364,10 @@ AMMWithdraw::applyGuts(Sandbox& sb) auto const expected = ammHolds( sb, *ammSle, - amount ? amount->issue() : std::optional{}, - amount2 ? amount2->issue() : std::optional{}, + amount ? amount->asset() : std::optional{}, + amount2 ? amount2->asset() : std::optional{}, FreezeHandling::fhZERO_IF_FROZEN, + AuthHandling::ahIGNORE_AUTH, ctx_.journal); if (!expected) return {expected.error(), false}; @@ -418,12 +444,7 @@ AMMWithdraw::applyGuts(Sandbox& sb) return {result, false}; auto const res = deleteAMMAccountIfEmpty( - sb, - ammSle, - newLPTokenBalance, - ctx_.tx[sfAsset].get(), - ctx_.tx[sfAsset2].get(), - j_); + sb, ammSle, newLPTokenBalance, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], j_); // LCOV_EXCL_START if (!res.second) return {res.first, false}; @@ -476,6 +497,7 @@ AMMWithdraw::withdraw( lpTokensWithdraw, tfee, FreezeHandling::fhZERO_IF_FROZEN, + AuthHandling::ahIGNORE_AUTH, isWithdrawAll(ctx_.tx), mPriorBalance, j_); @@ -495,6 +517,7 @@ AMMWithdraw::withdraw( STAmount const& lpTokensWithdraw, std::uint16_t tfee, FreezeHandling freezeHandling, + AuthHandling authHandling, WithdrawAll withdrawAll, XRPAmount const& priorBalance, beast::Journal const& journal) @@ -503,9 +526,10 @@ AMMWithdraw::withdraw( auto const expected = ammHolds( view, ammSle, - amountWithdraw.issue(), + amountWithdraw.asset(), std::nullopt, freezeHandling, + authHandling, journal); // LCOV_EXCL_START if (!expected) @@ -596,33 +620,77 @@ AMMWithdraw::withdraw( return {tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}}; } - // Check the reserve in case a trustline has to be created + // Check the reserve in case a trustline or MPT has to be created bool const enabledFixAMMv1_2 = view.rules().enabled(fixAMMv1_2); - auto sufficientReserve = [&](Issue const& issue) -> TER { - if (!enabledFixAMMv1_2 || isXRP(issue)) + // If seated after a call to sufficientReserve() then MPToken must be + // authorized + std::optional mptokenKey; + auto sufficientReserve = [&](Asset const& asset) -> TER { + mptokenKey = std::nullopt; + if (!enabledFixAMMv1_2 || isXRP(asset)) return tesSUCCESS; - if (!view.exists(keylet::line(account, issue))) + bool const isIssue = asset.holds(); + bool const checkReserve = [&] { + if (isIssue) + return !view.exists(keylet::line(account, asset.get())); + auto const issuanceKey = keylet::mptIssuance(asset.get()); + mptokenKey = keylet::mptoken(issuanceKey.key, account); + if (!view.exists(*mptokenKey)) + return true; + mptokenKey = std::nullopt; + return false; + }(); + if (checkReserve) { - auto const sleAccount = view.read(keylet::account(account)); + auto sleAccount = view.peek(keylet::account(account)); if (!sleAccount) return tecINTERNAL; // LCOV_EXCL_LINE - auto const balance = (*sleAccount)[sfBalance].xrp(); + STAmount const balance = (*sleAccount)[sfBalance]; std::uint32_t const ownerCount = sleAccount->at(sfOwnerCount); - // See also SetTrust::doApply() + // See also SetTrust::doApply() and MPTokenAuthorize::authorize() XRPAmount const reserve( (ownerCount < 2) ? XRPAmount(beast::zero) : view.fees().accountReserve(ownerCount + 1)); - if (std::max(priorBalance, balance) < reserve) + auto const balance_ = + isIssue ? std::max(priorBalance, balance.xrp()) : priorBalance; + if (balance_ < reserve) return tecINSUFFICIENT_RESERVE; + + // Update owner count. + if (!isIssue) + adjustOwnerCount(view, sleAccount, 1, journal); } return tesSUCCESS; }; - if (auto const err = sufficientReserve(amountWithdrawActual.issue())) + // Create MPToken if it doesn't exist + auto createMPToken = [&](Asset const& asset) -> TER { + // If mptoken is seated then must authorize + if (mptokenKey) + { + auto const& mptIssue = asset.get(); + if (auto const err = + requireAuth(view, mptIssue, account, MPTAuthType::WeakAuth); + err != tesSUCCESS) + return err; + + if (auto const err = MPTokenAuthorize::createMPToken( + view, mptIssue.getMptID(), account, 0); + err != tesSUCCESS) + return err; + } + return tesSUCCESS; + }; + + if (auto const err = sufficientReserve(amountWithdrawActual.asset())) return {err, STAmount{}, STAmount{}, STAmount{}}; + if (auto const res = createMPToken(amountWithdrawActual.asset()); + res != tesSUCCESS) + return {res, STAmount{}, STAmount{}, STAmount{}}; + // Withdraw amountWithdraw auto res = accountSend( view, @@ -643,10 +711,14 @@ AMMWithdraw::withdraw( // Withdraw amount2Withdraw if (amount2WithdrawActual) { - if (auto const err = sufficientReserve(amount2WithdrawActual->issue()); + if (auto const err = sufficientReserve(amount2WithdrawActual->asset()); err != tesSUCCESS) return {err, STAmount{}, STAmount{}, STAmount{}}; + if (auto const res = createMPToken(amount2WithdrawActual->asset()); + res != tesSUCCESS) + return {res, STAmount{}, STAmount{}, STAmount{}}; + res = accountSend( view, ammAccount, @@ -669,7 +741,7 @@ AMMWithdraw::withdraw( view, account, lpTokensWithdrawActual, - lpTokensWithdrawActual.issue(), + lpTokensWithdrawActual.get(), journal); if (res != tesSUCCESS) { @@ -713,6 +785,7 @@ AMMWithdraw::equalWithdrawTokens( lpTokensWithdraw, tfee, FreezeHandling::fhZERO_IF_FROZEN, + AuthHandling::ahIGNORE_AUTH, isWithdrawAll(ctx_.tx), mPriorBalance, ctx_.journal); @@ -724,15 +797,15 @@ AMMWithdraw::deleteAMMAccountIfEmpty( Sandbox& sb, std::shared_ptr const ammSle, STAmount const& lpTokenBalance, - Issue const& issue1, - Issue const& issue2, + Asset const& asset1, + Asset const& asset2, beast::Journal const& journal) { TER ter; bool updateBalance = true; if (lpTokenBalance == beast::zero) { - ter = deleteAMMAccount(sb, issue1, issue2, journal); + ter = deleteAMMAccount(sb, asset1, asset2, journal); if (ter != tesSUCCESS && ter != tecINCOMPLETE) return {ter, false}; // LCOV_EXCL_LINE else @@ -763,6 +836,7 @@ AMMWithdraw::equalWithdrawTokens( STAmount const& lpTokensWithdraw, std::uint16_t tfee, FreezeHandling freezeHanding, + AuthHandling authHandling, WithdrawAll withdrawAll, XRPAmount const& priorBalance, beast::Journal const& journal) @@ -784,6 +858,7 @@ AMMWithdraw::equalWithdrawTokens( lpTokensWithdraw, tfee, freezeHanding, + authHandling, WithdrawAll::Yes, priorBalance, journal); @@ -791,9 +866,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 @@ -813,6 +888,7 @@ AMMWithdraw::equalWithdrawTokens( lpTokensWithdraw, tfee, freezeHanding, + authHandling, withdrawAll, priorBalance, journal); @@ -874,9 +950,9 @@ AMMWithdraw::equalWithdrawLimit( ammAccount, amountBalance, amount, - toSTAmount(amount2.issue(), amount2Withdraw), + toSTAmount(amount2.asset(), amount2Withdraw), lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + toSTAmount(lptAMMBalance.asset(), lptAMMBalance * frac), tfee); } @@ -890,10 +966,10 @@ AMMWithdraw::equalWithdrawLimit( ammSle, ammAccount, amountBalance, - toSTAmount(amount.issue(), amountWithdraw), + toSTAmount(amount.asset(), amountWithdraw), amount2, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + toSTAmount(lptAMMBalance.asset(), lptAMMBalance * frac), tfee); } @@ -1014,7 +1090,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( @@ -1025,7 +1101,7 @@ AMMWithdraw::singleWithdrawEPrice( amountWithdraw, std::nullopt, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), tokens), + toSTAmount(lptAMMBalance.asset(), tokens), tfee); } diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.h b/src/xrpld/app/tx/detail/AMMWithdraw.h index ae9328cb05e..b3cbc105330 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.h +++ b/src/xrpld/app/tx/detail/AMMWithdraw.h @@ -112,6 +112,7 @@ class AMMWithdraw : public Transactor STAmount const& lpTokensWithdraw, std::uint16_t tfee, FreezeHandling freezeHanding, + AuthHandling authHandling, WithdrawAll withdrawAll, XRPAmount const& priorBalance, beast::Journal const& journal); @@ -145,6 +146,7 @@ class AMMWithdraw : public Transactor STAmount const& lpTokensWithdraw, std::uint16_t tfee, FreezeHandling freezeHandling, + AuthHandling authHandling, WithdrawAll withdrawAll, XRPAmount const& priorBalance, beast::Journal const& journal); @@ -154,8 +156,8 @@ class AMMWithdraw : public Transactor Sandbox& sb, std::shared_ptr const ammSle, STAmount const& lpTokenBalance, - Issue const& issue1, - Issue const& issue2, + Asset const& asset1, + Asset const& asset2, beast::Journal const& journal); private: diff --git a/src/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index f6e5f6f3e3f..48bee8ebfda 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,15 @@ CashCheck::preflight(PreflightContext const& ctx) if (!ctx.rules.enabled(featureChecks)) return temDISABLED; + // Exactly one of Amount or DeliverMin must be present. + auto const optAmount = ctx.tx[~sfAmount]; + auto const optDeliverMin = ctx.tx[~sfDeliverMin]; + + if (!ctx.rules.enabled(featureMPTokensV2) && + ((optAmount && optAmount->holds()) || + (optDeliverMin && optDeliverMin->holds()))) + return temDISABLED; + NotTEC const ret{preflight1(ctx)}; if (!isTesSuccess(ret)) return ret; @@ -49,10 +59,6 @@ CashCheck::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - // Exactly one of Amount or DeliverMin must be present. - auto const optAmount = ctx.tx[~sfAmount]; - auto const optDeliverMin = ctx.tx[~sfDeliverMin]; - if (static_cast(optAmount) == static_cast(optDeliverMin)) { JLOG(ctx.j.warn()) @@ -70,7 +76,7 @@ CashCheck::preflight(PreflightContext const& ctx) return temBAD_AMOUNT; } - if (badCurrency() == value.getCurrency()) + if (badCurrency() == value.asset()) { JLOG(ctx.j.warn()) << "Malformed transaction: Bad currency."; return temBAD_CURRENCY; @@ -141,8 +147,7 @@ CashCheck::preclaim(PreclaimContext const& ctx) }(ctx.tx)}; STAmount const sendMax = sleCheck->at(sfSendMax); - Currency const currency{value.getCurrency()}; - if (currency != sendMax.getCurrency()) + if (!equalTokens(value.asset(), sendMax.asset())) { JLOG(ctx.j.warn()) << "Check cash does not match check currency."; return temMALFORMED; @@ -167,6 +172,7 @@ CashCheck::preclaim(PreclaimContext const& ctx) sleCheck->at(sfAccount), value, fhZERO_IF_FROZEN, + ahIGNORE_AUTH, ctx.j)}; // Note that src will have one reserve's worth of additional XRP @@ -187,62 +193,97 @@ CashCheck::preclaim(PreclaimContext const& ctx) // An issuer can always accept their own currency. if (!value.native() && (value.getIssuer() != dstId)) { - auto const sleTrustLine = - ctx.view.read(keylet::line(dstId, issuerId, currency)); - - if (!sleTrustLine && - !ctx.view.rules().enabled(featureCheckCashMakesTrustLine)) + if (value.holds()) { - JLOG(ctx.j.warn()) - << "Cannot cash check for IOU without trustline."; - return tecNO_LINE; - } + Currency const currency{value.get().currency}; + auto const sleTrustLine = + ctx.view.read(keylet::line(dstId, issuerId, currency)); - auto const sleIssuer = ctx.view.read(keylet::account(issuerId)); - if (!sleIssuer) - { - JLOG(ctx.j.warn()) - << "Can't receive IOUs from non-existent issuer: " - << to_string(issuerId); - return tecNO_ISSUER; - } + if (!sleTrustLine && + !ctx.view.rules().enabled(featureCheckCashMakesTrustLine)) + { + JLOG(ctx.j.warn()) + << "Cannot cash check for IOU without trustline."; + return tecNO_LINE; + } - if (sleIssuer->at(sfFlags) & lsfRequireAuth) - { - if (!sleTrustLine) + auto const sleIssuer = ctx.view.read(keylet::account(issuerId)); + if (!sleIssuer) { - // We can only create a trust line if the issuer does not - // have requireAuth set. - return tecNO_AUTH; + JLOG(ctx.j.warn()) + << "Can't receive IOUs from non-existent issuer: " + << to_string(issuerId); + return tecNO_ISSUER; } - // Entries have a canonical representation, determined by a - // lexicographical "greater than" comparison employing strict - // weak ordering. Determine which entry we need to access. - bool const canonical_gt(dstId > issuerId); + if (sleIssuer->at(sfFlags) & lsfRequireAuth) + { + if (!sleTrustLine) + { + // We can only create a trust line if the issuer does + // not have requireAuth set. + return tecNO_AUTH; + } + + // Entries have a canonical representation, determined by a + // lexicographical "greater than" comparison employing + // strict weak ordering. Determine which entry we need to + // access. + bool const canonical_gt(dstId > issuerId); + + bool const is_authorized( + sleTrustLine->at(sfFlags) & + (canonical_gt ? lsfLowAuth : lsfHighAuth)); + + if (!is_authorized) + { + JLOG(ctx.j.warn()) + << "Can't receive IOUs from issuer without auth."; + return tecNO_AUTH; + } + } - bool const is_authorized( - sleTrustLine->at(sfFlags) & - (canonical_gt ? lsfLowAuth : lsfHighAuth)); + // The trustline from source to issuer does not need to + // be checked for freezing, since we already verified that the + // source has sufficient non-frozen funds available. - if (!is_authorized) + // However, the trustline from destination to issuer may not + // be frozen. + if (isFrozen(ctx.view, dstId, currency, issuerId)) { JLOG(ctx.j.warn()) - << "Can't receive IOUs from issuer without auth."; - return tecNO_AUTH; + << "Cashing a check to a frozen trustline."; + return tecFROZEN; } } + else + { + auto const sleIssuer = ctx.view.read(keylet::account(issuerId)); + if (!sleIssuer) + { + JLOG(ctx.j.warn()) + << "Can't receive MPTs from non-existent issuer: " + << to_string(issuerId); + return tecNO_ISSUER; + } - // The trustline from source to issuer does not need to - // be checked for freezing, since we already verified that the - // source has sufficient non-frozen funds available. + if (auto const err = requireAuth( + ctx.view, + value.get(), + dstId, + MPTAuthType::WeakAuth); + err != tesSUCCESS) + { + JLOG(ctx.j.warn()) + << "Cashing a check to a MPT requiring auth."; + return err; + } - // However, the trustline from destination to issuer may not - // be frozen. - if (isFrozen(ctx.view, dstId, currency, issuerId)) - { - JLOG(ctx.j.warn()) << "Cashing a check to a frozen trustline."; - return tecFROZEN; + if (isFrozen(ctx.view, dstId, value.asset().get())) + { + JLOG(ctx.j.warn()) << "Cashing a check to a frozen MPT."; + return tecFROZEN; + } } } } @@ -339,33 +380,25 @@ CashCheck::doApply() // maximum possible currency because there might be a gateway // transfer rate to account for. Since the transfer rate cannot // exceed 200%, we use 1/2 maxValue as our limit. + auto const maxDeliverMin = [&]() { + if (optDeliverMin->holds()) + return STAmount( + optDeliverMin->asset(), + STAmount::cMaxValue / 2, + STAmount::cMaxOffset); + return STAmount(optDeliverMin->asset(), maxMPTokenAmount / 2); + }; STAmount const flowDeliver{ - optDeliverMin ? STAmount( - optDeliverMin->issue(), - STAmount::cMaxValue / 2, - STAmount::cMaxOffset) + optDeliverMin ? maxDeliverMin() : ctx_.tx.getFieldAmount(sfAmount)}; - // If a trust line does not exist yet create one. - Issue const& trustLineIssue = flowDeliver.issue(); - AccountID const issuer = flowDeliver.getIssuer(); - AccountID const truster = issuer == account_ ? srcId : account_; - Keylet const trustLineKey = keylet::line(truster, trustLineIssue); - bool const destLow = issuer > account_; - bool const checkCashMakesTrustLine = psb.rules().enabled(featureCheckCashMakesTrustLine); - if (checkCashMakesTrustLine && !psb.exists(trustLineKey)) - { - // 1. Can the check casher meet the reserve for the trust line? - // 2. Create trust line between destination (this) account - // and the issuer. - // 3. Apply correct noRipple settings on trust line. Use... - // a. this (destination) account and - // b. issuing account (not sending account). - - auto const sleDst = psb.peek(keylet::account(account_)); + // Check reserve. Return destination account SLE if enough reserve, + // otherwise return nullptr. + auto checkReserve = [&]() -> std::shared_ptr { + auto sleDst = psb.peek(keylet::account(account_)); // Can the account cover the trust line's reserve? if (std::uint32_t const ownerCount = {sleDst->at(sfOwnerCount)}; @@ -374,20 +407,49 @@ CashCheck::doApply() JLOG(j_.trace()) << "Trust line does not exist. " "Insufficent reserve to create line."; - return tecNO_LINE_INSUF_RESERVE; + return nullptr; } + return sleDst; + }; - Currency const currency = flowDeliver.getCurrency(); - STAmount initialBalance(flowDeliver.issue()); - initialBalance.setIssuer(noAccount()); - - // clang-format off + std::optional trustLineKey; + STAmount savedLimit; + bool destLow = false; + if (flowDeliver.holds()) + { + // If a trust line does not exist yet create one. + Issue const& trustLineIssue = flowDeliver.get(); + AccountID const issuer = flowDeliver.getIssuer(); + AccountID const truster = issuer == account_ ? srcId : account_; + trustLineKey = keylet::line(truster, trustLineIssue); + destLow = issuer > account_; + + if (checkCashMakesTrustLine && !psb.exists(*trustLineKey)) + { + // 1. Can the check casher meet the reserve for the trust + // line? + // 2. Create trust line between destination (this) account + // and the issuer. + // 3. Apply correct noRipple settings on trust line. Use... + // a. this (destination) account and + // b. issuing account (not sending account). + + auto const sleDst = checkReserve(); + if (sleDst == nullptr) + return tecNO_LINE_INSUF_RESERVE; + + Currency const currency = + flowDeliver.asset().get().currency; + STAmount initialBalance(flowDeliver.asset()); + initialBalance.setIssuer(noAccount()); + + // clang-format off if (TER const ter = trustCreate( psb, // payment sandbox destLow, // is dest low? issuer, // source account_, // destination - trustLineKey.key, // ledger index + trustLineKey->key, // ledger index sleDst, // Account to add to false, // authorize account (sleDst->getFlags() & lsfDefaultRipple) == 0, @@ -402,41 +464,69 @@ CashCheck::doApply() { return ter; } - // clang-format on + // clang-format on - psb.update(sleDst); + psb.update(sleDst); - // Note that we _don't_ need to be careful about destroying - // the trust line if the check cashing fails. The transaction - // machinery will automatically clean it up. - } + // Note that we _don't_ need to be careful about destroying + // the trust line if the check cashing fails. The + // transaction machinery will automatically clean it up. + } - // Since the destination is signing the check, they clearly want - // the funds even if their new total funds would exceed the limit - // on their trust line. So we tweak the trust line limits before - // calling flow and then restore the trust line limits afterwards. - auto const sleTrustLine = psb.peek(trustLineKey); - if (!sleTrustLine) - return tecNO_LINE; - - SF_AMOUNT const& tweakedLimit = destLow ? sfLowLimit : sfHighLimit; - STAmount const savedLimit = sleTrustLine->at(tweakedLimit); - - // Make sure the tweaked limits are restored when we leave scope. - scope_exit fixup( - [&psb, &trustLineKey, &tweakedLimit, &savedLimit]() { - if (auto const sleTrustLine = psb.peek(trustLineKey)) - sleTrustLine->at(tweakedLimit) = savedLimit; - }); + // Since the destination is signing the check, they clearly want + // the funds even if their new total funds would exceed the + // limit on their trust line. So we tweak the trust line limits + // before calling flow and then restore the trust line limits + // afterwards. + auto const sleTrustLine = psb.peek(*trustLineKey); + if (!sleTrustLine) + return tecNO_LINE; - if (checkCashMakesTrustLine) + SF_AMOUNT const& tweakedLimit = + destLow ? sfLowLimit : sfHighLimit; + savedLimit = sleTrustLine->at(tweakedLimit); + + if (checkCashMakesTrustLine) + { + // Set the trust line limit to the highest possible value + // while flow runs. + STAmount const bigAmount( + trustLineIssue, + STAmount::cMaxValue, + STAmount::cMaxOffset); + sleTrustLine->at(tweakedLimit) = bigAmount; + } + } + else if (account_ != flowDeliver.getIssuer()) { - // Set the trust line limit to the highest possible value - // while flow runs. - STAmount const bigAmount( - trustLineIssue, STAmount::cMaxValue, STAmount::cMaxOffset); - sleTrustLine->at(tweakedLimit) = bigAmount; + auto const& mptID = flowDeliver.get().getMptID(); + // Create MPT if it doesn't exist + auto const mptokenKey = keylet::mptoken(mptID, account_); + if (!psb.exists(mptokenKey)) + { + auto sleDst = checkReserve(); + if (sleDst == nullptr) + return tecNO_LINE_INSUF_RESERVE; + + if (auto const err = MPTokenAuthorize::createMPToken( + psb, mptID, account_, 0); + err != tesSUCCESS) + return err; + + adjustOwnerCount(psb, sleDst, 1, j_); + } } + // Make sure the tweaked limits are restored when we leave + // scope. + scope_exit fixup([&psb, &trustLineKey, destLow, &savedLimit]() { + if (trustLineKey) + { + SF_AMOUNT const& tweakedLimit = + destLow ? sfLowLimit : sfHighLimit; + if (auto const sleTrustLine = psb.peek(*trustLineKey)) + sleTrustLine->at(tweakedLimit) = savedLimit; + } + }); // Let flow() do the heavy lifting on a check for an IOU. auto const result = flow( @@ -468,7 +558,8 @@ CashCheck::doApply() << "flow did not produce DeliverMin."; return tecPATH_PARTIAL; } - if (doFix1623 && !checkCashMakesTrustLine) + if (doFix1623 && !checkCashMakesTrustLine && + optDeliverMin->holds()) // Set the delivered_amount metadata. ctx_.deliver(result.actualAmountOut); } diff --git a/src/xrpld/app/tx/detail/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index d60acf9fd73..ffa9e8e51bf 100644 --- a/src/xrpld/app/tx/detail/Clawback.cpp +++ b/src/xrpld/app/tx/detail/Clawback.cpp @@ -122,8 +122,8 @@ preclaimHelper( (issuerFlagsIn & lsfNoFreeze)) return tecNO_PERMISSION; - auto const sleRippleState = - ctx.view.read(keylet::line(holder, issuer, clawAmount.getCurrency())); + auto const sleRippleState = ctx.view.read( + keylet::line(holder, issuer, clawAmount.get().currency)); if (!sleRippleState) return tecNO_LINE; @@ -149,7 +149,7 @@ preclaimHelper( if (accountHolds( ctx.view, holder, - clawAmount.getCurrency(), + clawAmount.get().currency, issuer, fhIGNORE_FREEZE, ctx.j) <= beast::zero) @@ -239,7 +239,7 @@ applyHelper(ApplyContext& ctx) STAmount const spendableAmount = accountHolds( ctx.view(), holder, - clawAmount.getCurrency(), + clawAmount.get().currency, clawAmount.getIssuer(), fhIGNORE_FREEZE, ctx.journal); diff --git a/src/xrpld/app/tx/detail/CreateCheck.cpp b/src/xrpld/app/tx/detail/CreateCheck.cpp index 3a278eed738..ee7eea7c4a2 100644 --- a/src/xrpld/app/tx/detail/CreateCheck.cpp +++ b/src/xrpld/app/tx/detail/CreateCheck.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -34,6 +35,10 @@ CreateCheck::preflight(PreflightContext const& ctx) if (!ctx.rules.enabled(featureChecks)) return temDISABLED; + if (!ctx.rules.enabled(featureMPTokensV2) && + ctx.tx[sfSendMax].holds()) + return temDISABLED; + NotTEC const ret{preflight1(ctx)}; if (!isTesSuccess(ret)) return ret; @@ -60,7 +65,7 @@ CreateCheck::preflight(PreflightContext const& ctx) return temBAD_AMOUNT; } - if (badCurrency() == sendMax.getCurrency()) + if (badCurrency() == sendMax.asset()) { JLOG(ctx.j.warn()) << "Malformed transaction: Bad currency."; return temBAD_CURRENCY; @@ -120,39 +125,53 @@ CreateCheck::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "Creating a check for frozen asset"; return tecFROZEN; } - // If this account has a trustline for the currency, that - // trustline may not be frozen. - // - // Note that we DO allow create check for a currency that the - // account does not yet have a trustline to. - AccountID const srcId{ctx.tx.getAccountID(sfAccount)}; - if (issuerId != srcId) + if (sendMax.holds()) { - // Check if the issuer froze the line - auto const sleTrust = ctx.view.read( - keylet::line(srcId, issuerId, sendMax.getCurrency())); - if (sleTrust && - sleTrust->isFlag( - (issuerId > srcId) ? lsfHighFreeze : lsfLowFreeze)) + // If this account has a trustline for the currency, that + // trustline may not be frozen. + // + // Note that we DO allow create check for a currency that the + // account does not yet have a trustline to. + AccountID const srcId{ctx.tx.getAccountID(sfAccount)}; + if (issuerId != srcId) { - JLOG(ctx.j.warn()) - << "Creating a check for frozen trustline."; - return tecFROZEN; + // Check if the issuer froze the line + auto const sleTrust = ctx.view.read(keylet::line( + srcId, issuerId, sendMax.get().currency)); + if (sleTrust && + sleTrust->isFlag( + (issuerId > srcId) ? lsfHighFreeze : lsfLowFreeze)) + { + JLOG(ctx.j.warn()) + << "Creating a check for frozen trustline."; + return tecFROZEN; + } + } + if (issuerId != dstId) + { + // Check if dst froze the line. + auto const sleTrust = ctx.view.read(keylet::line( + issuerId, dstId, sendMax.get().currency)); + if (sleTrust && + sleTrust->isFlag( + (dstId > issuerId) ? lsfHighFreeze : lsfLowFreeze)) + { + JLOG(ctx.j.warn()) << "Creating a check for " + "destination frozen trustline."; + return tecFROZEN; + } } } - if (issuerId != dstId) + else { - // Check if dst froze the line. - auto const sleTrust = ctx.view.read( - keylet::line(issuerId, dstId, sendMax.getCurrency())); - if (sleTrust && - sleTrust->isFlag( - (dstId > issuerId) ? lsfHighFreeze : lsfLowFreeze)) - { - JLOG(ctx.j.warn()) - << "Creating a check for destination frozen trustline."; + auto const& mptIssue = sendMax.get(); + auto const& srcId = ctx.tx[sfAccount]; + if (srcId != mptIssue.getIssuer() && + isFrozen(ctx.view, srcId, mptIssue)) + return tecFROZEN; + if (dstId != mptIssue.getIssuer() && + isFrozen(ctx.view, dstId, mptIssue)) return tecFROZEN; - } } } } @@ -161,6 +180,15 @@ CreateCheck::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "Creating a check that has already expired."; return tecEXPIRED; } + + if (auto const ter = isMPTTxAllowed( + ctx.view, + ttCHECK_CREATE, + ctx.tx[sfSendMax].asset(), + ctx.tx[sfAccount]); + ter != tesSUCCESS) + return ter; + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index f1b66468840..fd2132c080e 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -18,12 +18,14 @@ //============================================================================== #include +#include #include #include #include #include #include #include +#include #include namespace ripple { @@ -42,6 +44,11 @@ CreateOffer::makeTxConsequences(PreflightContext const& ctx) NotTEC CreateOffer::preflight(PreflightContext const& ctx) { + if (!ctx.rules.enabled(featureMPTokensV2) && + (ctx.tx[sfTakerPays].holds() || + ctx.tx[sfTakerGets].holds())) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -98,18 +105,18 @@ CreateOffer::preflight(PreflightContext const& ctx) } auto const& uPaysIssuerID = saTakerPays.getIssuer(); - auto const& uPaysCurrency = saTakerPays.getCurrency(); + auto const& uPaysAsset = saTakerPays.asset(); auto const& uGetsIssuerID = saTakerGets.getIssuer(); - auto const& uGetsCurrency = saTakerGets.getCurrency(); + auto const& uGetsAsset = saTakerGets.asset(); - if (uPaysCurrency == uGetsCurrency && uPaysIssuerID == uGetsIssuerID) + if (uPaysAsset == uGetsAsset) { JLOG(j.debug()) << "Malformed offer: redundant (IOU for IOU)"; return temREDUNDANT; } // We don't allow a non-native currency to use the currency code XRP. - if (badCurrency() == uPaysCurrency || badCurrency() == uGetsCurrency) + if (badCurrency() == uPaysAsset || badCurrency() == uGetsAsset) { JLOG(j.debug()) << "Malformed offer: bad currency"; return temBAD_CURRENCY; @@ -134,7 +141,7 @@ CreateOffer::preclaim(PreclaimContext const& ctx) auto saTakerGets = ctx.tx[sfTakerGets]; auto const& uPaysIssuerID = saTakerPays.getIssuer(); - auto const& uPaysCurrency = saTakerPays.getCurrency(); + auto const& uPaysAsset = saTakerPays.asset(); auto const& uGetsIssuerID = saTakerGets.getIssuer(); @@ -155,8 +162,22 @@ CreateOffer::preclaim(PreclaimContext const& ctx) return tecFROZEN; } - if (accountFunds(ctx.view, id, saTakerGets, fhZERO_IF_FROZEN, viewJ) <= - beast::zero) + if (auto const ter = + isMPTTxAllowed(ctx.view, ttOFFER_CREATE, saTakerPays.asset(), id); + ter != tesSUCCESS) + return ter; + if (auto const ter = + isMPTTxAllowed(ctx.view, ttOFFER_CREATE, saTakerGets.asset(), id); + ter != tesSUCCESS) + return ter; + + if (accountFunds( + ctx.view, + id, + saTakerGets, + fhZERO_IF_FROZEN, + ahIGNORE_AUTH, + viewJ) <= beast::zero) { JLOG(ctx.j.debug()) << "delay: Offers must be at least partially funded."; @@ -188,12 +209,8 @@ CreateOffer::preclaim(PreclaimContext const& ctx) // Make sure that we are authorized to hold what the taker will pay us. if (!saTakerPays.native()) { - auto result = checkAcceptAsset( - ctx.view, - ctx.flags, - id, - ctx.j, - Issue(uPaysCurrency, uPaysIssuerID)); + auto result = + checkAcceptAsset(ctx.view, ctx.flags, id, ctx.j, uPaysAsset); if (result != tesSUCCESS) return result; } @@ -207,20 +224,20 @@ CreateOffer::checkAcceptAsset( ApplyFlags const flags, AccountID const id, beast::Journal const j, - Issue const& issue) + Asset const& asset) { // Only valid for custom currencies XRPL_ASSERT( - !isXRP(issue.currency), + !isXRP(asset), "ripple::CreateOffer::checkAcceptAsset : input is not XRP"); - auto const issuerAccount = view.read(keylet::account(issue.account)); + auto const issuerAccount = view.read(keylet::account(asset.getIssuer())); if (!issuerAccount) { JLOG(j.debug()) << "delay: can't receive IOUs from non-existent issuer: " - << to_string(issue.account); + << to_string(asset.getIssuer()); return (flags & tapRETRY) ? TER{terNO_ACCOUNT} : TER{tecNO_ISSUER}; } @@ -228,480 +245,72 @@ CreateOffer::checkAcceptAsset( // This code is attached to the DepositPreauth amendment as a matter of // convenience. The change is not significant enough to deserve its // own amendment. - if (view.rules().enabled(featureDepositPreauth) && (issue.account == id)) + if (view.rules().enabled(featureDepositPreauth) && + (asset.getIssuer() == id)) // An account can always accept its own issuance. return tesSUCCESS; - if ((*issuerAccount)[sfFlags] & lsfRequireAuth) + if (asset.holds()) { - auto const trustLine = - view.read(keylet::line(id, issue.account, issue.currency)); - - if (!trustLine) + auto const& issue = asset.get(); + if ((*issuerAccount)[sfFlags] & lsfRequireAuth) { - return (flags & tapRETRY) ? TER{terNO_LINE} : TER{tecNO_LINE}; - } - - // Entries have a canonical representation, determined by a - // lexicographical "greater than" comparison employing strict weak - // ordering. Determine which entry we need to access. - bool const canonical_gt(id > issue.account); - - bool const is_authorized( - (*trustLine)[sfFlags] & (canonical_gt ? lsfLowAuth : lsfHighAuth)); - - if (!is_authorized) - { - JLOG(j.debug()) - << "delay: can't receive IOUs from issuer without auth."; - - return (flags & tapRETRY) ? TER{terNO_AUTH} : TER{tecNO_AUTH}; - } - } - - // An account can not create a trustline to itself, so no line can exist - // to be frozen. Additionally, an issuer can always accept its own - // issuance. - if (issue.account == id) - { - return tesSUCCESS; - } - - auto const trustLine = - view.read(keylet::line(id, issue.account, issue.currency)); - - if (!trustLine) - { - return tesSUCCESS; - } - - // There's no difference which side enacted deep freeze, accepting - // tokens shouldn't be possible. - bool const deepFrozen = - (*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze); - - if (deepFrozen) - { - return tecFROZEN; - } - - return tesSUCCESS; -} - -bool -CreateOffer::dry_offer(ApplyView& view, Offer const& offer) -{ - if (offer.fully_consumed()) - return true; - auto const amount = accountFunds( - view, - offer.owner(), - offer.amount().out, - fhZERO_IF_FROZEN, - ctx_.app.journal("View")); - return (amount <= beast::zero); -} - -std::pair -CreateOffer::select_path( - bool have_direct, - OfferStream const& direct, - bool have_bridge, - OfferStream const& leg1, - OfferStream const& leg2) -{ - // If we don't have any viable path, why are we here?! - XRPL_ASSERT( - have_direct || have_bridge, - "ripple::CreateOffer::select_path : valid inputs"); - - // If there's no bridged path, the direct is the best by default. - if (!have_bridge) - return std::make_pair(true, direct.tip().quality()); - - Quality const bridged_quality( - composed_quality(leg1.tip().quality(), leg2.tip().quality())); - - if (have_direct) - { - // We compare the quality of the composed quality of the bridged - // offers and compare it against the direct offer to pick the best. - Quality const direct_quality(direct.tip().quality()); - - if (bridged_quality < direct_quality) - return std::make_pair(true, direct_quality); - } - - // Either there was no direct offer, or it didn't have a better quality - // than the bridge. - return std::make_pair(false, bridged_quality); -} - -bool -CreateOffer::reachedOfferCrossingLimit(Taker const& taker) const -{ - auto const crossings = - taker.get_direct_crossings() + (2 * taker.get_bridge_crossings()); - - // The crossing limit is part of the Ripple protocol and - // changing it is a transaction-processing change. - return crossings >= 850; -} - -std::pair -CreateOffer::bridged_cross( - Taker& taker, - ApplyView& view, - ApplyView& view_cancel, - NetClock::time_point const when) -{ - auto const& takerAmount = taker.original_offer(); - - XRPL_ASSERT( - !isXRP(takerAmount.in) && !isXRP(takerAmount.out), - "ripple::CreateOffer::bridged_cross : neither is XRP"); - - if (isXRP(takerAmount.in) || isXRP(takerAmount.out)) - Throw("Bridging with XRP and an endpoint."); - - OfferStream offers_direct( - view, - view_cancel, - Book(taker.issue_in(), taker.issue_out()), - when, - stepCounter_, - j_); - - OfferStream offers_leg1( - view, - view_cancel, - Book(taker.issue_in(), xrpIssue()), - when, - stepCounter_, - j_); - - OfferStream offers_leg2( - view, - view_cancel, - Book(xrpIssue(), taker.issue_out()), - when, - stepCounter_, - j_); - - TER cross_result = tesSUCCESS; - - // Note the subtle distinction here: self-offers encountered in the - // bridge are taken, but self-offers encountered in the direct book - // are not. - bool have_bridge = offers_leg1.step() && offers_leg2.step(); - bool have_direct = step_account(offers_direct, taker); - int count = 0; - - auto viewJ = ctx_.app.journal("View"); + auto const trustLine = + view.read(keylet::line(id, asset.getIssuer(), issue.currency)); - // Modifying the order or logic of the operations in the loop will cause - // a protocol breaking change. - while (have_direct || have_bridge) - { - bool leg1_consumed = false; - bool leg2_consumed = false; - bool direct_consumed = false; - - auto const [use_direct, quality] = select_path( - have_direct, offers_direct, have_bridge, offers_leg1, offers_leg2); - - // We are always looking at the best quality; we are done with - // crossing as soon as we cross the quality boundary. - if (taker.reject(quality)) - break; - - count++; - - if (use_direct) - { - if (auto stream = j_.debug()) + if (!trustLine) { - stream << count << " Direct:"; - stream << " offer: " << offers_direct.tip(); - stream << " in: " << offers_direct.tip().amount().in; - stream << " out: " << offers_direct.tip().amount().out; - stream << " owner: " << offers_direct.tip().owner(); - stream << " funds: " - << accountFunds( - view, - offers_direct.tip().owner(), - offers_direct.tip().amount().out, - fhIGNORE_FREEZE, - viewJ); + return (flags & tapRETRY) ? TER{terNO_LINE} : TER{tecNO_LINE}; } - cross_result = taker.cross(offers_direct.tip()); + // Entries have a canonical representation, determined by a + // lexicographical "greater than" comparison employing strict weak + // ordering. Determine which entry we need to access. + bool const canonical_gt(id > asset.getIssuer()); - JLOG(j_.debug()) << "Direct Result: " << transToken(cross_result); + bool const is_authorized( + (*trustLine)[sfFlags] & + (canonical_gt ? lsfLowAuth : lsfHighAuth)); - if (dry_offer(view, offers_direct.tip())) - { - direct_consumed = true; - have_direct = step_account(offers_direct, taker); - } - } - else - { - if (auto stream = j_.debug()) + if (!is_authorized) { - auto const owner1_funds_before = accountFunds( - view, - offers_leg1.tip().owner(), - offers_leg1.tip().amount().out, - fhIGNORE_FREEZE, - viewJ); - - auto const owner2_funds_before = accountFunds( - view, - offers_leg2.tip().owner(), - offers_leg2.tip().amount().out, - fhIGNORE_FREEZE, - viewJ); - - stream << count << " Bridge:"; - stream << " offer1: " << offers_leg1.tip(); - stream << " in: " << offers_leg1.tip().amount().in; - stream << " out: " << offers_leg1.tip().amount().out; - stream << " owner: " << offers_leg1.tip().owner(); - stream << " funds: " << owner1_funds_before; - stream << " offer2: " << offers_leg2.tip(); - stream << " in: " << offers_leg2.tip().amount().in; - stream << " out: " << offers_leg2.tip().amount().out; - stream << " owner: " << offers_leg2.tip().owner(); - stream << " funds: " << owner2_funds_before; - } - - cross_result = taker.cross(offers_leg1.tip(), offers_leg2.tip()); + JLOG(j.debug()) + << "delay: can't receive IOUs from issuer without auth."; - JLOG(j_.debug()) << "Bridge Result: " << transToken(cross_result); - - if (view.rules().enabled(fixTakerDryOfferRemoval)) - { - // have_bridge can be true the next time 'round only if - // neither of the OfferStreams are dry. - leg1_consumed = dry_offer(view, offers_leg1.tip()); - if (leg1_consumed) - have_bridge &= offers_leg1.step(); - - leg2_consumed = dry_offer(view, offers_leg2.tip()); - if (leg2_consumed) - have_bridge &= offers_leg2.step(); - } - else - { - // This old behavior may leave an empty offer in the book for - // the second leg. - if (dry_offer(view, offers_leg1.tip())) - { - leg1_consumed = true; - have_bridge = (have_bridge && offers_leg1.step()); - } - if (dry_offer(view, offers_leg2.tip())) - { - leg2_consumed = true; - have_bridge = (have_bridge && offers_leg2.step()); - } + return (flags & tapRETRY) ? TER{terNO_AUTH} : TER{tecNO_AUTH}; } } - if (cross_result != tesSUCCESS) - { - cross_result = tecFAILED_PROCESSING; - break; - } - - if (taker.done()) - { - JLOG(j_.debug()) << "The taker reports he's done during crossing!"; - break; - } - - if (reachedOfferCrossingLimit(taker)) - { - JLOG(j_.debug()) << "The offer crossing limit has been exceeded!"; - break; - } - - // Postcondition: If we aren't done, then we *must* have consumed at - // least one offer fully. - XRPL_ASSERT( - direct_consumed || leg1_consumed || leg2_consumed, - "ripple::CreateOffer::bridged_cross : consumed an offer"); - - if (!direct_consumed && !leg1_consumed && !leg2_consumed) - Throw( - "bridged crossing: nothing was fully consumed."); - } - - return std::make_pair(cross_result, taker.remaining_offer()); -} - -std::pair -CreateOffer::direct_cross( - Taker& taker, - ApplyView& view, - ApplyView& view_cancel, - NetClock::time_point const when) -{ - OfferStream offers( - view, - view_cancel, - Book(taker.issue_in(), taker.issue_out()), - when, - stepCounter_, - j_); - - TER cross_result(tesSUCCESS); - int count = 0; - - bool have_offer = step_account(offers, taker); - - // Modifying the order or logic of the operations in the loop will cause - // a protocol breaking change. - while (have_offer) - { - bool direct_consumed = false; - auto& offer(offers.tip()); - - // We are done with crossing as soon as we cross the quality boundary - if (taker.reject(offer.quality())) - break; - - count++; - - if (auto stream = j_.debug()) + // An account can not create a trustline to itself, so no line can exist + // to be frozen. Additionally, an issuer can always accept its own + // issuance. + if (issue.account == id) { - stream << count << " Direct:"; - stream << " offer: " << offer; - stream << " in: " << offer.amount().in; - stream << " out: " << offer.amount().out; - stream << "quality: " << offer.quality(); - stream << " owner: " << offer.owner(); - stream << " funds: " - << accountFunds( - view, - offer.owner(), - offer.amount().out, - fhIGNORE_FREEZE, - ctx_.app.journal("View")); + return tesSUCCESS; } - cross_result = taker.cross(offer); - - JLOG(j_.debug()) << "Direct Result: " << transToken(cross_result); - - if (dry_offer(view, offer)) - { - direct_consumed = true; - have_offer = step_account(offers, taker); - } + auto const trustLine = + view.read(keylet::line(id, issue.account, issue.currency)); - if (cross_result != tesSUCCESS) + if (!trustLine) { - cross_result = tecFAILED_PROCESSING; - break; + return tesSUCCESS; } - if (taker.done()) - { - JLOG(j_.debug()) << "The taker reports he's done during crossing!"; - break; - } + // There's no difference which side enacted deep freeze, accepting + // tokens shouldn't be possible. + bool const deepFrozen = + (*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze); - if (reachedOfferCrossingLimit(taker)) + if (deepFrozen) { - JLOG(j_.debug()) << "The offer crossing limit has been exceeded!"; - break; + return tecFROZEN; } - // Postcondition: If we aren't done, then we *must* have consumed the - // offer on the books fully! - XRPL_ASSERT( - direct_consumed, - "ripple::CreateOffer::direct_cross : consumed an offer"); - - if (!direct_consumed) - Throw( - "direct crossing: nothing was fully consumed."); - } - - return std::make_pair(cross_result, taker.remaining_offer()); -} - -// Step through the stream for as long as possible, skipping any offers -// that are from the taker or which cross the taker's threshold. -// Return false if the is no offer in the book, true otherwise. -bool -CreateOffer::step_account(OfferStream& stream, Taker const& taker) -{ - while (stream.step()) - { - auto const& offer = stream.tip(); - - // This offer at the tip crosses the taker's threshold. We're done. - if (taker.reject(offer.quality())) - return true; - - // This offer at the tip is not from the taker. We're done. - if (offer.owner() != taker.account()) - return true; - } - - // We ran out of offers. Can't advance. - return false; -} - -// Fill as much of the offer as possible by consuming offers -// already on the books. Return the status and the amount of -// the offer to left unfilled. -std::pair -CreateOffer::takerCross( - Sandbox& sb, - Sandbox& sbCancel, - Amounts const& takerAmount) -{ - NetClock::time_point const when = sb.parentCloseTime(); - - beast::WrappedSink takerSink(j_, "Taker "); - - Taker taker( - cross_type_, - sb, - account_, - takerAmount, - ctx_.tx.getFlags(), - beast::Journal(takerSink)); - - // If the taker is unfunded before we begin crossing - // there's nothing to do - just return an error. - // - // We check this in preclaim, but when selling XRP - // charged fees can cause a user's available balance - // to go to 0 (by causing it to dip below the reserve) - // so we check this case again. - if (taker.unfunded()) - { - JLOG(j_.debug()) << "Not crossing: taker is unfunded."; - return {tecUNFUNDED_OFFER, takerAmount}; - } - - try - { - if (cross_type_ == CrossType::IouToIou) - return bridged_cross(taker, sb, sbCancel, when); - - return direct_cross(taker, sb, sbCancel, when); - } - catch (std::exception const& e) - { - JLOG(j_.error()) << "Exception during offer crossing: " << e.what(); - return {tecINTERNAL, taker.remaining_offer()}; + return tesSUCCESS; } + else + return requireAuth(view, asset.get(), id); } std::pair @@ -718,8 +327,8 @@ CreateOffer::flowCross( // We check this in preclaim, but when selling XRP charged fees can // cause a user's available balance to go to 0 (by causing it to dip // below the reserve) so we check this case again. - STAmount const inStartBalance = - accountFunds(psb, account_, takerAmount.in, fhZERO_IF_FROZEN, j_); + STAmount const inStartBalance = accountFunds( + psb, account_, takerAmount.in, fhZERO_IF_FROZEN, ahIGNORE_AUTH, j_); if (inStartBalance <= beast::zero) { // The account balance can't cover even part of the offer. @@ -734,13 +343,19 @@ CreateOffer::flowCross( STAmount sendMax = takerAmount.in; if (!sendMax.native() && (account_ != sendMax.getIssuer())) { - gatewayXferRate = transferRate(psb, sendMax.getIssuer()); + gatewayXferRate = [&]() { + if (sendMax.holds()) + return transferRate(psb, sendMax.getIssuer()); + else + return transferRate( + psb, sendMax.get().getMptID()); + }(); if (gatewayXferRate.value != QUALITY_ONE) { sendMax = multiplyRound( takerAmount.in, gatewayXferRate, - takerAmount.in.issue(), + takerAmount.in.asset(), true); } } @@ -781,13 +396,15 @@ CreateOffer::flowCross( // we allow delivery of the largest possible amount. if (deliver.native()) deliver = STAmount{STAmount::cMaxNative}; + // We can't use the maximum possible currency here because + // there might be a gateway transfer rate to account for. + // Since the transfer rate cannot exceed 200%, we use 1/2 + // maxValue for our limit. + else if (deliver.holds()) + deliver = STAmount{deliver.asset(), maxMPTokenAmount / 2}; else - // We can't use the maximum possible currency here because - // there might be a gateway transfer rate to account for. - // Since the transfer rate cannot exceed 200%, we use 1/2 - // maxValue for our limit. deliver = STAmount{ - takerAmount.out.issue(), + takerAmount.out.asset(), STAmount::cMaxValue / 2, STAmount::cMaxOffset}; } @@ -821,7 +438,12 @@ CreateOffer::flowCross( if (isTesSuccess(result.result())) { STAmount const takerInBalance = accountFunds( - psb, account_, takerAmount.in, fhZERO_IF_FROZEN, j_); + psb, + account_, + takerAmount.in, + fhZERO_IF_FROZEN, + ahIGNORE_AUTH, + j_); if (takerInBalance <= beast::zero) { @@ -850,7 +472,7 @@ CreateOffer::flowCross( nonGatewayAmountIn = divideRound( result.actualAmountIn, gatewayXferRate, - takerAmount.in.issue(), + takerAmount.in.asset(), true); afterCross.in -= nonGatewayAmountIn; @@ -872,11 +494,11 @@ CreateOffer::flowCross( return divRoundStrict( afterCross.in, rate, - takerAmount.out.issue(), + takerAmount.out.asset(), false); return divRound( - afterCross.in, rate, takerAmount.out.issue(), true); + afterCross.in, rate, takerAmount.out.asset(), true); }(); } else @@ -891,7 +513,7 @@ CreateOffer::flowCross( if (afterCross.out < beast::zero) afterCross.out.clear(); afterCross.in = mulRound( - afterCross.out, rate, takerAmount.in.issue(), true); + afterCross.out, rate, takerAmount.in.asset(), true); } } } @@ -909,21 +531,11 @@ CreateOffer::flowCross( std::pair CreateOffer::cross(Sandbox& sb, Sandbox& sbCancel, Amounts const& takerAmount) { - if (sb.rules().enabled(featureFlowCross)) - { - PaymentSandbox psbFlow{&sb}; - PaymentSandbox psbCancelFlow{&sbCancel}; - auto const ret = flowCross(psbFlow, psbCancelFlow, takerAmount); - psbFlow.apply(sb); - psbCancelFlow.apply(sbCancel); - return ret; - } - - Sandbox sbTaker{&sb}; - Sandbox sbCancelTaker{&sbCancel}; - auto const ret = takerCross(sbTaker, sbCancelTaker, takerAmount); - sbTaker.apply(sb); - sbCancelTaker.apply(sbCancel); + PaymentSandbox psbFlow{&sb}; + PaymentSandbox psbCancelFlow{&sbCancel}; + auto const ret = flowCross(psbFlow, psbCancelFlow, takerAmount); + psbFlow.apply(sb); + psbCancelFlow.apply(sbCancel); return ret; } @@ -932,24 +544,13 @@ CreateOffer::format_amount(STAmount const& amount) { std::string txt = amount.getText(); txt += "/"; - txt += to_string(amount.issue().currency); + if (amount.holds()) + txt += to_string(amount.get().currency); + else + txt += to_string(amount.get()); return txt; } -void -CreateOffer::preCompute() -{ - cross_type_ = CrossType::IouToIou; - bool const pays_xrp = ctx_.tx.getFieldAmount(sfTakerPays).native(); - bool const gets_xrp = ctx_.tx.getFieldAmount(sfTakerGets).native(); - if (pays_xrp && !gets_xrp) - cross_type_ = CrossType::IouToXrp; - else if (gets_xrp && !pays_xrp) - cross_type_ = CrossType::XrpToIou; - - return Transactor::preCompute(); -} - std::pair CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) { @@ -1044,12 +645,12 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) if (bSell) { // this is a sell, round taker pays - saTakerPays = multiply(saTakerGets, rate, saTakerPays.issue()); + saTakerPays = multiply(saTakerGets, rate, saTakerPays.asset()); } else { // this is a buy, round taker gets - saTakerGets = divide(saTakerPays, rate, saTakerGets.issue()); + saTakerGets = divide(saTakerPays, rate, saTakerGets.asset()); } if (!saTakerGets || !saTakerPays) { @@ -1069,8 +670,8 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) Amounts place_offer; JLOG(j_.debug()) << "Attempting cross: " - << to_string(takerAmount.in.issue()) << " -> " - << to_string(takerAmount.out.issue()); + << to_string(takerAmount.in.asset()) << " -> " + << to_string(takerAmount.out.asset()); if (auto stream = j_.trace()) { @@ -1106,10 +707,10 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) } XRPL_ASSERT( - saTakerGets.issue() == place_offer.in.issue(), + saTakerGets.asset() == place_offer.in.asset(), "ripple::CreateOffer::applyGuts : taker gets issue match"); XRPL_ASSERT( - saTakerPays.issue() == place_offer.out.issue(), + saTakerPays.asset() == place_offer.out.asset(), "ripple::CreateOffer::applyGuts : taker pays issue match"); if (takerAmount != place_offer) @@ -1221,10 +822,10 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) // Update owner count. adjustOwnerCount(sb, sleCreator, 1, viewJ); - JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.issue()) - << " : " << to_string(saTakerGets.issue()); + JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.asset()) + << " : " << to_string(saTakerGets.asset()); - Book const book{saTakerPays.issue(), saTakerGets.issue()}; + Book const book{saTakerPays.asset(), saTakerGets.asset()}; // Add offer to order book, using the original rate // before any crossing occured. @@ -1232,10 +833,30 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) bool const bookExisted = static_cast(sb.peek(dir)); auto const bookNode = sb.dirAppend(dir, offer_index, [&](SLE::ref sle) { - sle->setFieldH160(sfTakerPaysCurrency, saTakerPays.issue().currency); - sle->setFieldH160(sfTakerPaysIssuer, saTakerPays.issue().account); - sle->setFieldH160(sfTakerGetsCurrency, saTakerGets.issue().currency); - sle->setFieldH160(sfTakerGetsIssuer, saTakerGets.issue().account); + if (saTakerPays.holds()) + { + sle->setFieldH160( + sfTakerPaysCurrency, saTakerPays.get().currency); + sle->setFieldH160( + sfTakerPaysIssuer, saTakerPays.get().account); + } + else + { + sle->setFieldH192( + sfTakerPaysMPT, saTakerPays.get().getMptID()); + } + if (saTakerGets.holds()) + { + sle->setFieldH160( + sfTakerGetsCurrency, saTakerGets.get().currency); + sle->setFieldH160( + sfTakerGetsIssuer, saTakerGets.get().account); + } + else + { + sle->setFieldH192( + sfTakerGetsMPT, saTakerGets.get().getMptID()); + } sle->setFieldU64(sfExchangeRate, uRate); }); diff --git a/src/xrpld/app/tx/detail/CreateOffer.h b/src/xrpld/app/tx/detail/CreateOffer.h index 234267804c9..5f130c29785 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.h +++ b/src/xrpld/app/tx/detail/CreateOffer.h @@ -21,7 +21,6 @@ #define RIPPLE_TX_CREATEOFFER_H_INCLUDED #include -#include #include #include @@ -37,8 +36,7 @@ class CreateOffer : public Transactor static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; /** Construct a Transactor subclass that creates an offer in the ledger. */ - explicit CreateOffer(ApplyContext& ctx) - : Transactor(ctx), stepCounter_(1000, j_) + explicit CreateOffer(ApplyContext& ctx) : Transactor(ctx) { } @@ -53,10 +51,6 @@ class CreateOffer : public Transactor static TER preclaim(PreclaimContext const& ctx); - /** Gather information beyond what the Transactor base class gathers. */ - void - preCompute() override; - /** Precondition: fee collection is likely. Attempt to create the offer. */ TER doApply() override; @@ -72,50 +66,7 @@ class CreateOffer : public Transactor ApplyFlags const flags, AccountID const id, beast::Journal const j, - Issue const& issue); - - bool - dry_offer(ApplyView& view, Offer const& offer); - - static std::pair - select_path( - bool have_direct, - OfferStream const& direct, - bool have_bridge, - OfferStream const& leg1, - OfferStream const& leg2); - - std::pair - bridged_cross( - Taker& taker, - ApplyView& view, - ApplyView& view_cancel, - NetClock::time_point const when); - - std::pair - direct_cross( - Taker& taker, - ApplyView& view, - ApplyView& view_cancel, - NetClock::time_point const when); - - // Step through the stream for as long as possible, skipping any offers - // that are from the taker or which cross the taker's threshold. - // Return false if the is no offer in the book, true otherwise. - static bool - step_account(OfferStream& stream, Taker const& taker); - - // True if the number of offers that have been crossed - // exceeds the limit. - bool - reachedOfferCrossingLimit(Taker const& taker) const; - - // Fill offer as much as possible by consuming offers already on the books, - // and adjusting account balances accordingly. - // - // Charges fees on top to taker. - std::pair - takerCross(Sandbox& sb, Sandbox& sbCancel, Amounts const& takerAmount); + Asset const& asset); // Use the payment flow code to perform offer crossing. std::pair @@ -133,13 +84,6 @@ class CreateOffer : public Transactor static std::string format_amount(STAmount const& amount); - -private: - // What kind of offer we are placing - CrossType cross_type_; - - // The number of steps to take through order books while crossing - OfferStream::StepCounter stepCounter_; }; using OfferCreate = CreateOffer; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index d39492c1085..37f878566d3 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -387,6 +387,7 @@ AccountRootsDeletedClean::finalize( view.rules().enabled(featureInvariantsV1_1); auto const objectExists = [&view, enforce, &j](auto const& keylet) { + (void)enforce; if (auto const sle = view.read(keylet)) { // Finding the object is bad @@ -534,8 +535,8 @@ NoXRPTrustLines::visitEntry( // relying on .native() just in case native somehow // were systematically incorrect xrpTrustLine_ = - after->getFieldAmount(sfLowLimit).issue() == xrpIssue() || - after->getFieldAmount(sfHighLimit).issue() == xrpIssue(); + after->getFieldAmount(sfLowLimit).asset() == xrpIssue() || + after->getFieldAmount(sfHighLimit).asset() == xrpIssue(); } } @@ -756,7 +757,7 @@ TransfersNotFrozen::recordBalanceChanges( STAmount const& balanceChange) { auto const balanceChangeSign = balanceChange.signum(); - auto const currency = after->at(sfBalance).getCurrency(); + auto const currency = after->at(sfBalance).get().currency; // Change from low account's perspective, which is trust line default recordBalance( @@ -1240,18 +1241,33 @@ ValidClawback::finalize( return false; } - if (trustlinesChanged == 1) + if (trustlinesChanged == 1 || mptokensChanged == 1) { AccountID const issuer = tx.getAccountID(sfAccount); STAmount const& amount = tx.getFieldAmount(sfAmount); AccountID const& holder = amount.getIssuer(); - STAmount const holderBalance = accountHolds( - view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j); + STAmount const holderBalance = [&]() { + if (amount.holds()) + return accountHolds( + view, + holder, + amount.get().currency, + issuer, + fhIGNORE_FREEZE, + j); + return accountHolds( + view, + issuer, + amount.get(), + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + j); + }(); if (holderBalance.signum() < 0) { JLOG(j.fatal()) - << "Invariant failed: trustline balance is negative"; + << "Invariant failed: trustline or MPT balance is negative"; return false; } } @@ -1311,7 +1327,8 @@ ValidMPTIssuance::finalize( { if (result == tesSUCCESS) { - if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_CREATE) + auto const txnType = tx.getTxnType(); + if (txnType == ttMPTOKEN_ISSUANCE_CREATE) { if (mptIssuancesCreated_ == 0) { @@ -1332,7 +1349,7 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0; } - if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_DESTROY) + if (txnType == ttMPTOKEN_ISSUANCE_DESTROY) { if (mptIssuancesDeleted_ == 0) { @@ -1353,7 +1370,7 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1; } - if (tx.getTxnType() == ttMPTOKEN_AUTHORIZE) + if (txnType == ttMPTOKEN_AUTHORIZE) { bool const submittedByIssuer = tx.isFieldPresent(sfHolder); @@ -1393,7 +1410,7 @@ ValidMPTIssuance::finalize( return true; } - if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_SET) + if (txnType == ttMPTOKEN_ISSUANCE_SET) { if (mptIssuancesDeleted_ > 0) { @@ -1419,6 +1436,73 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensCreated_ == 0 && mptokensDeleted_ == 0; } + + if (txnType == ttAMM_CREATE || txnType == ttCHECK_CASH) + { + if (mptIssuancesDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while removing MPT issuances"; + } + else if (mptIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while creating MPT issuances"; + } + else if (mptokensDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while removing MPTokens"; + } + // AMM can be created with IOU/MPT or MPT/MPT + else if ( + (txnType == ttAMM_CREATE && mptokensCreated_ > 2) || + (txnType == ttCHECK_CASH && mptokensCreated_ > 1)) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while creating MPTokens"; + } + + return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && + ((txnType == ttAMM_CREATE && mptokensCreated_ <= 2) || + (txnType == ttCHECK_CASH && mptokensCreated_ <= 1)) && + mptokensDeleted_ == 0; + } + + if (txnType == ttAMM_DELETE || txnType == ttAMM_WITHDRAW || + txnType == ttAMM_CLAWBACK) + { + if (mptIssuancesDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while removing MPT issuances"; + } + else if (mptIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while creating MPT issuances"; + } + else if (mptokensDeleted_ > 2) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while removing MPTokens"; + } + // MPToken can be created if LP withdraws from MPT pool, + // and doesn't own MPToken object for this MPT + else if ( + (txnType == ttAMM_WITHDRAW || txnType == ttAMM_CLAWBACK) && + mptokensCreated_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while creating MPTokens"; + } + + return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && + ((txnType == ttAMM_DELETE && mptokensCreated_ == 0) || + ((txnType == ttAMM_WITHDRAW || txnType == ttAMM_CLAWBACK) && + mptokensCreated_ <= 1)) && + mptokensDeleted_ <= 2; + } } if (mptIssuancesCreated_ != 0) diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index 8042c9c6982..dd4d9bb8235 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -135,6 +135,32 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +TER +MPTokenAuthorize::createMPToken( + ApplyView& view, + MPTID const& mptIssuanceID, + AccountID const& account, + std::uint32_t flags) +{ + auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); + + auto const ownerNode = view.dirInsert( + keylet::ownerDir(account), mptokenKey, describeOwnerDir(account)); + + if (!ownerNode) + return tecDIR_FULL; + + auto mptoken = std::make_shared(mptokenKey); + (*mptoken)[sfAccount] = account; + (*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID; + (*mptoken)[sfFlags] = flags; + (*mptoken)[sfOwnerNode] = *ownerNode; + + view.insert(mptoken); + + return tesSUCCESS; +} + TER MPTokenAuthorize::authorize( ApplyView& view, @@ -191,23 +217,10 @@ MPTokenAuthorize::authorize( if (args.priorBalance < reserveCreate) return tecINSUFFICIENT_RESERVE; - auto const mptokenKey = - keylet::mptoken(args.mptIssuanceID, args.account); - - auto const ownerNode = view.dirInsert( - keylet::ownerDir(args.account), - mptokenKey, - describeOwnerDir(args.account)); - - if (!ownerNode) - return tecDIR_FULL; - - auto mptoken = std::make_shared(mptokenKey); - (*mptoken)[sfAccount] = args.account; - (*mptoken)[sfMPTokenIssuanceID] = args.mptIssuanceID; - (*mptoken)[sfFlags] = 0; - (*mptoken)[sfOwnerNode] = *ownerNode; - view.insert(mptoken); + if (auto const ter = + createMPToken(view, args.mptIssuanceID, args.account, 0); + ter != tesSUCCESS) + return ter; // Update owner count. adjustOwnerCount(view, sleAcct, 1, journal); diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index 79dc1734b5b..4c57797260c 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; @@ -54,6 +54,13 @@ class MPTokenAuthorize : public Transactor beast::Journal journal, MPTAuthorizeArgs const& args); + static TER + createMPToken( + ApplyView& view, + MPTID const& mptIssuanceID, + AccountID const& account, + std::uint32_t flags); + TER doApply() override; }; diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp index 9ae6616e382..c7a5f58a28c 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp @@ -104,7 +104,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) return tecNFTOKEN_BUY_SELL_MISMATCH; // The two offers being brokered must be for the same asset: - if ((*bo)[sfAmount].issue() != (*so)[sfAmount].issue()) + if ((*bo)[sfAmount].asset() != (*so)[sfAmount].asset()) return tecNFTOKEN_BUY_SELL_MISMATCH; // The two offers may not form a loop. A broker may not sell the @@ -152,7 +152,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) // cut, if any). if (auto const brokerFee = ctx.tx[~sfNFTokenBrokerFee]) { - if (brokerFee->issue() != (*bo)[sfAmount].issue()) + if (brokerFee->asset() != (*bo)[sfAmount].asset()) return tecNFTOKEN_BUY_SELL_MISMATCH; if (brokerFee >= (*bo)[sfAmount]) @@ -203,7 +203,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) accountHolds( ctx.view, (*bo)[sfOwner], - needed.getCurrency(), + needed.get().currency, needed.getIssuer(), fhZERO_IF_FROZEN, ctx.j) < needed) @@ -240,7 +240,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) if (accountHolds( ctx.view, ctx.tx[sfAccount], - needed.getCurrency(), + needed.get().currency, needed.getIssuer(), fhZERO_IF_FROZEN, ctx.j) < needed) @@ -302,7 +302,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) auto const issuer = nft::getIssuer(tokenID); // Issuer doesn't need a trust line to accept their own currency. if (issuer != amount.getIssuer() && - !ctx.view.read(keylet::line(issuer, amount.issue()))) + !ctx.view.read(keylet::line(issuer, amount.get()))) return tecNO_LINE; } } diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.cpp b/src/xrpld/app/tx/detail/NFTokenUtils.cpp index 04eb53ae764..465694653ce 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.cpp +++ b/src/xrpld/app/tx/detail/NFTokenUtils.cpp @@ -915,15 +915,19 @@ tokenOfferCreatePreclaim( if (view.rules().enabled(featureNFTokenMintOffer)) { if (nftIssuer != amount.getIssuer() && - !view.read(keylet::line(nftIssuer, amount.issue()))) + !view.read(keylet::line(nftIssuer, amount.get()))) return tecNO_LINE; } - else if (!view.exists(keylet::line(nftIssuer, amount.issue()))) + else if (!view.exists(keylet::line(nftIssuer, amount.get()))) { return tecNO_LINE; } - if (isFrozen(view, nftIssuer, amount.getCurrency(), amount.getIssuer())) + if (isFrozen( + view, + nftIssuer, + amount.get().currency, + amount.getIssuer())) return tecFROZEN; } @@ -937,7 +941,8 @@ tokenOfferCreatePreclaim( return tefNFTOKEN_IS_NOT_TRANSFERABLE; } - if (isFrozen(view, acctID, amount.getCurrency(), amount.getIssuer())) + if (isFrozen( + view, acctID, amount.get().currency, amount.getIssuer())) return tecFROZEN; // If this is an offer to buy the token, the account must have the @@ -958,7 +963,7 @@ tokenOfferCreatePreclaim( accountHolds( view, acctID, - amount.getCurrency(), + amount.get().currency, amount.getIssuer(), FreezeHandling::fhZERO_IF_FROZEN, j) diff --git a/src/xrpld/app/tx/detail/Offer.h b/src/xrpld/app/tx/detail/Offer.h index 23129952c3d..38eb36b1804 100644 --- a/src/xrpld/app/tx/detail/Offer.h +++ b/src/xrpld/app/tx/detail/Offer.h @@ -35,8 +35,8 @@ template class TOfferBase { protected: - Issue issIn_; - Issue issOut_; + Asset assetIn_; + Asset assetOut_; }; template <> @@ -132,10 +132,10 @@ class TOffer : private TOfferBase return m_entry->key(); } - Issue const& - issueIn() const; - Issue const& - issueOut() const; + Asset const& + assetIn() const; + Asset const& + assetOut() const; TAmounts limitOut( @@ -155,7 +155,7 @@ class TOffer : private TOfferBase isFunded() const { // Offer owner is issuer; they have unlimited funds - return m_account == issueOut().account; + return m_account == assetOut().getIssuer(); } static std::pair @@ -187,8 +187,8 @@ TOffer::TOffer(SLE::pointer const& entry, Quality quality) auto const tg = m_entry->getFieldAmount(sfTakerGets); m_amounts.in = toAmount(tp); m_amounts.out = toAmount(tg); - this->issIn_ = tp.issue(); - this->issOut_ = tg.issue(); + this->assetIn_ = tp.asset(); + this->assetOut_ = tg.asset(); } template <> @@ -208,11 +208,21 @@ template void TOffer::setFieldAmounts() { -#ifdef _MSC_VER - UNREACHABLE("ripple::TOffer::setFieldAmounts : must be specialized"); -#else - static_assert(sizeof(TOut) == -1, "Must be specialized"); -#endif + if constexpr (std::is_same_v) + m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in)); + else if constexpr (std::is_same_v) + m_entry->setFieldAmount(sfTakerPays, m_amounts.in); + else + m_entry->setFieldAmount( + sfTakerPays, toSTAmount(m_amounts.in, assetIn())); + + if constexpr (std::is_same_v) + m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out)); + else if constexpr (std::is_same_v) + m_entry->setFieldAmount(sfTakerGets, m_amounts.out); + else + m_entry->setFieldAmount( + sfTakerGets, toSTAmount(m_amounts.out, assetOut())); } template @@ -257,64 +267,32 @@ TOffer::send(Args&&... args) return accountSend(std::forward(args)...); } -template <> -inline void -TOffer::setFieldAmounts() -{ - m_entry->setFieldAmount(sfTakerPays, m_amounts.in); - m_entry->setFieldAmount(sfTakerGets, m_amounts.out); -} - -template <> -inline void -TOffer::setFieldAmounts() -{ - m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in, issIn_)); - m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out, issOut_)); -} - -template <> -inline void -TOffer::setFieldAmounts() -{ - m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in, issIn_)); - m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out)); -} - -template <> -inline void -TOffer::setFieldAmounts() -{ - m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in)); - m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out, issOut_)); -} - template -Issue const& -TOffer::issueIn() const +Asset const& +TOffer::assetIn() const { - return this->issIn_; + return this->assetIn_; } template <> -inline Issue const& -TOffer::issueIn() const +inline Asset const& +TOffer::assetIn() const { - return m_amounts.in.issue(); + return m_amounts.in.asset(); } template -Issue const& -TOffer::issueOut() const +Asset const& +TOffer::assetOut() const { - return this->issOut_; + return this->assetOut_; } template <> -inline Issue const& -TOffer::issueOut() const +inline Asset const& +TOffer::assetOut() const { - return m_amounts.out.issue(); + return m_amounts.out.asset(); } template diff --git a/src/xrpld/app/tx/detail/OfferStream.cpp b/src/xrpld/app/tx/detail/OfferStream.cpp index 4e1cdd9b238..98a9d8372bb 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& asset) -> bool { + return isXRP(asset.getIssuer()) || + view.read(keylet::account(asset.getIssuer())); }; return issuerExists(view, book.in) && issuerExists(view, book.out); } @@ -92,68 +93,41 @@ TOfferStreamBase::erase(ApplyView& view) << " removed from directory " << tip_.dir(); } -static STAmount +template +static T accountFundsHelper( ReadView const& view, AccountID const& id, - STAmount const& saDefault, - Issue const&, + T const& amtDefault, + Asset const& asset, FreezeHandling freezeHandling, + AuthHandling authHandling, beast::Journal j) { - return accountFunds(view, id, saDefault, freezeHandling, j); -} - -static IOUAmount -accountFundsHelper( - ReadView const& view, - AccountID const& id, - IOUAmount const& amtDefault, - Issue const& issue, - FreezeHandling freezeHandling, - beast::Journal j) -{ - if (issue.account == id) - // self funded - return amtDefault; - - return toAmount(accountHolds( - view, id, issue.currency, issue.account, freezeHandling, j)); -} + if constexpr (std::is_same_v) + return accountFunds( + view, id, amtDefault, freezeHandling, authHandling, j); + else + { + if constexpr ( + std::is_same_v || std::is_same_v) + { + if (asset.getIssuer() == id) + // self funded + return amtDefault; + } -static XRPAmount -accountFundsHelper( - ReadView const& view, - AccountID const& id, - XRPAmount const& amtDefault, - Issue const& issue, - FreezeHandling freezeHandling, - beast::Journal j) -{ - return toAmount(accountHolds( - view, id, issue.currency, issue.account, freezeHandling, j)); + return toAmount( + accountHolds(view, id, asset, freezeHandling, authHandling, j)); + } } template template + requires ValidTaker bool TOfferStreamBase::shouldRmSmallIncreasedQOffer() const { - static_assert( - std::is_same_v || - std::is_same_v, - "STAmount is not supported"); - - static_assert( - std::is_same_v || - std::is_same_v, - "STAmount is not supported"); - - static_assert( - !std::is_same_v || - !std::is_same_v, - "Cannot have XRP/XRP offers"); - if (!view_.rules().enabled(fixRmSmallIncreasedQOffers)) return false; @@ -178,7 +152,7 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const if constexpr (!inIsXRP && !outIsXRP) { - if (ofrAmts.in >= ofrAmts.out) + if (Number(ofrAmts.in) >= Number(ofrAmts.out)) return false; } @@ -186,7 +160,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. @@ -273,18 +247,21 @@ TOfferStreamBase::step() continue; } - bool const deepFrozen = isDeepFrozen( - view_, - offer_.owner(), - offer_.issueIn().currency, - offer_.issueIn().account); - if (deepFrozen) + if (offer_.assetIn().template holds()) { - JLOG(j_.trace()) - << "Removing deep frozen unfunded offer " << entry->key(); - permRmOffer(entry->key()); - offer_ = TOffer{}; - continue; + bool const deepFrozen = isDeepFrozen( + view_, + offer_.owner(), + offer_.assetIn().template get().currency, + offer_.assetIn().getIssuer()); + if (deepFrozen) + { + JLOG(j_.trace()) + << "Removing deep frozen unfunded offer " << entry->key(); + permRmOffer(entry->key()); + offer_ = TOffer{}; + continue; + } } // Calculate owner funds @@ -292,8 +269,9 @@ TOfferStreamBase::step() view_, offer_.owner(), amount.out, - offer_.issueOut(), + offer_.assetOut(), fhZERO_IF_FROZEN, + ahZERO_IF_UNAUTHORIZED, j_); // Check for unfunded offer @@ -306,8 +284,9 @@ TOfferStreamBase::step() cancelView_, offer_.owner(), amount.out, - offer_.issueOut(), + offer_.assetOut(), fhZERO_IF_FROZEN, + ahZERO_IF_UNAUTHORIZED, j_); if (original_funds == *ownerFunds_) @@ -326,37 +305,29 @@ TOfferStreamBase::step() } bool const rmSmallIncreasedQOffer = [&] { - bool const inIsXRP = isXRP(offer_.issueIn()); - bool const outIsXRP = isXRP(offer_.issueOut()); - if (inIsXRP && !outIsXRP) - { - // 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 (!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 && + !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(); + return std::visit( + [&]( + TInAmt const&, TOutAmt const&) { + using TIn_ = typename TInAmt::amount_type; + using TOut_ = typename TOutAmt::amount_type; + if constexpr ( + !std::is_same_v || + !std::is_same_v) + return shouldRmSmallIncreasedQOffer(); + else + return false; + }, + offer_.amount().in.asset().getAmountType(), + offer_.amount().out.asset().getAmountType()); } - UNREACHABLE( - "rippls::TOfferStreamBase::step::rmSmallIncreasedQOffer : XRP " - "vs XRP offer"); + assert(0); return false; }(); @@ -366,8 +337,9 @@ TOfferStreamBase::step() cancelView_, offer_.owner(), amount.out, - offer_.issueOut(), + offer_.assetOut(), fhZERO_IF_FROZEN, + ahZERO_IF_UNAUTHORIZED, j_); if (original_funds == *ownerFunds_) @@ -394,12 +366,6 @@ TOfferStreamBase::step() return true; } -void -OfferStream::permRmOffer(uint256 const& offerIndex) -{ - offerDelete(cancelView_, cancelView_.peek(keylet::offer(offerIndex)), j_); -} - template void FlowOfferStream::permRmOffer(uint256 const& offerIndex) @@ -411,9 +377,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..1cc9399fa8e 100644 --- a/src/xrpld/app/tx/detail/OfferStream.h +++ b/src/xrpld/app/tx/detail/OfferStream.h @@ -86,6 +86,7 @@ class TOfferStreamBase permRmOffer(uint256 const& offerIndex) = 0; template + requires ValidTaker bool shouldRmSmallIncreasedQOffer() const; @@ -127,33 +128,6 @@ class TOfferStreamBase } }; -/** Presents and consumes the offers in an order book. - - Two `ApplyView` objects accumulate changes to the ledger. `view` - is applied when the calling transaction succeeds. If the calling - transaction fails, then `view_cancel` is applied. - - Certain invalid offers are automatically removed: - - Offers with missing ledger entries - - Offers that expired - - Offers found unfunded: - An offer is found unfunded when the corresponding balance is zero - and the caller has not modified the balance. This is accomplished - by also looking up the balance in the cancel view. - - When an offer is removed, it is removed from both views. This grooms the - order book regardless of whether or not the transaction is successful. -*/ -class OfferStream : public TOfferStreamBase -{ -protected: - void - permRmOffer(uint256 const& offerIndex) override; - -public: - using TOfferStreamBase::TOfferStreamBase; -}; - /** Presents and consumes the offers in an order book. The `view_' ` `ApplyView` accumulates changes to the ledger. diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 1ed3bacbbd8..93379261f4d 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -77,14 +77,16 @@ Payment::preflight(PreflightContext const& ctx) auto& j = ctx.j; STAmount const dstAmount(tx.getFieldAmount(sfAmount)); - bool const mptDirect = dstAmount.holds(); + bool const isDstMPT = dstAmount.holds(); + bool const MPTokensV2 = ctx.rules.enabled(featureMPTokensV2); - if (mptDirect && !ctx.rules.enabled(featureMPTokensV1)) + if (!ctx.rules.enabled(featureMPTokensV1) && isDstMPT) return temDISABLED; std::uint32_t const txFlags = tx.getFlags(); - std::uint32_t paymentMask = mptDirect ? tfMPTPaymentMask : tfPaymentMask; + std::uint32_t paymentMask = + (isDstMPT && !MPTokensV2) ? tfMPTPaymentMask : tfPaymentMask; if (txFlags & paymentMask) { @@ -92,7 +94,7 @@ Payment::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - if (mptDirect && ctx.tx.isFieldPresent(sfPaths)) + if (!MPTokensV2 && isDstMPT && ctx.tx.isFieldPresent(sfPaths)) return temMALFORMED; bool const partialPaymentAllowed = txFlags & tfPartialPayment; @@ -107,8 +109,9 @@ Payment::preflight(PreflightContext const& ctx) STAmount const maxSourceAmount = getMaxSourceAmount(account, dstAmount, tx[~sfSendMax]); - if ((mptDirect && dstAmount.asset() != maxSourceAmount.asset()) || - (!mptDirect && maxSourceAmount.holds())) + if (!MPTokensV2 && + ((isDstMPT && dstAmount.asset() != maxSourceAmount.asset()) || + (!isDstMPT && maxSourceAmount.holds()))) { JLOG(j.trace()) << "Malformed transaction: inconsistent issues: " << dstAmount.getFullText() << " " @@ -166,7 +169,7 @@ Payment::preflight(PreflightContext const& ctx) << "SendMax specified for XRP to XRP."; return temBAD_SEND_XRP_MAX; } - if ((xrpDirect || mptDirect) && hasPaths) + if ((xrpDirect || (!MPTokensV2 && isDstMPT)) && hasPaths) { // XRP is sent without paths. JLOG(j.trace()) << "Malformed transaction: " @@ -180,7 +183,7 @@ Payment::preflight(PreflightContext const& ctx) << "Partial payment specified for XRP to XRP."; return temBAD_SEND_XRP_PARTIAL; } - if ((xrpDirect || mptDirect) && limitQuality) + if ((xrpDirect || (!MPTokensV2 && isDstMPT)) && limitQuality) { // Consistent but redundant transaction. JLOG(j.trace()) @@ -188,7 +191,7 @@ Payment::preflight(PreflightContext const& ctx) << "Limit quality specified for XRP to XRP or MPT to MPT."; return temBAD_SEND_XRP_LIMIT; } - if ((xrpDirect || mptDirect) && !defaultPathsAllowed) + if ((xrpDirect || (!MPTokensV2 && isDstMPT)) && !defaultPathsAllowed) { // Consistent but redundant transaction. JLOG(j.trace()) @@ -341,7 +344,7 @@ Payment::doApply() AccountID const dstAccountID(ctx_.tx.getAccountID(sfDestination)); STAmount const dstAmount(ctx_.tx.getFieldAmount(sfAmount)); - bool const mptDirect = dstAmount.holds(); + bool const isDstMPT = dstAmount.holds(); STAmount const maxSourceAmount = getMaxSourceAmount(account_, dstAmount, sendMax); @@ -379,9 +382,11 @@ Payment::doApply() sleDst->getFlags() & lsfDepositAuth && depositAuth; bool const depositPreauth = view().rules().enabled(featureDepositPreauth); + bool const MPTokensV2 = view().rules().enabled(featureMPTokensV2); - bool const ripple = - (hasPaths || sendMax || !dstAmount.native()) && !mptDirect; + // Direct MPT payment is handled by payment engine if MPTokensV2 is enabled + bool const ripple = (hasPaths || sendMax || !dstAmount.native()) && + (!isDstMPT || MPTokensV2); // If the destination has lsfDepositAuth set, then only direct XRP // payments (no intermediate steps) are allowed to the destination. @@ -452,7 +457,7 @@ Payment::doApply() terResult = tecPATH_DRY; return terResult; } - else if (mptDirect) + else if (isDstMPT) { JLOG(j_.trace()) << " dstAmount=" << dstAmount.getFullText(); auto const& mptIssue = dstAmount.get(); diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index b1e0494ba46..79cba8317d3 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -103,7 +103,7 @@ SetTrust::preflight(PreflightContext const& ctx) return temBAD_LIMIT; } - if (badCurrency() == saLimitAmount.getCurrency()) + if (badCurrency() == saLimitAmount.get().currency) { JLOG(j.trace()) << "Malformed transaction: specifies XRP as IOU"; return temBAD_CURRENCY; @@ -148,7 +148,7 @@ SetTrust::preclaim(PreclaimContext const& ctx) auto const saLimitAmount = ctx.tx[sfLimitAmount]; - auto const currency = saLimitAmount.getCurrency(); + auto const currency = saLimitAmount.get().currency; auto const uDstAccountID = saLimitAmount.getIssuer(); if (ctx.view.rules().enabled(fixTrustLinesToSelf)) @@ -220,7 +220,9 @@ SetTrust::preclaim(PreclaimContext const& ctx) ammSle->getFieldAmount(sfLPTokenBalance); lpTokens == beast::zero) return tecAMM_EMPTY; - else if (lpTokens.getCurrency() != saLimitAmount.getCurrency()) + else if ( + lpTokens.get().currency != + saLimitAmount.get().currency) return tecNO_PERMISSION; } else @@ -292,7 +294,7 @@ SetTrust::doApply() bool const bQualityIn(ctx_.tx.isFieldPresent(sfQualityIn)); bool const bQualityOut(ctx_.tx.isFieldPresent(sfQualityOut)); - Currency const currency(saLimitAmount.getCurrency()); + Currency const currency(saLimitAmount.get().currency); AccountID uDstAccountID(saLimitAmount.getIssuer()); // true, if current is high account. diff --git a/src/xrpld/app/tx/detail/Taker.cpp b/src/xrpld/app/tx/detail/Taker.cpp deleted file mode 100644 index e98d65fd114..00000000000 --- a/src/xrpld/app/tx/detail/Taker.cpp +++ /dev/null @@ -1,862 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2014 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include -#include -#include - -namespace ripple { - -static std::string -format_amount(STAmount const& amount) -{ - std::string txt = amount.getText(); - txt += "/"; - txt += to_string(amount.issue().currency); - return txt; -} - -BasicTaker::BasicTaker( - CrossType cross_type, - AccountID const& account, - Amounts const& amount, - Quality const& quality, - std::uint32_t flags, - Rate const& rate_in, - Rate const& rate_out, - beast::Journal journal) - : account_(account) - , quality_(quality) - , threshold_(quality_) - , sell_(flags & tfSell) - , original_(amount) - , remaining_(amount) - , issue_in_(remaining_.in.issue()) - , issue_out_(remaining_.out.issue()) - , m_rate_in(rate_in) - , m_rate_out(rate_out) - , cross_type_(cross_type) - , journal_(journal) -{ - XRPL_ASSERT( - remaining_.in > beast::zero, - "ripple::BasicTaker::BasicTaker : positive remaining in"); - XRPL_ASSERT( - remaining_.out > beast::zero, - "ripple::BasicTaker::BasicTaker : positive remaining out"); - - XRPL_ASSERT( - m_rate_in.value, "ripple::BasicTaker::BasicTaker : nonzero rate in"); - XRPL_ASSERT( - m_rate_out.value, "ripple::BasicTaker::BasicTaker : nonzero rate out"); - - // If we are dealing with a particular flavor, make sure that it's the - // flavor we expect: - XRPL_ASSERT( - cross_type != CrossType::XrpToIou || - (isXRP(issue_in()) && !isXRP(issue_out())), - "ripple::BasicTaker::BasicTaker : valid cross to IOU"); - - XRPL_ASSERT( - cross_type != CrossType::IouToXrp || - (!isXRP(issue_in()) && isXRP(issue_out())), - "ripple::BasicTaker::BasicTaker : valid cross to XRP"); - - // And make sure we're not crossing XRP for XRP - XRPL_ASSERT( - !isXRP(issue_in()) || !isXRP(issue_out()), - "ripple::BasicTaker::BasicTaker : not crossing XRP for XRP"); - - // If this is a passive order, we adjust the quality so as to prevent offers - // at the same quality level from being consumed. - if (flags & tfPassive) - ++threshold_; -} - -Rate -BasicTaker::effective_rate( - Rate const& rate, - Issue const& issue, - AccountID const& from, - AccountID const& to) -{ - // If there's a transfer rate, the issuer is not involved - // and the sender isn't the same as the recipient, return - // the actual transfer rate. - if (rate != parityRate && from != to && from != issue.account && - to != issue.account) - { - return rate; - } - - return parityRate; -} - -bool -BasicTaker::unfunded() const -{ - if (get_funds(account(), remaining_.in) > beast::zero) - return false; - - JLOG(journal_.debug()) << "Unfunded: taker is out of funds."; - return true; -} - -bool -BasicTaker::done() const -{ - // We are done if we have consumed all the input currency - if (remaining_.in <= beast::zero) - { - JLOG(journal_.debug()) - << "Done: all the input currency has been consumed."; - return true; - } - - // We are done if using buy semantics and we received the - // desired amount of output currency - if (!sell_ && (remaining_.out <= beast::zero)) - { - JLOG(journal_.debug()) << "Done: the desired amount has been received."; - return true; - } - - // We are done if the taker is out of funds - if (unfunded()) - { - JLOG(journal_.debug()) << "Done: taker out of funds."; - return true; - } - - return false; -} - -Amounts -BasicTaker::remaining_offer() const -{ - // If the taker is done, then there's no offer to place. - if (done()) - return Amounts(remaining_.in.zeroed(), remaining_.out.zeroed()); - - // Avoid math altogether if we didn't cross. - if (original_ == remaining_) - return original_; - - if (sell_) - { - XRPL_ASSERT( - remaining_.in > beast::zero, - "ripple::BasicTaker::remaining_offer : positive remaining in"); - - // We scale the output based on the remaining input: - return Amounts( - remaining_.in, - divRound(remaining_.in, quality_.rate(), issue_out_, true)); - } - - XRPL_ASSERT( - remaining_.out > beast::zero, - "ripple::BasicTaker::remaining_offer : positive remaining out"); - - // We scale the input based on the remaining output: - return Amounts( - mulRound(remaining_.out, quality_.rate(), issue_in_, true), - remaining_.out); -} - -Amounts const& -BasicTaker::original_offer() const -{ - return original_; -} - -// TODO: the presence of 'output' is an artifact caused by the fact that -// Amounts carry issue information which should be decoupled. -static STAmount -qual_div(STAmount const& amount, Quality const& quality, STAmount const& output) -{ - auto result = divide(amount, quality.rate(), output.issue()); - return std::min(result, output); -} - -static STAmount -qual_mul(STAmount const& amount, Quality const& quality, STAmount const& output) -{ - auto result = multiply(amount, quality.rate(), output.issue()); - return std::min(result, output); -} - -void -BasicTaker::log_flow(char const* description, Flow const& flow) -{ - auto stream = journal_.debug(); - if (!stream) - return; - - stream << description; - - if (isXRP(issue_in())) - stream << " order in: " << format_amount(flow.order.in); - else - stream << " order in: " << format_amount(flow.order.in) - << " (issuer: " << format_amount(flow.issuers.in) << ")"; - - if (isXRP(issue_out())) - stream << " order out: " << format_amount(flow.order.out); - else - stream << " order out: " << format_amount(flow.order.out) - << " (issuer: " << format_amount(flow.issuers.out) << ")"; -} - -BasicTaker::Flow -BasicTaker::flow_xrp_to_iou( - Amounts const& order, - Quality quality, - STAmount const& owner_funds, - STAmount const& taker_funds, - Rate const& rate_out) -{ - Flow f; - f.order = order; - f.issuers.out = multiply(f.order.out, rate_out); - - log_flow("flow_xrp_to_iou", f); - - // Clamp on owner balance - if (owner_funds < f.issuers.out) - { - f.issuers.out = owner_funds; - f.order.out = divide(f.issuers.out, rate_out); - f.order.in = qual_mul(f.order.out, quality, f.order.in); - log_flow("(clamped on owner balance)", f); - } - - // Clamp if taker wants to limit the output - if (!sell_ && remaining_.out < f.order.out) - { - f.order.out = remaining_.out; - f.order.in = qual_mul(f.order.out, quality, f.order.in); - f.issuers.out = multiply(f.order.out, rate_out); - log_flow("(clamped on taker output)", f); - } - - // Clamp on the taker's funds - if (taker_funds < f.order.in) - { - f.order.in = taker_funds; - f.order.out = qual_div(f.order.in, quality, f.order.out); - f.issuers.out = multiply(f.order.out, rate_out); - log_flow("(clamped on taker funds)", f); - } - - // Clamp on remaining offer if we are not handling the second leg - // of an autobridge. - if (cross_type_ == CrossType::XrpToIou && (remaining_.in < f.order.in)) - { - f.order.in = remaining_.in; - f.order.out = qual_div(f.order.in, quality, f.order.out); - f.issuers.out = multiply(f.order.out, rate_out); - log_flow("(clamped on taker input)", f); - } - - return f; -} - -BasicTaker::Flow -BasicTaker::flow_iou_to_xrp( - Amounts const& order, - Quality quality, - STAmount const& owner_funds, - STAmount const& taker_funds, - Rate const& rate_in) -{ - Flow f; - f.order = order; - f.issuers.in = multiply(f.order.in, rate_in); - - log_flow("flow_iou_to_xrp", f); - - // Clamp on owner's funds - if (owner_funds < f.order.out) - { - f.order.out = owner_funds; - f.order.in = qual_mul(f.order.out, quality, f.order.in); - f.issuers.in = multiply(f.order.in, rate_in); - log_flow("(clamped on owner funds)", f); - } - - // Clamp if taker wants to limit the output and we are not the - // first leg of an autobridge. - if (!sell_ && cross_type_ == CrossType::IouToXrp) - { - if (remaining_.out < f.order.out) - { - f.order.out = remaining_.out; - f.order.in = qual_mul(f.order.out, quality, f.order.in); - f.issuers.in = multiply(f.order.in, rate_in); - log_flow("(clamped on taker output)", f); - } - } - - // Clamp on the taker's input offer - if (remaining_.in < f.order.in) - { - f.order.in = remaining_.in; - f.issuers.in = multiply(f.order.in, rate_in); - f.order.out = qual_div(f.order.in, quality, f.order.out); - log_flow("(clamped on taker input)", f); - } - - // Clamp on the taker's input balance - if (taker_funds < f.issuers.in) - { - f.issuers.in = taker_funds; - f.order.in = divide(f.issuers.in, rate_in); - f.order.out = qual_div(f.order.in, quality, f.order.out); - log_flow("(clamped on taker funds)", f); - } - - return f; -} - -BasicTaker::Flow -BasicTaker::flow_iou_to_iou( - Amounts const& order, - Quality quality, - STAmount const& owner_funds, - STAmount const& taker_funds, - Rate const& rate_in, - Rate const& rate_out) -{ - Flow f; - f.order = order; - f.issuers.in = multiply(f.order.in, rate_in); - f.issuers.out = multiply(f.order.out, rate_out); - - log_flow("flow_iou_to_iou", f); - - // Clamp on owner balance - if (owner_funds < f.issuers.out) - { - f.issuers.out = owner_funds; - f.order.out = divide(f.issuers.out, rate_out); - f.order.in = qual_mul(f.order.out, quality, f.order.in); - f.issuers.in = multiply(f.order.in, rate_in); - log_flow("(clamped on owner funds)", f); - } - - // Clamp on taker's offer - if (!sell_ && remaining_.out < f.order.out) - { - f.order.out = remaining_.out; - f.order.in = qual_mul(f.order.out, quality, f.order.in); - f.issuers.out = multiply(f.order.out, rate_out); - f.issuers.in = multiply(f.order.in, rate_in); - log_flow("(clamped on taker output)", f); - } - - // Clamp on the taker's input offer - if (remaining_.in < f.order.in) - { - f.order.in = remaining_.in; - f.issuers.in = multiply(f.order.in, rate_in); - f.order.out = qual_div(f.order.in, quality, f.order.out); - f.issuers.out = multiply(f.order.out, rate_out); - log_flow("(clamped on taker input)", f); - } - - // Clamp on the taker's input balance - if (taker_funds < f.issuers.in) - { - f.issuers.in = taker_funds; - f.order.in = divide(f.issuers.in, rate_in); - f.order.out = qual_div(f.order.in, quality, f.order.out); - f.issuers.out = multiply(f.order.out, rate_out); - log_flow("(clamped on taker funds)", f); - } - - return f; -} - -// Calculates the direct flow through the specified offer -BasicTaker::Flow -BasicTaker::do_cross(Amounts offer, Quality quality, AccountID const& owner) -{ - auto const owner_funds = get_funds(owner, offer.out); - auto const taker_funds = get_funds(account(), offer.in); - - Flow result; - - if (cross_type_ == CrossType::XrpToIou) - { - result = flow_xrp_to_iou( - offer, - quality, - owner_funds, - taker_funds, - out_rate(owner, account())); - } - else if (cross_type_ == CrossType::IouToXrp) - { - result = flow_iou_to_xrp( - offer, - quality, - owner_funds, - taker_funds, - in_rate(owner, account())); - } - else - { - result = flow_iou_to_iou( - offer, - quality, - owner_funds, - taker_funds, - in_rate(owner, account()), - out_rate(owner, account())); - } - - if (!result.sanity_check()) - Throw("Computed flow fails sanity check."); - - remaining_.out -= result.order.out; - remaining_.in -= result.order.in; - - XRPL_ASSERT( - remaining_.in >= beast::zero, - "ripple::BasicTaker::do_cross : minimum remaining in"); - - return result; -} - -// Calculates the bridged flow through the specified offers -std::pair -BasicTaker::do_cross( - Amounts offer1, - Quality quality1, - AccountID const& owner1, - Amounts offer2, - Quality quality2, - AccountID const& owner2) -{ - XRPL_ASSERT( - !offer1.in.native(), - "ripple::BasicTaker::do_cross : offer1 in is not XRP"); - XRPL_ASSERT( - offer1.out.native(), - "ripple::BasicTaker::do_cross : offer1 out is XRP"); - XRPL_ASSERT( - offer2.in.native(), "ripple::BasicTaker::do_cross : offer2 in is XRP"); - XRPL_ASSERT( - !offer2.out.native(), - "ripple::BasicTaker::do_cross : offer2 out is not XRP"); - - // If the taker owns the first leg of the offer, then the taker's available - // funds aren't the limiting factor for the input - the offer itself is. - auto leg1_in_funds = get_funds(account(), offer1.in); - - if (account() == owner1) - { - JLOG(journal_.trace()) << "The taker owns the first leg of a bridge."; - leg1_in_funds = std::max(leg1_in_funds, offer1.in); - } - - // If the taker owns the second leg of the offer, then the taker's available - // funds are not the limiting factor for the output - the offer itself is. - auto leg2_out_funds = get_funds(owner2, offer2.out); - - if (account() == owner2) - { - JLOG(journal_.trace()) << "The taker owns the second leg of a bridge."; - leg2_out_funds = std::max(leg2_out_funds, offer2.out); - } - - // The amount available to flow via XRP is the amount that the owner of the - // first leg of the bridge has, up to the first leg's output. - // - // But, when both legs of a bridge are owned by the same person, the amount - // of XRP that can flow between the two legs is, essentially, infinite - // since all the owner is doing is taking out XRP of his left pocket - // and putting it in his right pocket. In that case, we set the available - // XRP to the largest of the two offers. - auto xrp_funds = get_funds(owner1, offer1.out); - - if (owner1 == owner2) - { - JLOG(journal_.trace()) - << "The bridge endpoints are owned by the same account."; - xrp_funds = std::max(offer1.out, offer2.in); - } - - if (auto stream = journal_.debug()) - { - stream << "Available bridge funds:"; - stream << " leg1 in: " << format_amount(leg1_in_funds); - stream << " leg2 out: " << format_amount(leg2_out_funds); - stream << " xrp: " << format_amount(xrp_funds); - } - - auto const leg1_rate = in_rate(owner1, account()); - auto const leg2_rate = out_rate(owner2, account()); - - // Attempt to determine the maximal flow that can be achieved across each - // leg independent of the other. - auto flow1 = - flow_iou_to_xrp(offer1, quality1, xrp_funds, leg1_in_funds, leg1_rate); - - if (!flow1.sanity_check()) - Throw("Computed flow1 fails sanity check."); - - auto flow2 = - flow_xrp_to_iou(offer2, quality2, leg2_out_funds, xrp_funds, leg2_rate); - - if (!flow2.sanity_check()) - Throw("Computed flow2 fails sanity check."); - - // We now have the maximal flows across each leg individually. We need to - // equalize them, so that the amount of XRP that flows out of the first leg - // is the same as the amount of XRP that flows into the second leg. We take - // the side which is the limiting factor (if any) and adjust the other. - if (flow1.order.out < flow2.order.in) - { - // Adjust the second leg of the offer down: - flow2.order.in = flow1.order.out; - flow2.order.out = qual_div(flow2.order.in, quality2, flow2.order.out); - flow2.issuers.out = multiply(flow2.order.out, leg2_rate); - log_flow("Balancing: adjusted second leg down", flow2); - } - else if (flow1.order.out > flow2.order.in) - { - // Adjust the first leg of the offer down: - flow1.order.out = flow2.order.in; - flow1.order.in = qual_mul(flow1.order.out, quality1, flow1.order.in); - flow1.issuers.in = multiply(flow1.order.in, leg1_rate); - log_flow("Balancing: adjusted first leg down", flow2); - } - - if (flow1.order.out != flow2.order.in) - Throw("Bridged flow is out of balance."); - - remaining_.out -= flow2.order.out; - remaining_.in -= flow1.order.in; - - return std::make_pair(flow1, flow2); -} - -//============================================================================== - -Taker::Taker( - CrossType cross_type, - ApplyView& view, - AccountID const& account, - Amounts const& offer, - std::uint32_t flags, - beast::Journal journal) - : BasicTaker( - cross_type, - account, - offer, - Quality(offer), - flags, - calculateRate(view, offer.in.getIssuer(), account), - calculateRate(view, offer.out.getIssuer(), account), - journal) - , view_(view) - , xrp_flow_(0) - , direct_crossings_(0) - , bridge_crossings_(0) -{ - XRPL_ASSERT( - issue_in() == offer.in.issue(), - "ripple::Taker::Taker : issue in is a match"); - XRPL_ASSERT( - issue_out() == offer.out.issue(), - "ripple::Taker::Taker : issue out is a match"); - - if (auto stream = journal_.debug()) - { - stream << "Crossing as: " << to_string(account); - - if (isXRP(issue_in())) - stream << " Offer in: " << format_amount(offer.in); - else - stream << " Offer in: " << format_amount(offer.in) - << " (issuer: " << issue_in().account << ")"; - - if (isXRP(issue_out())) - stream << " Offer out: " << format_amount(offer.out); - else - stream << " Offer out: " << format_amount(offer.out) - << " (issuer: " << issue_out().account << ")"; - - stream << " Balance: " - << format_amount(get_funds(account, offer.in)); - } -} - -void -Taker::consume_offer(Offer& offer, Amounts const& order) -{ - if (order.in < beast::zero) - Throw("flow with negative input."); - - if (order.out < beast::zero) - Throw("flow with negative output."); - - JLOG(journal_.debug()) << "Consuming from offer " << offer; - - if (auto stream = journal_.trace()) - { - auto const& available = offer.amount(); - - stream << " in:" << format_amount(available.in); - stream << " out:" << format_amount(available.out); - } - - offer.consume(view_, order); -} - -STAmount -Taker::get_funds(AccountID const& account, STAmount const& amount) const -{ - return accountFunds(view_, account, amount, fhZERO_IF_FROZEN, journal_); -} - -TER -Taker::transferXRP( - AccountID const& from, - AccountID const& to, - STAmount const& amount) -{ - if (!isXRP(amount)) - Throw("Using transferXRP with IOU"); - - if (from == to) - return tesSUCCESS; - - // Transferring zero is equivalent to not doing a transfer - if (amount == beast::zero) - return tesSUCCESS; - - return ripple::transferXRP(view_, from, to, amount, journal_); -} - -TER -Taker::redeemIOU( - AccountID const& account, - STAmount const& amount, - Issue const& issue) -{ - if (isXRP(amount)) - Throw("Using redeemIOU with XRP"); - - if (account == issue.account) - return tesSUCCESS; - - // Transferring zero is equivalent to not doing a transfer - if (amount == beast::zero) - return tesSUCCESS; - - // If we are trying to redeem some amount, then the account - // must have a credit balance. - if (get_funds(account, amount) <= beast::zero) - Throw("redeemIOU has no funds to redeem"); - - auto ret = ripple::redeemIOU(view_, account, amount, issue, journal_); - - if (get_funds(account, amount) < beast::zero) - Throw("redeemIOU redeemed more funds than available"); - - return ret; -} - -TER -Taker::issueIOU( - AccountID const& account, - STAmount const& amount, - Issue const& issue) -{ - if (isXRP(amount)) - Throw("Using issueIOU with XRP"); - - if (account == issue.account) - return tesSUCCESS; - - // Transferring zero is equivalent to not doing a transfer - if (amount == beast::zero) - return tesSUCCESS; - - return ripple::issueIOU(view_, account, amount, issue, journal_); -} - -// Performs funds transfers to fill the given offer and adjusts offer. -TER -Taker::fill(BasicTaker::Flow const& flow, Offer& offer) -{ - // adjust offer - consume_offer(offer, flow.order); - - TER result = tesSUCCESS; - - if (cross_type() != CrossType::XrpToIou) - { - XRPL_ASSERT( - !isXRP(flow.order.in), "ripple::Taker::fill : order in is not XRP"); - - if (result == tesSUCCESS) - result = - redeemIOU(account(), flow.issuers.in, flow.issuers.in.issue()); - - if (result == tesSUCCESS) - result = - issueIOU(offer.owner(), flow.order.in, flow.order.in.issue()); - } - else - { - XRPL_ASSERT( - isXRP(flow.order.in), "ripple::Taker::fill : order in is XRP"); - - if (result == tesSUCCESS) - result = transferXRP(account(), offer.owner(), flow.order.in); - } - - // Now send funds from the account whose offer we're taking - if (cross_type() != CrossType::IouToXrp) - { - XRPL_ASSERT( - !isXRP(flow.order.out), - "ripple::Taker::fill : order out is not XRP"); - - if (result == tesSUCCESS) - result = redeemIOU( - offer.owner(), flow.issuers.out, flow.issuers.out.issue()); - - if (result == tesSUCCESS) - result = - issueIOU(account(), flow.order.out, flow.order.out.issue()); - } - else - { - XRPL_ASSERT( - isXRP(flow.order.out), "ripple::Taker::fill : order out is XRP"); - - if (result == tesSUCCESS) - result = transferXRP(offer.owner(), account(), flow.order.out); - } - - if (result == tesSUCCESS) - direct_crossings_++; - - return result; -} - -// Performs bridged funds transfers to fill the given offers and adjusts offers. -TER -Taker::fill( - BasicTaker::Flow const& flow1, - Offer& leg1, - BasicTaker::Flow const& flow2, - Offer& leg2) -{ - // Adjust offers accordingly - consume_offer(leg1, flow1.order); - consume_offer(leg2, flow2.order); - - TER result = tesSUCCESS; - - // Taker to leg1: IOU - if (leg1.owner() != account()) - { - if (result == tesSUCCESS) - result = redeemIOU( - account(), flow1.issuers.in, flow1.issuers.in.issue()); - - if (result == tesSUCCESS) - result = - issueIOU(leg1.owner(), flow1.order.in, flow1.order.in.issue()); - } - - // leg1 to leg2: bridging over XRP - if (result == tesSUCCESS) - result = transferXRP(leg1.owner(), leg2.owner(), flow1.order.out); - - // leg2 to Taker: IOU - if (leg2.owner() != account()) - { - if (result == tesSUCCESS) - result = redeemIOU( - leg2.owner(), flow2.issuers.out, flow2.issuers.out.issue()); - - if (result == tesSUCCESS) - result = - issueIOU(account(), flow2.order.out, flow2.order.out.issue()); - } - - if (result == tesSUCCESS) - { - bridge_crossings_++; - xrp_flow_ += flow1.order.out; - } - - return result; -} - -TER -Taker::cross(Offer& offer) -{ - // In direct crossings, at least one leg must not be XRP. - if (isXRP(offer.amount().in) && isXRP(offer.amount().out)) - return tefINTERNAL; - - auto const amount = - do_cross(offer.amount(), offer.quality(), offer.owner()); - - return fill(amount, offer); -} - -TER -Taker::cross(Offer& leg1, Offer& leg2) -{ - // In bridged crossings, XRP must can't be the input to the first leg - // or the output of the second leg. - if (isXRP(leg1.amount().in) || isXRP(leg2.amount().out)) - return tefINTERNAL; - - auto ret = do_cross( - leg1.amount(), - leg1.quality(), - leg1.owner(), - leg2.amount(), - leg2.quality(), - leg2.owner()); - - return fill(ret.first, leg1, ret.second, leg2); -} - -Rate -Taker::calculateRate( - ApplyView const& view, - AccountID const& issuer, - AccountID const& account) -{ - return isXRP(issuer) || (account == issuer) ? parityRate - : transferRate(view, issuer); -} - -} // namespace ripple diff --git a/src/xrpld/app/tx/detail/Taker.h b/src/xrpld/app/tx/detail/Taker.h deleted file mode 100644 index 3e64c59b542..00000000000 --- a/src/xrpld/app/tx/detail/Taker.h +++ /dev/null @@ -1,342 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2014 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_BOOK_TAKER_H_INCLUDED -#define RIPPLE_APP_BOOK_TAKER_H_INCLUDED - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace ripple { - -/** The flavor of an offer crossing */ -enum class CrossType { XrpToIou, IouToXrp, IouToIou }; - -/** State for the active party during order book or payment operations. */ -class BasicTaker -{ -private: - AccountID account_; - Quality quality_; - Quality threshold_; - - bool sell_; - - // The original in and out quantities. - Amounts const original_; - - // The amounts still left over for us to try and take. - Amounts remaining_; - - // The issuers for the input and output - Issue const& issue_in_; - Issue const& issue_out_; - - // The rates that will be paid when the input and output currencies are - // transfered and the currency issuer isn't involved: - Rate const m_rate_in; - Rate const m_rate_out; - - // The type of crossing that we are performing - CrossType cross_type_; - -protected: - beast::Journal const journal_; - - struct Flow - { - explicit Flow() = default; - - Amounts order; - Amounts issuers; - - bool - sanity_check() const - { - using beast::zero; - - if (isXRP(order.in) && isXRP(order.out)) - return false; - - return order.in >= zero && order.out >= zero && - issuers.in >= zero && issuers.out >= zero; - } - }; - -private: - void - log_flow(char const* description, Flow const& flow); - - Flow - flow_xrp_to_iou( - Amounts const& offer, - Quality quality, - STAmount const& owner_funds, - STAmount const& taker_funds, - Rate const& rate_out); - - Flow - flow_iou_to_xrp( - Amounts const& offer, - Quality quality, - STAmount const& owner_funds, - STAmount const& taker_funds, - Rate const& rate_in); - - Flow - flow_iou_to_iou( - Amounts const& offer, - Quality quality, - STAmount const& owner_funds, - STAmount const& taker_funds, - Rate const& rate_in, - Rate const& rate_out); - - // Calculates the transfer rate that we should use when calculating - // flows for a particular issue between two accounts. - static Rate - effective_rate( - Rate const& rate, - Issue const& issue, - AccountID const& from, - AccountID const& to); - - // The transfer rate for the input currency between the given accounts - Rate - in_rate(AccountID const& from, AccountID const& to) const - { - return effective_rate(m_rate_in, original_.in.issue(), from, to); - } - - // The transfer rate for the output currency between the given accounts - Rate - out_rate(AccountID const& from, AccountID const& to) const - { - return effective_rate(m_rate_out, original_.out.issue(), from, to); - } - -public: - BasicTaker() = delete; - BasicTaker(BasicTaker const&) = delete; - - BasicTaker( - CrossType cross_type, - AccountID const& account, - Amounts const& amount, - Quality const& quality, - std::uint32_t flags, - Rate const& rate_in, - Rate const& rate_out, - beast::Journal journal = beast::Journal{beast::Journal::getNullSink()}); - - virtual ~BasicTaker() = default; - - /** Returns the amount remaining on the offer. - This is the amount at which the offer should be placed. It may either - be for the full amount when there were no crossing offers, or for zero - when the offer fully crossed, or any amount in between. - It is always at the original offer quality (quality_) - */ - Amounts - remaining_offer() const; - - /** Returns the amount that the offer was originally placed at. */ - Amounts const& - original_offer() const; - - /** Returns the account identifier of the taker. */ - AccountID const& - account() const noexcept - { - return account_; - } - - /** Returns `true` if the quality does not meet the taker's requirements. */ - bool - reject(Quality const& quality) const noexcept - { - return quality < threshold_; - } - - /** Returns the type of crossing that is being performed */ - CrossType - cross_type() const - { - return cross_type_; - } - - /** Returns the Issue associated with the input of the offer */ - Issue const& - issue_in() const - { - return issue_in_; - } - - /** Returns the Issue associated with the output of the offer */ - Issue const& - issue_out() const - { - return issue_out_; - } - - /** Returns `true` if the taker has run out of funds. */ - bool - unfunded() const; - - /** Returns `true` if order crossing should not continue. - Order processing is stopped if the taker's order quantities have - been reached, or if the taker has run out of input funds. - */ - bool - done() const; - - /** Perform direct crossing through given offer. - @return an `Amounts` describing the flow achieved during cross - */ - BasicTaker::Flow - do_cross(Amounts offer, Quality quality, AccountID const& owner); - - /** Perform bridged crossing through given offers. - @return a pair of `Amounts` describing the flow achieved during cross - */ - std::pair - do_cross( - Amounts offer1, - Quality quality1, - AccountID const& owner1, - Amounts offer2, - Quality quality2, - AccountID const& owner2); - - virtual STAmount - get_funds(AccountID const& account, STAmount const& funds) const = 0; -}; - -//------------------------------------------------------------------------------ - -class Taker : public BasicTaker -{ -public: - Taker() = delete; - Taker(Taker const&) = delete; - - Taker( - CrossType cross_type, - ApplyView& view, - AccountID const& account, - Amounts const& offer, - std::uint32_t flags, - beast::Journal journal); - ~Taker() = default; - - void - consume_offer(Offer& offer, Amounts const& order); - - STAmount - get_funds(AccountID const& account, STAmount const& funds) const override; - - STAmount const& - get_xrp_flow() const - { - return xrp_flow_; - } - - std::uint32_t - get_direct_crossings() const - { - return direct_crossings_; - } - - std::uint32_t - get_bridge_crossings() const - { - return bridge_crossings_; - } - - /** Perform a direct or bridged offer crossing as appropriate. - Funds will be transferred accordingly, and offers will be adjusted. - @return tesSUCCESS if successful, or an error code otherwise. - */ - /** @{ */ - TER - cross(Offer& offer); - - TER - cross(Offer& leg1, Offer& leg2); - /** @} */ - -private: - static Rate - calculateRate( - ApplyView const& view, - AccountID const& issuer, - AccountID const& account); - - TER - fill(BasicTaker::Flow const& flow, Offer& offer); - - TER - fill( - BasicTaker::Flow const& flow1, - Offer& leg1, - BasicTaker::Flow const& flow2, - Offer& leg2); - - TER - transferXRP( - AccountID const& from, - AccountID const& to, - STAmount const& amount); - - TER - redeemIOU( - AccountID const& account, - STAmount const& amount, - Issue const& issue); - - TER - issueIOU( - AccountID const& account, - STAmount const& amount, - Issue const& issue); - -private: - // The underlying ledger entry we are dealing with - ApplyView& view_; - - // The amount of XRP that flowed if we were autobridging - STAmount xrp_flow_; - - // The number direct crossings that we performed - std::uint32_t direct_crossings_; - - // The number autobridged crossings that we performed - std::uint32_t bridge_crossings_; -}; - -} // namespace ripple - -#endif diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 9d3e9e39460..7c7a7bc1443 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -857,6 +857,32 @@ removeDeletedTrustLines( } } +static void +removeDeletedMPTs( + ApplyView& view, + std::vector const& mpts, + beast::Journal viewJ) +{ + // There could be at most two MPTs - one for each side of AMM pool + if (mpts.size() > 2) + { + JLOG(viewJ.error()) + << "removeDeletedTrustLines: deleted mpts exceed 2 " << mpts.size(); + return; + } + + for (auto const& index : mpts) + { + if (auto const sleState = view.peek({ltMPTOKEN, index}); + deleteAMMMPToken(view, sleState, (*sleState)[sfIssuer], viewJ) != + tesSUCCESS) + { + JLOG(viewJ.error()) + << "removeDeletedMPTs: failed to delete AMM MPT"; + } + } +} + /** Reset the context, discarding any changes made and adjust the fee */ std::pair Transactor::reset(XRPAmount fee) @@ -983,20 +1009,22 @@ Transactor::operator()() // when transactions fail with a `tec` code. std::vector removedOffers; std::vector removedTrustLines; + std::vector removedMPTs; std::vector expiredNFTokenOffers; std::vector expiredCredentials; bool const doOffers = ((result == tecOVERSIZE) || (result == tecKILLED)); - bool const doLines = (result == tecINCOMPLETE); + bool const doLinesOrMPTs = (result == tecINCOMPLETE); bool const doNFTokenOffers = (result == tecEXPIRED); bool const doCredentials = (result == tecEXPIRED); - if (doOffers || doLines || doNFTokenOffers || doCredentials) + if (doOffers || doLinesOrMPTs || doNFTokenOffers || doCredentials) { ctx_.visit([doOffers, &removedOffers, - doLines, + doLinesOrMPTs, &removedTrustLines, + &removedMPTs, doNFTokenOffers, &expiredNFTokenOffers, doCredentials, @@ -1020,11 +1048,13 @@ Transactor::operator()() removedOffers.push_back(index); } - if (doLines && before && after && - (before->getType() == ltRIPPLE_STATE)) + if (doLinesOrMPTs && before && after) { // Removal of obsolete AMM trust line - removedTrustLines.push_back(index); + if (before->getType() == ltRIPPLE_STATE) + removedTrustLines.push_back(index); + else if (before->getType() == ltMPTOKEN) + removedMPTs.push_back(index); } if (doNFTokenOffers && before && after && @@ -1057,8 +1087,11 @@ Transactor::operator()() view(), expiredNFTokenOffers, ctx_.app.journal("View")); if (result == tecINCOMPLETE) + { removeDeletedTrustLines( view(), removedTrustLines, ctx_.app.journal("View")); + removeDeletedMPTs(view(), removedMPTs, ctx_.app.journal("View")); + } if (result == tecEXPIRED) removeExpiredCredentials( diff --git a/src/xrpld/app/tx/detail/XChainBridge.cpp b/src/xrpld/app/tx/detail/XChainBridge.cpp index 0c6be61040c..e6f6aafd83f 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.cpp +++ b/src/xrpld/app/tx/detail/XChainBridge.cpp @@ -682,7 +682,7 @@ finalizeClaimHelper( saveNumberRoundMode _{Number::setround(round_mode)}; STAmount const den{rewardAccounts.size()}; - return divide(rewardPool, den, rewardPool.issue()); + return divide(rewardPool, den, rewardPool.asset()); }(); STAmount distributed = rewardPool.zeroed(); for (auto const& rewardAccount : rewardAccounts) @@ -1237,7 +1237,7 @@ attestationPreflight(PreflightContext const& ctx) return temXCHAIN_BAD_PROOF; auto const expectedIssue = bridgeSpec.issue(STXChainBridge::srcChain(att->wasLockingChainSend)); - if (att->sendingAmount.issue() != expectedIssue) + if (att->sendingAmount.asset() != expectedIssue) return temXCHAIN_BAD_PROOF; return preflight2(ctx); @@ -1682,8 +1682,8 @@ XChainClaim::preflight(PreflightContext const& ctx) auto const amount = ctx.tx[sfAmount]; if (amount.signum() <= 0 || - (amount.issue() != bridgeSpec.lockingChainIssue() && - amount.issue() != bridgeSpec.issuingChainIssue())) + (amount.asset() != bridgeSpec.lockingChainIssue() && + amount.asset() != bridgeSpec.issuingChainIssue())) { return temBAD_AMOUNT; } @@ -1726,12 +1726,12 @@ XChainClaim::preclaim(PreclaimContext const& ctx) if (isLockingChain) { - if (bridgeSpec.lockingChainIssue() != thisChainAmount.issue()) + if (bridgeSpec.lockingChainIssue() != thisChainAmount.asset()) return tecXCHAIN_BAD_TRANSFER_ISSUE; } else { - if (bridgeSpec.issuingChainIssue() != thisChainAmount.issue()) + if (bridgeSpec.issuingChainIssue() != thisChainAmount.asset()) return tecXCHAIN_BAD_TRANSFER_ISSUE; } } @@ -1922,8 +1922,8 @@ XChainCommit::preflight(PreflightContext const& ctx) if (amount.signum() <= 0 || !isLegalNet(amount)) return temBAD_AMOUNT; - if (amount.issue() != bridgeSpec.lockingChainIssue() && - amount.issue() != bridgeSpec.issuingChainIssue()) + if (amount.asset() != bridgeSpec.lockingChainIssue() && + amount.asset() != bridgeSpec.issuingChainIssue()) return temBAD_ISSUER; return preflight2(ctx); @@ -1962,12 +1962,12 @@ XChainCommit::preclaim(PreclaimContext const& ctx) if (isLockingChain) { - if (bridgeSpec.lockingChainIssue() != ctx.tx[sfAmount].issue()) + if (bridgeSpec.lockingChainIssue() != ctx.tx[sfAmount].asset()) return tecXCHAIN_BAD_TRANSFER_ISSUE; } else { - if (bridgeSpec.issuingChainIssue() != ctx.tx[sfAmount].issue()) + if (bridgeSpec.issuingChainIssue() != ctx.tx[sfAmount].asset()) return tecXCHAIN_BAD_TRANSFER_ISSUE; } @@ -2194,7 +2194,7 @@ XChainCreateAccountCommit::preflight(PreflightContext const& ctx) if (reward.signum() < 0 || !reward.native()) return temBAD_AMOUNT; - if (reward.issue() != amount.issue()) + if (reward.asset() != amount.asset()) return temBAD_AMOUNT; return preflight2(ctx); @@ -2227,7 +2227,7 @@ XChainCreateAccountCommit::preclaim(PreclaimContext const& ctx) if (amount < *minCreateAmount) return tecXCHAIN_INSUFF_CREATE_AMOUNT; - if (minCreateAmount->issue() != amount.issue()) + if (minCreateAmount->asset() != amount.asset()) return tecXCHAIN_BAD_TRANSFER_ISSUE; AccountID const thisDoor = (*sleBridge)[sfAccount]; @@ -2250,7 +2250,7 @@ XChainCreateAccountCommit::preclaim(PreclaimContext const& ctx) STXChainBridge::ChainType const dstChain = STXChainBridge::otherChain(srcChain); - if (bridgeSpec.issue(srcChain) != ctx.tx[sfAmount].issue()) + if (bridgeSpec.issue(srcChain) != ctx.tx[sfAmount].asset()) return tecXCHAIN_BAD_TRANSFER_ISSUE; if (!isXRP(bridgeSpec.issue(dstChain))) diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index aca3f9fa6d8..ddb3d032941 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -164,8 +164,8 @@ isDeepFrozen( isLPTokenFrozen( ReadView const& view, AccountID const& account, - Issue const& asset, - Issue const& asset2); + Asset const& asset, + Asset const& asset2); // Returns the amount an account can spend without going into debt. // @@ -196,6 +196,15 @@ accountHolds( AuthHandling zeroIfUnauthorized, beast::Journal j); +[[nodiscard]] STAmount +accountHolds( + ReadView const& view, + AccountID const& account, + Asset const& issue, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j); + // Returns the amount an account can spend of the currency type saDefault, or // returns saDefault if this account is the issuer of the currency in // question. Should be used in favor of accountHolds when questioning how much @@ -209,6 +218,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 @@ -539,17 +557,44 @@ transferXRP( STAmount const& amount, beast::Journal j); +/* Check if MPToken exists: + * - StrongAuth - before checking lsfMPTRequireAuth is set + * - WeakAuth - after checking if lsfMPTRequireAuth is set + */ +enum class MPTAuthType : bool { StrongAuth = true, WeakAuth = false }; + /** Check if the account lacks required authorization. * Return tecNO_AUTH or tecNO_LINE if it does * and tesSUCCESS otherwise. */ [[nodiscard]] TER requireAuth(ReadView const& view, Issue const& issue, AccountID const& account); +/* If StrongAuth then return tecNO_AUTH if MPToken doesn't exist or + * lsfMPTRequireAuth is set and MPToken is not authorized. If WeakAuth then + * return tecNO_AUTH if lsfMPTRequireAuth is set and MPToken doesn't exist or is + * not authorized. + */ [[nodiscard]] TER requireAuth( ReadView const& view, MPTIssue const& mptIssue, - AccountID const& account); + AccountID const& account, + MPTAuthType authType = MPTAuthType::StrongAuth); +[[nodiscard]] TER inline requireAuth( + ReadView const& view, + Asset const& asset, + AccountID const& account, + MPTAuthType authType = MPTAuthType::StrongAuth) +{ + return std::visit( + [&](TIss const& issue_) { + if constexpr (std::is_same_v) + return requireAuth(view, issue_, account); + else + return requireAuth(view, issue_, account, authType); + }, + asset.value()); +} /** Check if the destination account is allowed * to receive MPT. Return tecNO_AUTH if it doesn't @@ -596,6 +641,16 @@ deleteAMMTrustLine( std::optional const& ammAccountID, beast::Journal j); +/** Delete AMMs MPToken. The passed `sle` must be obtained from a prior + * call to view.peek(). + */ +[[nodiscard]] TER +deleteAMMMPToken( + ApplyView& view, + std::shared_ptr sleMPT, + AccountID const& ammAccountID, + beast::Journal j); + } // namespace ripple #endif diff --git a/src/xrpld/ledger/detail/PaymentSandbox.cpp b/src/xrpld/ledger/detail/PaymentSandbox.cpp index 745d8a90c7a..d931cc1465e 100644 --- a/src/xrpld/ledger/detail/PaymentSandbox.cpp +++ b/src/xrpld/ledger/detail/PaymentSandbox.cpp @@ -55,7 +55,7 @@ DeferredCredits::credit( !amount.negative(), "ripple::detail::DeferredCredits::credit : positive amount"); - auto const k = makeKey(sender, receiver, amount.getCurrency()); + auto const k = makeKey(sender, receiver, amount.get().currency); auto i = credits_.find(k); if (i == credits_.end()) { @@ -188,7 +188,7 @@ PaymentSandbox::balanceHook( magnitudes, (B+C)-C may not equal B. */ - auto const currency = amount.getCurrency(); + auto const currency = amount.get().currency; auto delta = amount.zeroed(); auto lastBal = amount; @@ -374,7 +374,7 @@ PaymentSandbox::balanceChanges(ReadView const& view) const } // The following are now set, put them in the map auto delta = newBalance - oldBalance; - auto const cur = newBalance.getCurrency(); + auto const cur = newBalance.get().currency; result[std::make_tuple(lowID, highID, cur)] = delta; auto r = result.emplace(std::make_tuple(lowID, lowID, cur), delta); if (r.second) diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 85abf7fc62c..ab54ffcb214 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -297,11 +297,10 @@ bool isLPTokenFrozen( ReadView const& view, AccountID const& account, - Issue const& asset, - Issue const& asset2) + Asset const& asset, + Asset const& asset2) { - return isFrozen(view, account, asset.currency, asset.account) || - isFrozen(view, account, asset2.currency, asset2.account); + return isFrozen(view, account, asset) || isFrozen(view, account, asset2); } STAmount @@ -353,8 +352,8 @@ accountHolds( isLPTokenFrozen( view, account, - (*sleAmm)[sfAsset].get(), - (*sleAmm)[sfAsset2].get())) + (*sleAmm)[sfAsset], + (*sleAmm)[sfAsset2])) { return false; } @@ -438,6 +437,26 @@ accountHolds( return amount; } +STAmount +accountHolds( + ReadView const& view, + AccountID const& account, + Asset const& issue, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j) +{ + return std::visit( + [&](TIss const& issue_) { + if constexpr (std::is_same_v) + return accountHolds(view, account, issue_, zeroIfFrozen, j); + else + return accountHolds( + view, account, issue_, zeroIfFrozen, zeroIfUnauthorized, j); + }, + issue.value()); +} + STAmount accountFunds( ReadView const& view, @@ -452,12 +471,28 @@ accountFunds( return accountHolds( view, id, - saDefault.getCurrency(), + saDefault.get().currency, saDefault.getIssuer(), freezeHandling, 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. @@ -1000,7 +1035,8 @@ trustCreate( sleRippleState->setFieldAmount( bSetHigh ? sfLowLimit : sfHighLimit, STAmount(Issue{ - saBalance.getCurrency(), bSetDst ? uSrcAccountID : uDstAccountID})); + saBalance.get().currency, + bSetDst ? uSrcAccountID : uDstAccountID})); if (uQualityIn) sleRippleState->setFieldU32( @@ -1138,7 +1174,7 @@ rippleCreditIOU( beast::Journal j) { AccountID const& issuer = saAmount.getIssuer(); - Currency const& currency = saAmount.getCurrency(); + Currency const& currency = saAmount.get().currency; // Make sure issuer is involved. XRPL_ASSERT( @@ -1692,7 +1728,8 @@ issueIOU( "ripple::issueIOU : neither account nor issuer is XRP"); // Consistency check - XRPL_ASSERT(issue == amount.issue(), "ripple::issueIOU : matching issue"); + XRPL_ASSERT( + issue == amount.get(), "ripple::issueIOU : matching issue"); // Can't send to self! XRPL_ASSERT( @@ -1792,7 +1829,8 @@ redeemIOU( "ripple::redeemIOU : neither account nor issuer is XRP"); // Consistency check - XRPL_ASSERT(issue == amount.issue(), "ripple::redeemIOU : matching issue"); + XRPL_ASSERT( + issue == amount.get(), "ripple::redeemIOU : matching issue"); // Can't send to self! XRPL_ASSERT( @@ -1919,7 +1957,8 @@ TER requireAuth( ReadView const& view, MPTIssue const& mptIssue, - AccountID const& account) + AccountID const& account, + MPTAuthType authType) { auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); auto const sleIssuance = view.read(mptID); @@ -1937,12 +1976,12 @@ requireAuth( auto const sleToken = view.read(mptokenID); // if account has no MPToken, fail - if (!sleToken) + if (!sleToken && authType == MPTAuthType::StrongAuth) return tecNO_AUTH; // mptoken must be authorized if issuance enabled requireAuth if (sleIssuance->getFieldU32(sfFlags) & lsfMPTRequireAuth && - !(sleToken->getFlags() & lsfMPTAuthorized)) + (!sleToken || (!(sleToken->getFlags() & lsfMPTAuthorized)))) return tecNO_AUTH; return tesSUCCESS; @@ -2095,6 +2134,25 @@ deleteAMMTrustLine( return tesSUCCESS; } +TER +deleteAMMMPToken( + ApplyView& view, + std::shared_ptr sleMpt, + AccountID const& ammAccountID, + beast::Journal j) +{ + if (!view.dirRemove( + keylet::ownerDir(ammAccountID), + (*sleMpt)[sfOwnerNode], + sleMpt->key(), + false)) + return tecINTERNAL; + + view.erase(sleMpt); + + return tesSUCCESS; +} + TER rippleCredit( ApplyView& view, diff --git a/src/xrpld/rpc/BookChanges.h b/src/xrpld/rpc/BookChanges.h index c87fa0ccf4e..44d408fa469 100644 --- a/src/xrpld/rpc/BookChanges.h +++ b/src/xrpld/rpc/BookChanges.h @@ -121,8 +121,8 @@ computeBookChanges(std::shared_ptr const& lpAccepted) STAmount deltaPays = finalFields.getFieldAmount(sfTakerPays) - previousFields.getFieldAmount(sfTakerPays); - std::string g{to_string(deltaGets.issue())}; - std::string p{to_string(deltaPays.issue())}; + std::string g{to_string(deltaGets.asset())}; + std::string p{to_string(deltaPays.asset())}; bool const noswap = isXRP(deltaGets) ? true : (isXRP(deltaPays) ? false : (g < p)); @@ -190,6 +190,16 @@ computeBookChanges(std::shared_ptr const& lpAccepted) jvObj[jss::changes] = Json::arrayValue; + auto volToStr = [](STAmount const& vol) { + if (vol.holds()) + { + if (isXRP(vol)) + return to_string(vol.xrp()); + return to_string(vol.iou()); + } + return to_string(vol.mpt()); + }; + for (auto const& entry : tally) { Json::Value& inner = jvObj[jss::changes].append(Json::objectValue); @@ -197,15 +207,21 @@ computeBookChanges(std::shared_ptr const& lpAccepted) STAmount volA = std::get<0>(entry.second); STAmount volB = std::get<1>(entry.second); - inner[jss::currency_a] = - (isXRP(volA) ? "XRP_drops" : to_string(volA.issue())); - inner[jss::currency_b] = - (isXRP(volB) ? "XRP_drops" : to_string(volB.issue())); - - inner[jss::volume_a] = - (isXRP(volA) ? to_string(volA.xrp()) : to_string(volA.iou())); - inner[jss::volume_b] = - (isXRP(volB) ? to_string(volB.xrp()) : to_string(volB.iou())); + // Do we need mpt_issuance_id_a, etc if MPT? + if (volA.holds()) + inner[jss::currency_a] = + (isXRP(volA) ? "XRP_drops" : to_string(volA.asset())); + else + inner[jss::mpt_issuance_id_a] = to_string(volA.asset()); + + if (volB.holds()) + inner[jss::currency_b] = + (isXRP(volB) ? "XRP_drops" : to_string(volB.asset())); + else + inner[jss::mpt_issuance_id_b] = to_string(volB.asset()); + + inner[jss::volume_a] = volToStr(volA); + inner[jss::volume_b] = volToStr(volB); inner[jss::high] = to_string(std::get<2>(entry.second).iou()); inner[jss::low] = to_string(std::get<3>(entry.second).iou()); diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h b/src/xrpld/rpc/MPTokenIssuanceID.h index ef194bd398c..87e9ff73147 100644 --- a/src/xrpld/rpc/MPTokenIssuanceID.h +++ b/src/xrpld/rpc/MPTokenIssuanceID.h @@ -45,7 +45,7 @@ canHaveMPTokenIssuanceID( std::shared_ptr const& serializedTx, TxMeta const& transactionMeta); -std::optional +std::optional getIDFromCreatedIssuance(TxMeta const& transactionMeta); void diff --git a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp index 721be652622..8b4da3988c0 100644 --- a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp +++ b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp @@ -47,7 +47,7 @@ canHaveMPTokenIssuanceID( return true; } -std::optional +std::optional getIDFromCreatedIssuance(TxMeta const& transactionMeta) { for (STObject const& node : transactionMeta.getNodes()) @@ -74,7 +74,7 @@ insertMPTokenIssuanceID( if (!canHaveMPTokenIssuanceID(transaction, transactionMeta)) return; - std::optional result = getIDFromCreatedIssuance(transactionMeta); + std::optional result = getIDFromCreatedIssuance(transactionMeta); if (result) response[jss::mpt_issuance_id] = to_string(result.value()); } diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index e99f175cbe7..145ea4fd662 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -36,6 +36,7 @@ #include +#include #include namespace ripple { @@ -1136,5 +1137,68 @@ getLedgerByContext(RPC::JsonContext& context) return RPC::make_error( rpcNOT_READY, "findCreate failed to return an inbound ledger"); } + +error_code_i +parseSubUnsubJson( + Asset& asset, + Json::Value const& params, + Json::StaticString const& name, + beast::Journal j) +{ + auto const& jv = params[name]; + auto const [issuerError, assetError] = [&]() { + if (name == jss::taker_pays) + return std::make_pair(rpcSRC_ISR_MALFORMED, rpcSRC_CUR_MALFORMED); + return std::make_pair(rpcDST_ISR_MALFORMED, rpcDST_AMT_MALFORMED); + }(); + + if (jv.isMember(jss::mpt_issuance_id) && + (jv.isMember(jss::currency) || jss::issuer)) + { + JLOG(j.info()) << std::format("Bad {} currency or MPT.", name.c_str()); + return rpcINVALID_PARAMS; + } + + if (jv.isMember(jss::currency)) + { + Issue issue = xrpIssue(); + // Parse mandatory currency. + if (!jv.isMember(jss::currency) || + !to_currency(issue.currency, jv[jss::currency].asString())) + { + JLOG(j.info()) << std::format("Bad {} currency.", name.c_str()); + return assetError; + } + + // Parse optional issuer. + if (((jv.isMember(jss::issuer)) && + (!jv[jss::issuer].isString() || + !to_issuer(issue.account, jv[jss::issuer].asString()))) + // Don't allow illegal issuers. + || (!issue.currency != !issue.account) || + noAccount() == issue.account) + { + JLOG(j.info()) << std::format("Bad {} issuer.", name.c_str()); + return issuerError; + } + asset = issue; + } + else if (jv.isMember(jss::mpt_issuance_id)) + { + MPTID mptid; + if (!mptid.parseHex(jv[jss::mpt_issuance_id].asString())) + return assetError; + asset = mptid; + } + else + { + JLOG(j.info()) << std::format( + "Neither {} currency or MPT is present.", name.c_str()); + return assetError; + } + + return rpcSUCCESS; +} + } // namespace RPC } // namespace ripple diff --git a/src/xrpld/rpc/detail/RPCHelpers.h b/src/xrpld/rpc/detail/RPCHelpers.h index 74171af28e3..026562f7341 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.h +++ b/src/xrpld/rpc/detail/RPCHelpers.h @@ -270,6 +270,16 @@ keypairForSignature( Json::Value const& params, Json::Value& error, unsigned int apiVersion = apiVersionIfUnspecified); + +/** Parse subscribe/unsubscribe parameters + */ +error_code_i +parseSubUnsubJson( + Asset& asset, + Json::Value const& jv, + Json::StaticString const& name, + beast::Journal j); + } // namespace RPC } // namespace ripple diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 376a0ce24a5..7f96f5e4ad2 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -213,7 +213,9 @@ checkPayment( return RPC::invalid_field_error("tx_json.Destination"); if (params.isMember(jss::build_path) && - ((doPath == false) || amount.holds())) + ((doPath == false) || + (!app.openLedger().current()->rules().enabled(featureMPTokensV2) && + amount.holds()))) return RPC::make_error( rpcINVALID_PARAMS, "Field 'build_path' not allowed in this context."); @@ -234,9 +236,10 @@ checkPayment( } else { - // If no SendMax, default to Amount with sender as issuer. + // If no SendMax, default to Amount with sender as issuer if Issue. sendMax = amount; - sendMax.setIssuer(srcAddressID); + if (sendMax.holds()) + sendMax.setIssuer(srcAddressID); } if (sendMax.native() && amount.native()) @@ -253,12 +256,12 @@ checkPayment( if (auto ledger = app.openLedger().current()) { Pathfinder pf( - std::make_shared( - ledger, app.journal("RippleLineCache")), + std::make_shared( + ledger, app.journal("AssetCache")), srcAddressID, *dstAccountID, - sendMax.issue().currency, - sendMax.issue().account, + sendMax.asset(), + sendMax.getIssuer(), amount, std::nullopt, app); @@ -269,7 +272,7 @@ checkPayment( STPath fullLiquidityPath; STPathSet paths; result = pf.getBestPaths( - 4, fullLiquidityPath, paths, sendMax.issue().account); + 4, fullLiquidityPath, paths, sendMax.getIssuer()); } } diff --git a/src/xrpld/rpc/handlers/AMMInfo.cpp b/src/xrpld/rpc/handlers/AMMInfo.cpp index 1990cdafd3e..44bbd3a6676 100644 --- a/src/xrpld/rpc/handlers/AMMInfo.cpp +++ b/src/xrpld/rpc/handlers/AMMInfo.cpp @@ -45,16 +45,16 @@ getAccount(Json::Value const& v, Json::Value& result) return std::optional(accountID); } -Expected -getIssue(Json::Value const& v, beast::Journal j) +Expected +getAsset(Json::Value const& v, beast::Journal j) { try { - return issueFromJson(v); + return assetFromJson(v); } catch (std::runtime_error const& ex) { - JLOG(j.debug()) << "getIssue " << ex.what(); + JLOG(j.debug()) << "getAsset " << ex.what(); } return Unexpected(rpcISSUE_MALFORMED); } @@ -84,16 +84,16 @@ doAMMInfo(RPC::JsonContext& context) struct ValuesFromContextParams { std::optional accountID; - Issue issue1; - Issue issue2; + Asset asset1; + Asset asset2; std::shared_ptr amm; }; auto getValuesFromContextParams = [&]() -> Expected { std::optional accountID; - std::optional issue1; - std::optional issue2; + std::optional asset1; + std::optional asset2; std::optional ammID; constexpr auto invalid = [](Json::Value const& params) -> bool { @@ -109,16 +109,16 @@ doAMMInfo(RPC::JsonContext& context) if (params.isMember(jss::asset)) { - if (auto const i = getIssue(params[jss::asset], context.j)) - issue1 = *i; + if (auto const i = getAsset(params[jss::asset], context.j)) + asset1 = *i; else return Unexpected(i.error()); } if (params.isMember(jss::asset2)) { - if (auto const i = getIssue(params[jss::asset2], context.j)) - issue2 = *i; + if (auto const i = getAsset(params[jss::asset2], context.j)) + asset2 = *i; else return Unexpected(i.error()); } @@ -148,27 +148,27 @@ doAMMInfo(RPC::JsonContext& context) return Unexpected(rpcINVALID_PARAMS); XRPL_ASSERT( - (issue1.has_value() == issue2.has_value()) && - (issue1.has_value() != ammID.has_value()), - "ripple::doAMMInfo : issue1 and issue2 do match"); + (asset1.has_value() == asset2.has_value()) && + (asset1.has_value() != ammID.has_value()), + "ripple::doAMMInfo : asset1 and asset2 do match"); auto const ammKeylet = [&]() { - if (issue1 && issue2) - return keylet::amm(*issue1, *issue2); + if (asset1 && asset2) + return keylet::amm(*asset1, *asset2); XRPL_ASSERT(ammID, "ripple::doAMMInfo::ammKeylet : ammID is set"); return keylet::amm(*ammID); }(); auto const amm = ledger->read(ammKeylet); if (!amm) return Unexpected(rpcACT_NOT_FOUND); - if (!issue1 && !issue2) + if (!asset1 && !asset2) { - issue1 = (*amm)[sfAsset].get(); - issue2 = (*amm)[sfAsset2].get(); + asset1 = (*amm)[sfAsset]; + asset2 = (*amm)[sfAsset2]; } return ValuesFromContextParams{ - accountID, *issue1, *issue2, std::move(amm)}; + accountID, *asset1, *asset2, std::move(amm)}; }; auto const r = getValuesFromContextParams(); @@ -178,7 +178,7 @@ doAMMInfo(RPC::JsonContext& context) return result; } - auto const& [accountID, issue1, issue2, amm] = *r; + auto const& [accountID, asset1, asset2, amm] = *r; auto const ammAccountID = amm->getAccountID(sfAccount); @@ -186,9 +186,10 @@ doAMMInfo(RPC::JsonContext& context) auto const [asset1Balance, asset2Balance] = ammPoolHolds( *ledger, ammAccountID, - issue1, - issue2, + asset1, + asset2, FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, context.j); auto const lptAMMBalance = accountID ? ammLPHolds(*ledger, *amm, *accountID, context.j) @@ -253,11 +254,9 @@ doAMMInfo(RPC::JsonContext& context) } if (!isXRP(asset1Balance)) - ammResult[jss::asset_frozen] = - isFrozen(*ledger, ammAccountID, issue1.currency, issue1.account); + ammResult[jss::asset_frozen] = isFrozen(*ledger, ammAccountID, asset1); if (!isXRP(asset2Balance)) - ammResult[jss::asset2_frozen] = - isFrozen(*ledger, ammAccountID, issue2.currency, issue2.account); + ammResult[jss::asset2_frozen] = isFrozen(*ledger, ammAccountID, asset2); result[jss::amm] = std::move(ammResult); if (!result.isMember(jss::ledger_index) && diff --git a/src/xrpld/rpc/handlers/AccountCurrenciesHandler.cpp b/src/xrpld/rpc/handlers/AccountCurrenciesHandler.cpp index 6c8fe282674..02d6eeb0aad 100644 --- a/src/xrpld/rpc/handlers/AccountCurrenciesHandler.cpp +++ b/src/xrpld/rpc/handlers/AccountCurrenciesHandler.cpp @@ -74,9 +74,9 @@ doAccountCurrencies(RPC::JsonContext& context) STAmount const& saBalance = rspEntry.getBalance(); if (saBalance < rspEntry.getLimit()) - receive.insert(saBalance.getCurrency()); + receive.insert(saBalance.get().currency); if ((-saBalance) < rspEntry.getLimitPeer()) - send.insert(saBalance.getCurrency()); + send.insert(saBalance.get().currency); } send.erase(badCurrency()); diff --git a/src/xrpld/rpc/handlers/AccountLines.cpp b/src/xrpld/rpc/handlers/AccountLines.cpp index 5170342eb99..8b996c9711d 100644 --- a/src/xrpld/rpc/handlers/AccountLines.cpp +++ b/src/xrpld/rpc/handlers/AccountLines.cpp @@ -45,7 +45,7 @@ addLine(Json::Value& jsonLines, RPCTrustLine const& line) // Amount reported is negative if other account holds current // account's IOUs. jPeer[jss::balance] = saBalance.getText(); - jPeer[jss::currency] = to_string(saBalance.issue().currency); + jPeer[jss::currency] = to_string(saBalance.get().currency); jPeer[jss::limit] = saLimit.getText(); jPeer[jss::limit_peer] = saLimitPeer.getText(); jPeer[jss::quality_in] = line.getQualityIn().value; diff --git a/src/xrpld/rpc/handlers/BookOffers.cpp b/src/xrpld/rpc/handlers/BookOffers.cpp index dccc03de5be..1cb4c323853 100644 --- a/src/xrpld/rpc/handlers/BookOffers.cpp +++ b/src/xrpld/rpc/handlers/BookOffers.cpp @@ -30,8 +30,133 @@ #include #include +#include + namespace ripple { +std::optional +validateTakerJSON(Json::Value const& taker, Json::StaticString const& name) +{ + if (!taker.isMember(jss::currency) && !taker.isMember(jss::mpt_issuance_id)) + return RPC::missing_field_error( + std::format("{}.currency", name.c_str())); + + if (taker.isMember(jss::mpt_issuance_id) && + (taker.isMember(jss::currency) || taker.isMember(jss::issuer))) + return RPC::invalid_field_error(name.c_str()); + + if ((taker.isMember(jss::currency) && !taker[jss::currency].isString()) || + (taker.isMember(jss::mpt_issuance_id) && + !taker[jss::mpt_issuance_id].isString())) + return RPC::expected_field_error( + std::format("{}.currency", name.c_str()), "string"); + + return std::nullopt; +} + +std::optional +parseTakerAssetJSON( + Asset& asset, + Json::Value const& taker, + Json::StaticString const& name, + beast::Journal j) +{ + auto const assetError = [&]() { + if (name == jss::taker_pays) + return rpcSRC_CUR_MALFORMED; + return rpcDST_AMT_MALFORMED; + }(); + + if (taker.isMember(jss::currency)) + { + Issue issue = xrpIssue(); + + if (!to_currency(issue.currency, taker[jss::currency].asString())) + { + JLOG(j.info()) << std::format("Bad {} currency.", name.c_str()); + return RPC::make_error( + assetError, + std::format( + "Invalid field '{}.currency', bad currency.", + name.c_str())); + } + asset = issue; + } + else if (taker.isMember(jss::mpt_issuance_id)) + { + MPTID mptid; + if (!mptid.parseHex(taker[jss::mpt_issuance_id].asString())) + return RPC::make_error( + assetError, + std::format( + "Invalid field '{}.mpt_issuance_id'", name.c_str())); + asset = mptid; + } + + return std::nullopt; +} + +std::optional +parseTakerIssuerJSON( + Asset& asset, + Json::Value const& taker, + Json::StaticString const& name, + beast::Journal j) +{ + auto const issuerError = [&]() { + if (name == jss::taker_pays) + return rpcSRC_ISR_MALFORMED; + return rpcDST_ISR_MALFORMED; + }(); + + if (taker.isMember(jss::currency)) + { + Issue& issue = asset.get(); + + if (taker.isMember(jss::issuer)) + { + if (!taker[jss::issuer].isString()) + return RPC::expected_field_error( + std::format("{}.issuer", name.c_str()), "string"); + + if (!to_issuer(issue.account, taker[jss::issuer].asString())) + return RPC::make_error( + issuerError, + std::format( + "Invalid field '{}.issuer', bad issuer.", + name.c_str())); + + if (issue.account == noAccount()) + return RPC::make_error( + issuerError, + std::format( + "Invalid field '{}.issuer', bad issuer account one.", + name.c_str())); + } + else + { + issue.account = xrpAccount(); + } + + if (isXRP(issue.currency) && !isXRP(issue.account)) + return RPC::make_error( + issuerError, + std::format( + "Unneeded field '{}.issuer' for XRP currency " + "specification.", + name.c_str())); + + if (!isXRP(issue.currency) && isXRP(issue.account)) + return RPC::make_error( + issuerError, + std::format( + "Invalid field '{}.issuer', expected non-XRP issuer.", + name.c_str())); + } + + return std::nullopt; +} + Json::Value doBookOffers(RPC::JsonContext& context) { @@ -62,103 +187,29 @@ doBookOffers(RPC::JsonContext& context) 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( - 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."); + if (auto const err = validateTakerJSON(taker_pays, jss::taker_pays)) + return *err; - AccountID get_issuer; + if (auto const err = validateTakerJSON(taker_gets, jss::taker_gets)) + return *err; - if (taker_gets.isMember(jss::issuer)) - { - if (!taker_gets[jss::issuer].isString()) - return RPC::expected_field_error("taker_gets.issuer", "string"); + Book book; - 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 (auto const err = parseTakerAssetJSON( + book.in, taker_pays, jss::taker_pays, context.j)) + return *err; - 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 (auto const err = parseTakerAssetJSON( + book.out, taker_gets, jss::taker_gets, context.j)) + return *err; - if (isXRP(get_currency) && !isXRP(get_issuer)) - return RPC::make_error( - rpcDST_ISR_MALFORMED, - "Unneeded field 'taker_gets.issuer' for " - "XRP currency specification."); + if (auto const err = parseTakerIssuerJSON( + book.in, taker_pays, jss::taker_pays, context.j)) + return *err; - if (!isXRP(get_currency) && isXRP(get_issuer)) - return RPC::make_error( - rpcDST_ISR_MALFORMED, - "Invalid field 'taker_gets.issuer', expected non-XRP issuer."); + if (auto const err = parseTakerIssuerJSON( + book.out, taker_gets, jss::taker_gets, context.j)) + return *err; std::optional takerID; if (context.params.isMember(jss::taker)) @@ -171,7 +222,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 +240,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/GatewayBalances.cpp b/src/xrpld/rpc/handlers/GatewayBalances.cpp index 26f338cb4f9..08634eedbbf 100644 --- a/src/xrpld/rpc/handlers/GatewayBalances.cpp +++ b/src/xrpld/rpc/handlers/GatewayBalances.cpp @@ -180,7 +180,7 @@ doGatewayBalances(RPC::JsonContext& context) else { // normal negative balance, obligation to customer - auto& bal = sums[rs->getBalance().getCurrency()]; + auto& bal = sums[rs->getBalance().get().currency]; if (bal == beast::zero) { // This is needed to set the currency code correctly @@ -199,7 +199,7 @@ doGatewayBalances(RPC::JsonContext& context) // Very large sums of STAmount are approximations // anyway. bal = STAmount( - bal.issue(), + bal.asset(), STAmount::cMaxValue, STAmount::cMaxOffset); } @@ -232,7 +232,7 @@ doGatewayBalances(RPC::JsonContext& context) { Json::Value entry; entry[jss::currency] = - to_string(balance.issue().currency); + to_string(balance.get().currency); entry[jss::value] = balance.getText(); balanceArray.append(std::move(entry)); } diff --git a/src/xrpld/rpc/handlers/NoRippleCheck.cpp b/src/xrpld/rpc/handlers/NoRippleCheck.cpp index 94830a4f397..dcf02bae70d 100644 --- a/src/xrpld/rpc/handlers/NoRippleCheck.cpp +++ b/src/xrpld/rpc/handlers/NoRippleCheck.cpp @@ -178,7 +178,7 @@ doNoRippleCheck(RPC::JsonContext& context) .getIssuer(); STAmount peerLimit = ownedItem->getFieldAmount( bLow ? sfHighLimit : sfLowLimit); - problem += to_string(peerLimit.getCurrency()); + problem += to_string(peerLimit.get().currency); problem += " line to "; problem += to_string(peerLimit.getIssuer()); problems.append(problem); diff --git a/src/xrpld/rpc/handlers/Subscribe.cpp b/src/xrpld/rpc/handlers/Subscribe.cpp index 66fe89dea04..4e6bf20ebd9 100644 --- a/src/xrpld/rpc/handlers/Subscribe.cpp +++ b/src/xrpld/rpc/handlers/Subscribe.cpp @@ -241,55 +241,18 @@ doSubscribe(RPC::JsonContext& context) 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); - } + if (auto const err = RPC::parseSubUnsubJson( + book.in, j, jss::taker_pays, context.j); + err != rpcSUCCESS) + return rpcError(err); - // 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 (auto const err = RPC::parseSubUnsubJson( + book.out, j, jss::taker_gets, context.j); + err != rpcSUCCESS) + return rpcError(err); - if (book.in.currency == book.out.currency && - book.in.account == book.out.account) + if (book.in == book.out) { JLOG(context.j.info()) << "taker_gets same as taker_pays."; return rpcError(rpcBAD_MARKET); diff --git a/src/xrpld/rpc/handlers/Unsubscribe.cpp b/src/xrpld/rpc/handlers/Unsubscribe.cpp index bab0d99744c..5b48ce8d387 100644 --- a/src/xrpld/rpc/handlers/Unsubscribe.cpp +++ b/src/xrpld/rpc/handlers/Unsubscribe.cpp @@ -173,55 +173,17 @@ doUnsubscribe(RPC::JsonContext& context) 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."; + if (auto const err = RPC::parseSubUnsubJson( + book.in, jv, jss::taker_pays, context.j); + err != rpcSUCCESS) + return rpcError(err); - 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); - } + if (auto const err = RPC::parseSubUnsubJson( + book.out, jv, jss::taker_gets, context.j); + err != rpcSUCCESS) + return rpcError(err); if (book.in == book.out) {