From ba5f67ca490973c4108922da546047559e289b1d Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Fri, 28 Jun 2024 19:22:55 -0400 Subject: [PATCH] Introduce MPT support (XLS-33d): New Transactions: - MPTokenIssuanceCreate - MPTokenIssuanceDestory - MPTokenIssuanceSet - MPTokenAuthorize Modified Transactions: - Payment - Clawback New Objects: - MPTokenIssuance - MPTokenAuthorize API updates: - ledger_entry - account_objects - ledger_data Read full spec: https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0033d-multi-purpose-tokens --------- Co-authored-by: Shawn Xie Co-authored-by: Howard Hinnant --- Builds/levelization/results/loops.txt | 2 +- include/xrpl/basics/MPTAmount.h | 185 ++ include/xrpl/basics/Number.h | 7 + include/xrpl/basics/base_uint.h | 2 + include/xrpl/protocol/AmountConversions.h | 8 +- include/xrpl/protocol/Asset.h | 149 ++ include/xrpl/protocol/Feature.h | 3 +- include/xrpl/protocol/Indexes.h | 27 + include/xrpl/protocol/Issue.h | 17 +- include/xrpl/protocol/LedgerFormats.h | 25 + include/xrpl/protocol/MPTIssue.h | 77 + include/xrpl/protocol/Protocol.h | 6 + include/xrpl/protocol/SField.h | 11 + include/xrpl/protocol/SOTemplate.h | 31 +- include/xrpl/protocol/STAmount.h | 246 ++- include/xrpl/protocol/STBitString.h | 8 + include/xrpl/protocol/STObject.h | 2 + include/xrpl/protocol/Serializer.h | 6 + include/xrpl/protocol/TER.h | 10 +- include/xrpl/protocol/TxFlags.h | 31 + include/xrpl/protocol/TxFormats.h | 11 + include/xrpl/protocol/UintTypes.h | 3 + include/xrpl/protocol/jss.h | 234 +-- src/libxrpl/basics/MPTAmount.cpp | 85 + src/libxrpl/basics/Number.cpp | 5 + src/libxrpl/protocol/Asset.cpp | 93 + src/libxrpl/protocol/Feature.cpp | 1 + src/libxrpl/protocol/Indexes.cpp | 37 + src/libxrpl/protocol/Issue.cpp | 10 + src/libxrpl/protocol/LedgerFormats.cpp | 30 + src/libxrpl/protocol/MPTIssue.cpp | 60 + src/libxrpl/protocol/Quality.cpp | 10 +- src/libxrpl/protocol/Rate2.cpp | 4 +- src/libxrpl/protocol/SField.cpp | 10 + src/libxrpl/protocol/STAmount.cpp | 472 ++--- src/libxrpl/protocol/STInteger.cpp | 24 +- src/libxrpl/protocol/STObject.cpp | 6 + src/libxrpl/protocol/STParsedJSON.cpp | 34 +- src/libxrpl/protocol/STTx.cpp | 44 +- src/libxrpl/protocol/STVar.cpp | 6 + src/libxrpl/protocol/TER.cpp | 6 + src/libxrpl/protocol/TxFormats.cpp | 38 +- src/test/app/Clawback_test.cpp | 1 + src/test/app/Flow_test.cpp | 4 - src/test/app/MPToken_test.cpp | 1695 +++++++++++++++++ src/test/app/SetAuth_test.cpp | 4 +- src/test/app/TrustAndBalance_test.cpp | 1 - src/test/jtx.h | 1 + src/test/jtx/Env.h | 6 + src/test/jtx/amount.h | 79 +- src/test/jtx/impl/mpt.cpp | 398 ++++ src/test/jtx/impl/trust.cpp | 8 +- src/test/jtx/mpt.h | 264 +++ src/test/jtx/trust.h | 5 +- src/test/ledger/PaymentSandbox_test.cpp | 2 - src/test/protocol/Quality_test.cpp | 2 +- src/test/protocol/STAmount_test.cpp | 2 - src/test/protocol/STTx_test.cpp | 32 +- src/xrpld/app/ledger/detail/LedgerToJson.cpp | 13 + src/xrpld/app/misc/NetworkOPs.cpp | 3 + src/xrpld/app/paths/Credit.cpp | 4 +- src/xrpld/app/paths/PathRequest.cpp | 2 +- src/xrpld/app/paths/Pathfinder.cpp | 7 +- src/xrpld/app/tx/detail/Clawback.cpp | 162 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 204 +- src/xrpld/app/tx/detail/InvariantCheck.h | 28 +- src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 251 +++ src/xrpld/app/tx/detail/MPTokenAuthorize.h | 63 + .../app/tx/detail/MPTokenIssuanceCreate.cpp | 142 ++ .../app/tx/detail/MPTokenIssuanceCreate.h | 60 + .../app/tx/detail/MPTokenIssuanceDestroy.cpp | 82 + .../app/tx/detail/MPTokenIssuanceDestroy.h | 48 + .../app/tx/detail/MPTokenIssuanceSet.cpp | 118 ++ src/xrpld/app/tx/detail/MPTokenIssuanceSet.h | 48 + src/xrpld/app/tx/detail/Payment.cpp | 301 ++- src/xrpld/app/tx/detail/SetTrust.cpp | 2 +- src/xrpld/app/tx/detail/applySteps.cpp | 12 + src/xrpld/ledger/View.h | 61 + src/xrpld/ledger/detail/View.cpp | 298 ++- src/xrpld/rpc/MPTokenIssuanceID.h | 53 + src/xrpld/rpc/detail/MPTokenIssuanceID.cpp | 83 + src/xrpld/rpc/detail/RPCHelpers.cpp | 14 +- src/xrpld/rpc/detail/TransactionSign.cpp | 3 +- src/xrpld/rpc/detail/Tuning.h | 3 + src/xrpld/rpc/handlers/AccountObjects.cpp | 4 +- src/xrpld/rpc/handlers/AccountTx.cpp | 3 + src/xrpld/rpc/handlers/Handlers.h | 2 + src/xrpld/rpc/handlers/LedgerData.cpp | 4 + src/xrpld/rpc/handlers/LedgerEntry.cpp | 67 + src/xrpld/rpc/handlers/Tx.cpp | 2 + 90 files changed, 6178 insertions(+), 549 deletions(-) create mode 100644 include/xrpl/basics/MPTAmount.h create mode 100644 include/xrpl/protocol/Asset.h create mode 100644 include/xrpl/protocol/MPTIssue.h create mode 100644 src/libxrpl/basics/MPTAmount.cpp create mode 100644 src/libxrpl/protocol/Asset.cpp create mode 100644 src/libxrpl/protocol/MPTIssue.cpp create mode 100644 src/test/app/MPToken_test.cpp create mode 100644 src/test/jtx/impl/mpt.cpp create mode 100644 src/test/jtx/mpt.h create mode 100644 src/xrpld/app/tx/detail/MPTokenAuthorize.cpp create mode 100644 src/xrpld/app/tx/detail/MPTokenAuthorize.h create mode 100644 src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp create mode 100644 src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h create mode 100644 src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp create mode 100644 src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h create mode 100644 src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp create mode 100644 src/xrpld/app/tx/detail/MPTokenIssuanceSet.h create mode 100644 src/xrpld/rpc/MPTokenIssuanceID.h create mode 100644 src/xrpld/rpc/detail/MPTokenIssuanceID.cpp diff --git a/Builds/levelization/results/loops.txt b/Builds/levelization/results/loops.txt index f703a3a9d5d..fd5d6ffa04b 100644 --- a/Builds/levelization/results/loops.txt +++ b/Builds/levelization/results/loops.txt @@ -5,7 +5,7 @@ Loop: test.jtx test.unit_test test.unit_test == test.jtx Loop: xrpl.basics xrpl.json - xrpl.json ~= xrpl.basics + xrpl.json == xrpl.basics Loop: xrpld.app xrpld.core xrpld.app > xrpld.core diff --git a/include/xrpl/basics/MPTAmount.h b/include/xrpl/basics/MPTAmount.h new file mode 100644 index 00000000000..5f8f3dbbe76 --- /dev/null +++ b/include/xrpl/basics/MPTAmount.h @@ -0,0 +1,185 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_BASICS_INTEGRALAMOUNT_H_INCLUDED +#define RIPPLE_BASICS_INTEGRALAMOUNT_H_INCLUDED + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace ripple { + +class MPTAmount : private boost::totally_ordered, + private boost::additive, + private boost::equality_comparable, + private boost::additive +{ +public: + using value_type = std::int64_t; + +protected: + value_type value_; + +public: + MPTAmount() = default; + constexpr MPTAmount(MPTAmount const& other) = default; + constexpr MPTAmount& + operator=(MPTAmount const& other) = default; + + constexpr explicit MPTAmount(value_type value); + + constexpr MPTAmount& operator=(beast::Zero); + + MPTAmount& + operator+=(MPTAmount const& other); + + MPTAmount& + operator-=(MPTAmount const& other); + + MPTAmount + operator-() const; + + bool + operator==(MPTAmount const& other) const; + + bool + operator==(value_type other) const; + + bool + operator<(MPTAmount const& other) const; + + /** Returns true if the amount is not zero */ + explicit constexpr operator bool() const noexcept; + + /** Return the sign of the amount */ + constexpr int + signum() const noexcept; + + Json::Value + jsonClipped() const; + + /** Returns the underlying value. Code SHOULD NOT call this + function unless the type has been abstracted away, + e.g. in a templated function. + */ + constexpr value_type + value() const; + + friend std::istream& + operator>>(std::istream& s, MPTAmount& val); + + static MPTAmount + minPositiveAmount(); +}; + +constexpr MPTAmount::MPTAmount(value_type value) : value_(value) +{ +} + +constexpr MPTAmount& MPTAmount::operator=(beast::Zero) +{ + value_ = 0; + return *this; +} + +/** Returns true if the amount is not zero */ +constexpr MPTAmount::operator bool() const noexcept +{ + return value_ != 0; +} + +/** Return the sign of the amount */ +constexpr int +MPTAmount::signum() const noexcept +{ + return (value_ < 0) ? -1 : (value_ ? 1 : 0); +} + +/** Returns the underlying value. Code SHOULD NOT call this + function unless the type has been abstracted away, + e.g. in a templated function. +*/ +constexpr MPTAmount::value_type +MPTAmount::value() const +{ + return value_; +} + +inline std::istream& +operator>>(std::istream& s, MPTAmount& val) +{ + s >> val.value_; + return s; +} + +// Output MPTAmount as just the value. +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const MPTAmount& q) +{ + return os << q.value(); +} + +inline std::string +to_string(MPTAmount const& amount) +{ + return std::to_string(amount.value()); +} + +inline MPTAmount +mulRatio( + MPTAmount const& amt, + std::uint32_t num, + std::uint32_t den, + bool roundUp) +{ + using namespace boost::multiprecision; + + if (!den) + Throw("division by zero"); + + int128_t const amt128(amt.value()); + auto const neg = amt.value() < 0; + auto const m = amt128 * num; + auto r = m / den; + if (m % den) + { + if (!neg && roundUp) + r += 1; + if (neg && !roundUp) + r -= 1; + } + if (r > std::numeric_limits::max()) + Throw("MPT mulRatio overflow"); + return MPTAmount(r.convert_to()); +} + +} // namespace ripple + +#endif // RIPPLE_BASICS_INTEGRALAMOUNT_H_INCLUDED diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 30ce3f73173..89c15f7d1b8 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -20,6 +20,7 @@ #ifndef RIPPLE_BASICS_NUMBER_H_INCLUDED #define RIPPLE_BASICS_NUMBER_H_INCLUDED +#include #include #include #include @@ -52,6 +53,7 @@ class Number explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept; Number(XRPAmount const& x); + Number(MPTAmount const& x); constexpr rep mantissa() const noexcept; @@ -89,6 +91,7 @@ class Number lowest() noexcept; explicit operator XRPAmount() const; // round to nearest, even on tie + explicit operator MPTAmount() const; // round to nearest, even on tie explicit operator rep() const; // round to nearest, even on tie friend constexpr bool @@ -210,6 +213,10 @@ inline Number::Number(XRPAmount const& x) : Number{x.drops()} { } +inline Number::Number(MPTAmount const& x) : Number{x.value()} +{ +} + inline constexpr Number::rep Number::mantissa() const noexcept { diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index 2b44d3072ee..0518ee37ea5 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -548,6 +548,7 @@ class base_uint using uint128 = base_uint<128>; using uint160 = base_uint<160>; using uint256 = base_uint<256>; +using uint192 = base_uint<192>; template [[nodiscard]] inline constexpr std::strong_ordering @@ -633,6 +634,7 @@ operator<<(std::ostream& out, base_uint const& u) #ifndef __INTELLISENSE__ static_assert(sizeof(uint128) == 128 / 8, "There should be no padding bytes"); static_assert(sizeof(uint160) == 160 / 8, "There should be no padding bytes"); +static_assert(sizeof(uint192) == 192 / 8, "There should be no padding bytes"); static_assert(sizeof(uint256) == 256 / 8, "There should be no padding bytes"); #endif diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h index 0348e3c975d..270d009b916 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -33,13 +33,7 @@ toSTAmount(IOUAmount const& iou, Issue const& iss) { bool const isNeg = iou.signum() < 0; std::uint64_t const umant = isNeg ? -iou.mantissa() : iou.mantissa(); - return STAmount( - iss, - umant, - iou.exponent(), - /*native*/ false, - isNeg, - STAmount::unchecked()); + return STAmount(iss, umant, iou.exponent(), isNeg, STAmount::unchecked()); } inline STAmount diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h new file mode 100644 index 00000000000..760b2439557 --- /dev/null +++ b/include/xrpl/protocol/Asset.h @@ -0,0 +1,149 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_PROTOCOL_ASSET_H_INCLUDED +#define RIPPLE_PROTOCOL_ASSET_H_INCLUDED + +#include +#include +#include + +namespace ripple { + +template +concept ValidIssueType = + std::is_same_v || std::is_same_v; + +class Asset +{ +private: + using value_type = std::variant; + value_type issue_; + +public: + Asset() = default; + + Asset(Issue const& issue); + + Asset(MPTIssue const& mpt); + + Asset(MPTID const& mpt); + + explicit operator Issue() const; + + explicit operator MPTIssue() const; + + AccountID const& + getIssuer() const; + + template + constexpr TIss const& + get() const; + + template + TIss& + get(); + + template + constexpr bool + holds() const; + + std::string + getText() const; + + constexpr value_type const& + value() const; + + void + setJson(Json::Value& jv) const; + + friend constexpr bool + operator==(Asset const& lhs, Asset const& rhs); + + friend constexpr bool + operator!=(Asset const& lhs, Asset const& rhs); +}; + +template +constexpr bool +Asset::holds() const +{ + return std::holds_alternative(issue_); +} + +template +constexpr TIss const& +Asset::get() const +{ + if (!std::holds_alternative(issue_)) + Throw("Asset is not a requested issue"); + return std::get(issue_); +} + +template +TIss& +Asset::get() +{ + if (!std::holds_alternative(issue_)) + Throw("Asset is not a requested issue"); + return std::get(issue_); +} + +constexpr Asset::value_type const& +Asset::value() const +{ + return issue_; +} + +constexpr bool +operator==(Asset const& lhs, Asset const& rhs) +{ + return std::visit( + [&]( + TLhs const& issLhs, TRhs const& issRhs) { + if constexpr (std::is_same_v) + return issLhs == issRhs; + else + return false; + }, + lhs.issue_, + rhs.issue_); +} + +constexpr bool +operator!=(Asset const& lhs, Asset const& rhs) +{ + return !(lhs == rhs); +} + +inline bool +isXRP(Asset const& asset) +{ + return asset.holds() && isXRP(asset.get()); +} + +std::string +to_string(Asset const& asset); + +bool +validJSONAsset(Json::Value const& jv); + +} // namespace ripple + +#endif // RIPPLE_PROTOCOL_ASSET_H_INCLUDED diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index a00d6b85c1b..b0c8860fb85 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -80,7 +80,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 79; +static constexpr std::size_t numFeatures = 80; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -372,6 +372,7 @@ extern uint256 const fixEnforceNFTokenTrustline; extern uint256 const fixInnerObjTemplate2; extern uint256 const featureInvariantsV1_1; extern uint256 const fixNFTokenPageLinks; +extern uint256 const featureMPTokensV1; } // namespace ripple diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index f179bbacfab..e9f4100008e 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -287,6 +287,30 @@ did(AccountID const& account) noexcept; Keylet oracle(AccountID const& account, std::uint32_t const& documentID) noexcept; +Keylet +mptIssuance(AccountID const& issuer, std::uint32_t seq) noexcept; + +Keylet +mptIssuance(MPTID const& mpt) noexcept; + +inline Keylet +mptIssuance(uint256 const& issuance) +{ + return {ltMPTOKEN_ISSUANCE, issuance}; +} + +Keylet +mptoken(MPTID const& issuanceID, AccountID const& holder) noexcept; + +inline Keylet +mptoken(uint256 const& mptokenKey) +{ + return {ltMPTOKEN, mptokenKey}; +} + +Keylet +mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept; + } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: @@ -327,6 +351,9 @@ std::array, 6> const directAccountKeylets{ {&keylet::nftpage_max, jss::NFTokenPage, true}, {&keylet::did, jss::DID, true}}}; +MPTID +getMptID(AccountID const& account, std::uint32_t sequence); + } // namespace ripple #endif diff --git a/include/xrpl/protocol/Issue.h b/include/xrpl/protocol/Issue.h index a18502f2138..0c046378ea0 100644 --- a/include/xrpl/protocol/Issue.h +++ b/include/xrpl/protocol/Issue.h @@ -38,13 +38,12 @@ class Issue Currency currency{}; AccountID account{}; - Issue() - { - } + Issue() = default; - Issue(Currency const& c, AccountID const& a) : currency(c), account(a) - { - } + Issue(Currency const& c, AccountID const& a); + + AccountID const& + getIssuer() const; std::string getText() const; @@ -116,6 +115,12 @@ noIssue() return issue; } +inline bool +isXRP(Issue const& issue) +{ + return issue == xrpIssue(); +} + } // namespace ripple #endif diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 0ee6c992d8d..464fbd3e125 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -196,6 +196,19 @@ enum LedgerEntryType : std::uint16_t \sa keylet::oracle */ ltORACLE = 0x0080, + + /** A ledger object representing an individual MPToken asset type, but not + * any balances of that asset itself. + + \sa keylet::mptIssuance + */ + ltMPTOKEN_ISSUANCE = 0x007e, + + /** A ledger object representing an individual MPToken balance. + + \sa keylet::mptoken + */ + ltMPTOKEN = 0x007f, //--------------------------------------------------------------------------- /** A special type, matching any ledger entry type. @@ -308,6 +321,18 @@ enum LedgerSpecificFlags { // ltNFTOKEN_OFFER lsfSellNFToken = 0x00000001, + + // ltMPTOKEN_ISSUANCE + lsfMPTLocked = 0x00000001, // Also used in ltMPTOKEN + lsfMPTCanLock = 0x00000002, + lsfMPTRequireAuth = 0x00000004, + lsfMPTCanEscrow = 0x00000008, + lsfMPTCanTrade = 0x00000010, + lsfMPTCanTransfer = 0x00000020, + lsfMPTCanClawback = 0x00000040, + + // ltMPTOKEN + lsfMPTAuthorized = 0x00000002, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h new file mode 100644 index 00000000000..5892e2b91c8 --- /dev/null +++ b/include/xrpl/protocol/MPTIssue.h @@ -0,0 +1,77 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_PROTOCOL_MPTISSUE_H_INCLUDED +#define RIPPLE_PROTOCOL_MPTISSUE_H_INCLUDED + +#include +#include + +namespace ripple { + +class MPTIssue +{ +private: + MPTID mptID_; + +public: + MPTIssue() = default; + + MPTIssue(MPTID const& id); + + AccountID const& + getIssuer() const; + + MPTID const& + getMptID() const; + + friend constexpr bool + operator==(MPTIssue const& lhs, MPTIssue const& rhs); + + friend constexpr bool + operator!=(MPTIssue const& lhs, MPTIssue const& rhs); +}; + +constexpr bool +operator==(MPTIssue const& lhs, MPTIssue const& rhs) +{ + return lhs.mptID_ == rhs.mptID_; +} + +constexpr bool +operator!=(MPTIssue const& lhs, MPTIssue const& rhs) +{ + return !(lhs.mptID_ == rhs.mptID_); +} + +inline bool +isXRP(MPTID const&) +{ + return false; +} + +Json::Value +to_json(MPTIssue const& issue); + +std::string +to_string(MPTIssue const& mpt); + +} // namespace ripple + +#endif // RIPPLE_PROTOCOL_MPTISSUE_H_INCLUDED diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 8d8a71dfef8..f706b6a3bbb 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -95,6 +95,12 @@ std::size_t constexpr maxDIDAttestationLength = 256; /** The maximum length of a domain */ std::size_t constexpr maxDomainLength = 256; +/** The maximum length of MPTokenMetadata */ +std::size_t constexpr maxMPTokenMetadataLength = 1024; + +/** The maximum amount of MPTokenIssuance */ +std::uint64_t constexpr maxMPTokenAmount = 0x7FFF'FFFF'FFFF'FFFFull; + /** A ledger index. */ using LedgerIndex = std::uint32_t; diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 7f54201a4b8..b7391d2971b 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -373,6 +373,7 @@ extern SF_UINT8 const sfScale; extern SF_UINT8 const sfTickSize; extern SF_UINT8 const sfUNLModifyDisabling; extern SF_UINT8 const sfHookResult; +extern SF_UINT8 const sfAssetScale; // 16-bit integers (common) extern SF_UINT16 const sfLedgerEntryType; @@ -467,6 +468,10 @@ extern SF_UINT64 const sfXChainClaimID; extern SF_UINT64 const sfXChainAccountCreateCount; extern SF_UINT64 const sfXChainAccountClaimCount; extern SF_UINT64 const sfAssetPrice; +extern SF_UINT64 const sfMaximumAmount; +extern SF_UINT64 const sfOutstandingAmount; +extern SF_UINT64 const sfLockedAmount; +extern SF_UINT64 const sfMPTAmount; // 128-bit extern SF_UINT128 const sfEmailHash; @@ -477,6 +482,9 @@ extern SF_UINT160 const sfTakerPaysIssuer; extern SF_UINT160 const sfTakerGetsCurrency; extern SF_UINT160 const sfTakerGetsIssuer; +// 192-bit (common) +extern SF_UINT192 const sfMPTokenIssuanceID; + // 256-bit (common) extern SF_UINT256 const sfLedgerHash; extern SF_UINT256 const sfParentHash; @@ -564,6 +572,7 @@ extern SF_VL const sfDIDDocument; extern SF_VL const sfData; extern SF_VL const sfAssetClass; extern SF_VL const sfProvider; +extern SF_VL const sfMPTokenMetadata; // variable length (uncommon) extern SF_VL const sfFulfillment; @@ -587,6 +596,7 @@ extern SF_ACCOUNT const sfUnauthorize; extern SF_ACCOUNT const sfRegularKey; extern SF_ACCOUNT const sfNFTokenMinter; extern SF_ACCOUNT const sfEmitCallback; +extern SF_ACCOUNT const sfMPTokenHolder; // account (uncommon) extern SF_ACCOUNT const sfHookAccount; @@ -651,6 +661,7 @@ extern SField const sfXChainClaimProofSig; extern SField const sfXChainCreateAccountProofSig; extern SField const sfXChainClaimAttestationCollectionElement; extern SField const sfXChainCreateAccountAttestationCollectionElement; +extern SField const MPToken; // array of objects (common) // ARRAY/1 is reserved for end of array diff --git a/include/xrpl/protocol/SOTemplate.h b/include/xrpl/protocol/SOTemplate.h index c0fcfb64358..51479353a95 100644 --- a/include/xrpl/protocol/SOTemplate.h +++ b/include/xrpl/protocol/SOTemplate.h @@ -39,6 +39,9 @@ enum SOEStyle { // constructed with STObject::makeInnerObject() }; +/** Amount fields that can support MPT */ +enum SOETxMPTAmount { soeMPTNone, soeMPTSupported, soeMPTNotSupported }; + //------------------------------------------------------------------------------ /** An element in a SOTemplate. */ @@ -47,10 +50,11 @@ class SOElement // Use std::reference_wrapper so SOElement can be stored in a std::vector. std::reference_wrapper sField_; SOEStyle style_; + SOETxMPTAmount supportMpt_; -public: - SOElement(SField const& fieldName, SOEStyle style) - : sField_(fieldName), style_(style) +private: + void + init(SField const& fieldName) const { if (!sField_.get().isUseful()) { @@ -62,6 +66,21 @@ class SOElement } } +public: + SOElement(SField const& fieldName, SOEStyle style) + : sField_(fieldName), style_(style), supportMpt_(soeMPTNone) + { + init(fieldName); + } + SOElement( + TypedField const& fieldName, + SOEStyle style, + SOETxMPTAmount supportMpt = soeMPTNotSupported) + : sField_(fieldName), style_(style), supportMpt_(supportMpt) + { + init(fieldName); + } + SField const& sField() const { @@ -73,6 +92,12 @@ class SOElement { return style_; } + + SOETxMPTAmount + supportMPT() const + { + return supportMpt_; + } }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 3eed0860f54..2f037a1c285 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -23,9 +23,10 @@ #include #include #include +#include #include #include -#include +#include #include #include #include @@ -33,6 +34,10 @@ namespace ripple { +template +concept AssetType = std::is_same_v || + std::is_convertible_v || std::is_convertible_v; + // Internal form: // 1: If amount is zero, then value is zero and offset is -100 // 2: Otherwise: @@ -51,10 +56,9 @@ class STAmount final : public STBase, public CountedObject using rep = std::pair; private: - Issue mIssue; + Asset mAsset; mantissa_type mValue; exponent_type mOffset; - bool mIsNative; // A shorthand for isXRP(mIssue). bool mIsNegative; public: @@ -70,8 +74,10 @@ class STAmount final : public STBase, public CountedObject // Max native value on network. static const std::uint64_t cMaxNativeN = 100000000000000000ull; - static const std::uint64_t cNotNative = 0x8000000000000000ull; - static const std::uint64_t cPosNative = 0x4000000000000000ull; + static const std::uint64_t cIssuedCurrency = 0x8000000000000000ull; + static const std::uint64_t cPositive = 0x4000000000000000ull; + static const std::uint64_t cMPToken = 0x2000000000000000ull; + static const std::uint64_t cValueMask = ~(cPositive | cMPToken); static std::uint64_t const uRateOne; @@ -84,31 +90,31 @@ class STAmount final : public STBase, public CountedObject }; // Do not call canonicalize + template STAmount( SField const& name, - Issue const& issue, + A const& asset, mantissa_type mantissa, exponent_type exponent, - bool native, bool negative, unchecked); + template STAmount( - Issue const& issue, + A const& asset, mantissa_type mantissa, exponent_type exponent, - bool native, bool negative, unchecked); // Call canonicalize + template STAmount( SField const& name, - Issue const& issue, - mantissa_type mantissa, - exponent_type exponent, - bool native, - bool negative); + A const& asset, + mantissa_type mantissa = 0, + exponent_type exponent = 0, + bool negative = false); STAmount(SField const& name, std::int64_t mantissa); @@ -117,37 +123,42 @@ class STAmount final : public STBase, public CountedObject std::uint64_t mantissa = 0, bool negative = false); - STAmount( - SField const& name, - Issue const& issue, - std::uint64_t mantissa = 0, - int exponent = 0, - bool negative = false); - explicit STAmount(std::uint64_t mantissa = 0, bool negative = false); explicit STAmount(SField const& name, STAmount const& amt); + template STAmount( - Issue const& issue, + A const& asset, std::uint64_t mantissa = 0, int exponent = 0, - bool negative = false); + bool negative = false) + : mAsset(asset) + , mValue(mantissa) + , mOffset(exponent) + , mIsNegative(negative) + { + canonicalize(); + } // VFALCO Is this needed when we have the previous signature? + template STAmount( - Issue const& issue, + A const& asset, std::uint32_t mantissa, int exponent = 0, bool negative = false); - STAmount(Issue const& issue, std::int64_t mantissa, int exponent = 0); + template + STAmount(A const& asset, std::int64_t mantissa, int exponent = 0); - STAmount(Issue const& issue, int mantissa, int exponent = 0); + template + STAmount(A const& asset, int mantissa, int exponent = 0); // Legacy support for new-style amounts STAmount(IOUAmount const& amount, Issue const& issue); STAmount(XRPAmount const& amount); + STAmount(MPTAmount const& amount, MPTIssue const& issue); operator Number() const; //-------------------------------------------------------------------------- @@ -162,12 +173,23 @@ class STAmount final : public STBase, public CountedObject bool native() const noexcept; + template + constexpr bool + holds() const noexcept; + bool negative() const noexcept; std::uint64_t mantissa() const noexcept; + Asset const& + asset() const; + + template + constexpr TIss const& + get() const; + Issue const& issue() const; @@ -223,17 +245,14 @@ class STAmount final : public STBase, public CountedObject // Zero while copying currency and issuer. void - clear(STAmount const& saTmpl); - - void - clear(Issue const& issue); + clear(Asset const& asset); void setIssuer(AccountID const& uIssuer); - /** Set the Issue for this amount and update mIsNative. */ + /** Set the Issue for this amount. */ void - setIssue(Issue const& issue); + setIssue(Asset const& asset); //-------------------------------------------------------------------------- // @@ -265,6 +284,8 @@ class STAmount final : public STBase, public CountedObject xrp() const; IOUAmount iou() const; + MPTAmount + mpt() const; private: static std::unique_ptr @@ -289,6 +310,99 @@ class STAmount final : public STBase, public CountedObject operator+(STAmount const& v1, STAmount const& v2); }; +template +STAmount::STAmount( + SField const& name, + A const& asset, + mantissa_type mantissa, + exponent_type exponent, + bool negative, + unchecked) + : STBase(name) + , mAsset(asset) + , mValue(mantissa) + , mOffset(exponent) + , mIsNegative(negative) +{ +} + +template +STAmount::STAmount( + A const& asset, + mantissa_type mantissa, + exponent_type exponent, + bool negative, + unchecked) + : mAsset(asset), mValue(mantissa), mOffset(exponent), mIsNegative(negative) +{ +} + +template +STAmount::STAmount( + SField const& name, + A const& asset, + std::uint64_t mantissa, + int exponent, + bool negative) + : STBase(name) + , mAsset(asset) + , mValue(mantissa) + , mOffset(exponent) + , mIsNegative(negative) +{ + assert(mValue <= std::numeric_limits::max()); + canonicalize(); +} + +template +STAmount::STAmount(A const& asset, std::int64_t mantissa, int exponent) + : mAsset(asset), mOffset(exponent) +{ + set(mantissa); + canonicalize(); +} + +template +STAmount::STAmount( + A const& asset, + std::uint32_t mantissa, + int exponent, + bool negative) + : STAmount(asset, safe_cast(mantissa), exponent, negative) +{ +} + +template +STAmount::STAmount(A const& asset, int mantissa, int exponent) + : STAmount(asset, safe_cast(mantissa), exponent) +{ +} + +// Legacy support for new-style amounts +inline STAmount::STAmount(IOUAmount const& amount, Issue const& issue) + : mAsset(issue) + , mOffset(amount.exponent()) + , mIsNegative(amount < beast::zero) +{ + if (mIsNegative) + mValue = static_cast(-amount.mantissa()); + else + mValue = static_cast(amount.mantissa()); + + canonicalize(); +} + +inline STAmount::STAmount(MPTAmount const& amount, MPTIssue const& issue) + : mAsset(issue), mOffset(0), mIsNegative(amount < beast::zero) +{ + if (mIsNegative) + mValue = unsafe_cast(-amount.value()); + else + mValue = unsafe_cast(amount.value()); + + canonicalize(); +} + //------------------------------------------------------------------------------ // // Creation @@ -300,7 +414,7 @@ STAmount amountFromQuality(std::uint64_t rate); STAmount -amountFromString(Issue const& issue, std::string const& amount); +amountFromString(Asset const& issue, std::string const& amount); STAmount amountFromJson(SField const& name, Json::Value const& v); @@ -331,7 +445,14 @@ STAmount::exponent() const noexcept inline bool STAmount::native() const noexcept { - return mIsNative; + return isXRP(mAsset); +} + +template +constexpr bool +STAmount::holds() const noexcept +{ + return mAsset.holds(); } inline bool @@ -346,22 +467,35 @@ STAmount::mantissa() const noexcept return mValue; } +inline Asset const& +STAmount::asset() const +{ + return mAsset; +} + +template +constexpr TIss const& +STAmount::get() const +{ + return mAsset.get(); +} + inline Issue const& STAmount::issue() const { - return mIssue; + return get(); } inline Currency const& STAmount::getCurrency() const { - return mIssue.currency; + return mAsset.get().currency; } inline AccountID const& STAmount::getIssuer() const { - return mIssue.account; + return mAsset.getIssuer(); } inline int @@ -373,7 +507,9 @@ STAmount::signum() const noexcept inline STAmount STAmount::zeroed() const { - return STAmount(mIssue); + if (mAsset.holds()) + return STAmount(mAsset.get()); + return STAmount(mAsset.get()); } inline STAmount::operator bool() const noexcept @@ -383,8 +519,10 @@ inline STAmount::operator bool() const noexcept inline STAmount::operator Number() const { - if (mIsNative) + if (native()) return xrp(); + if (mAsset.holds()) + return mpt(); return iou(); } @@ -413,30 +551,22 @@ STAmount::clear() { // The -100 is used to allow 0 to sort less than a small positive values // which have a negative exponent. - mOffset = mIsNative ? 0 : -100; + mOffset = native() ? 0 : -100; mValue = 0; mIsNegative = false; } -// Zero while copying currency and issuer. -inline void -STAmount::clear(STAmount const& saTmpl) -{ - clear(saTmpl.mIssue); -} - inline void -STAmount::clear(Issue const& issue) +STAmount::clear(Asset const& asset) { - setIssue(issue); + setIssue(asset); clear(); } inline void STAmount::setIssuer(AccountID const& uIssuer) { - mIssue.account = uIssuer; - setIssue(mIssue); + mAsset.get().account = uIssuer; } inline STAmount const& @@ -501,17 +631,17 @@ STAmount operator-(STAmount const& v1, STAmount const& v2); STAmount -divide(STAmount const& v1, STAmount const& v2, Issue const& issue); +divide(STAmount const& v1, STAmount const& v2, Asset const& asset); STAmount -multiply(STAmount const& v1, STAmount const& v2, Issue const& issue); +multiply(STAmount const& v1, STAmount const& v2, Asset const& asset); // multiply rounding result in specified direction STAmount mulRound( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp); // multiply following the rounding directions more precisely. @@ -519,7 +649,7 @@ STAmount mulRoundStrict( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp); // divide rounding result in specified direction @@ -527,7 +657,7 @@ STAmount divRound( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp); // divide following the rounding directions more precisely. @@ -535,7 +665,7 @@ STAmount divRoundStrict( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp); // Someone is offering X for Y, what is the rate? @@ -549,7 +679,7 @@ getRate(STAmount const& offerOut, STAmount const& offerIn); inline bool isXRP(STAmount const& amount) { - return isXRP(amount.issue().currency); + return isXRP(amount.asset()); } // Since `canonicalize` does not have access to a ledger, this is needed to put diff --git a/include/xrpl/protocol/STBitString.h b/include/xrpl/protocol/STBitString.h index 7dc92303e72..f3a74f2fc54 100644 --- a/include/xrpl/protocol/STBitString.h +++ b/include/xrpl/protocol/STBitString.h @@ -84,6 +84,7 @@ class STBitString final : public STBase, public CountedObject> using STUInt128 = STBitString<128>; using STUInt160 = STBitString<160>; +using STUInt192 = STBitString<192>; using STUInt256 = STBitString<256>; template @@ -136,6 +137,13 @@ STUInt160::getSType() const return STI_UINT160; } +template <> +inline SerializedTypeID +STUInt192::getSType() const +{ + return STI_UINT192; +} + template <> inline SerializedTypeID STUInt256::getSType() const diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index b3cef83de5f..e55351cbc24 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -226,6 +226,8 @@ class STObject : public STBase, public CountedObject uint160 getFieldH160(SField const& field) const; + uint192 + getFieldH192(SField const& field) const; uint256 getFieldH256(SField const& field) const; AccountID diff --git a/include/xrpl/protocol/Serializer.h b/include/xrpl/protocol/Serializer.h index b85e8eb013d..d8d0b9222e3 100644 --- a/include/xrpl/protocol/Serializer.h +++ b/include/xrpl/protocol/Serializer.h @@ -373,6 +373,12 @@ class SerialIter return getBitString<160>(); } + uint192 + get192() + { + return getBitString<192>(); + } + uint256 get256() { diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index aae3c7107bd..b4dcd6ff875 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -125,6 +125,7 @@ enum TEMcodes : TERUnderlyingType { temSEQ_AND_TICKET, temBAD_NFTOKEN_TRANSFER_FEE, + temBAD_MPTOKEN_TRANSFER_FEE, temBAD_AMM_TOKENS, @@ -138,7 +139,7 @@ enum TEMcodes : TERUnderlyingType { temEMPTY_DID, temARRAY_EMPTY, - temARRAY_TOO_LARGE, + temARRAY_TOO_LARGE }; //------------------------------------------------------------------------------ @@ -339,7 +340,12 @@ enum TECcodes : TERUnderlyingType { tecINVALID_UPDATE_TIME = 188, tecTOKEN_PAIR_NOT_FOUND = 189, tecARRAY_EMPTY = 190, - tecARRAY_TOO_LARGE = 191 + tecARRAY_TOO_LARGE = 191, + tecMPTOKEN_EXISTS = 192, + tecMPT_MAX_AMOUNT_EXCEEDED = 193, + tecMPT_LOCKED = 194, + tecMPT_NOT_SUPPORTED = 195, + tecMPT_ISSUANCE_NOT_FOUND = 196 }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index ba2b97562db..e364b9ba6e2 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -22,6 +22,8 @@ #include +#include + namespace ripple { /** Transaction flags. @@ -130,6 +132,23 @@ constexpr std::uint32_t const tfOnlyXRP = 0x00000002; constexpr std::uint32_t const tfTrustLine = 0x00000004; constexpr std::uint32_t const tfTransferable = 0x00000008; +// MPTokenIssuanceCreate flags: +// NOTE - there is intentionally no flag here for 0x01 because that +// corresponds to lsfMPTLocked, which this transaction cannot mutate. +constexpr std::uint32_t const tfMPTCanLock = lsfMPTCanLock; +constexpr std::uint32_t const tfMPTRequireAuth = lsfMPTRequireAuth; +constexpr std::uint32_t const tfMPTCanEscrow = lsfMPTCanEscrow; +constexpr std::uint32_t const tfMPTCanTrade = lsfMPTCanTrade; +constexpr std::uint32_t const tfMPTCanTransfer = lsfMPTCanTransfer; +constexpr std::uint32_t const tfMPTCanClawback = lsfMPTCanClawback; + +// MPTokenAuthorize flags: +constexpr std::uint32_t const tfMPTUnauthorize = 0x00000001; + +// MPTokenIssuanceSet flags: +constexpr std::uint32_t const tfMPTLock = 0x00000001; +constexpr std::uint32_t const tfMPTUnlock = 0x00000002; + // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between // accounts allowed a TrustLine to be added to the issuer of that token // without explicit permission from that issuer. This was enabled by @@ -185,6 +204,18 @@ constexpr std::uint32_t tfDepositMask = ~(tfUniversal | tfDepositSubTx); constexpr std::uint32_t tfClearAccountCreateAmount = 0x00010000; constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreateAmount); +// MPTokenIssuanceCreate flags: +constexpr std::uint32_t const tfMPTokenIssuanceCreateMask = + ~(tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback | tfUniversal); + +// MPTokenIssuanceDestroy flags: +constexpr std::uint32_t const tfMPTokenIssuanceDestroyMask = ~tfUniversal; + +// MPTokenAuthorize flags: +constexpr std::uint32_t const tfMPTokenAuthorizeMask = ~(tfMPTUnauthorize | tfUniversal); + +// MPTokenIssuanceSet flags: +constexpr std::uint32_t const tfMPTokenIssuanceSetMask = ~(tfMPTLock | tfMPTUnlock | tfUniversal); // clang-format on } // namespace ripple diff --git a/include/xrpl/protocol/TxFormats.h b/include/xrpl/protocol/TxFormats.h index a3f5cca108c..aa26a4641d0 100644 --- a/include/xrpl/protocol/TxFormats.h +++ b/include/xrpl/protocol/TxFormats.h @@ -199,6 +199,17 @@ enum TxType : std::uint16_t /** This transaction type fixes a problem in the ledger state */ ttLEDGER_STATE_FIX = 53, + /** This transaction creates a new MPTokenIssuance object. */ + ttMPTOKEN_ISSUANCE_CREATE = 54, + + /** This transaction destroys an existing MPTokenIssuance object. */ + ttMPTOKEN_ISSUANCE_DESTROY = 55, + + /** This transaction destroys an existing MPTokenIssuance object. */ + ttMPTOKEN_AUTHORIZE = 56, + + /** This transaction sets an existing MPTokenIssuance or MPToken object. */ + ttMPTOKEN_ISSUANCE_SET = 57, /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/include/xrpl/protocol/UintTypes.h b/include/xrpl/protocol/UintTypes.h index a0a8069f669..74f3917faaf 100644 --- a/include/xrpl/protocol/UintTypes.h +++ b/include/xrpl/protocol/UintTypes.h @@ -58,6 +58,9 @@ using Currency = base_uint<160, detail::CurrencyTag>; /** NodeID is a 160-bit hash representing one node. */ using NodeID = base_uint<160, detail::NodeIDTag>; +/** MPT is a 192-bit hash representing MPTID. */ +using MPTID = base_uint<192>; + /** XRP currency. */ Currency const& xrpCurrency(); diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index e3eda80b44f..a5d3788379f 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -41,116 +41,123 @@ namespace jss { error: Common properties of RPC error responses. */ -JSS(AL_size); // out: GetCounts -JSS(AL_hit_rate); // out: GetCounts -JSS(Account); // in: TransactionSign; field. -JSS(AccountDelete); // transaction type. -JSS(AccountRoot); // ledger type. -JSS(AccountSet); // transaction type. -JSS(AMM); // ledger type -JSS(AMMBid); // transaction type -JSS(AMMID); // field -JSS(AMMCreate); // transaction type -JSS(AMMDeposit); // transaction type -JSS(AMMDelete); // transaction type -JSS(AMMVote); // transaction type -JSS(AMMWithdraw); // transaction type -JSS(Amendments); // ledger type. -JSS(Amount); // in: TransactionSign; field. -JSS(Amount2); // in/out: AMM IOU/XRP pool, deposit, withdraw amount -JSS(Asset); // in: AMM Asset1 -JSS(Asset2); // in: AMM Asset2 -JSS(AssetClass); // in: Oracle -JSS(AssetPrice); // in: Oracle -JSS(AuthAccount); // in: AMM Auction Slot -JSS(AuthAccounts); // in: AMM Auction Slot -JSS(BaseAsset); // in: Oracle -JSS(Bridge); // ledger type. -JSS(Check); // ledger type. -JSS(CheckCancel); // transaction type. -JSS(CheckCash); // transaction type. -JSS(CheckCreate); // transaction type. -JSS(Clawback); // transaction type. -JSS(ClearFlag); // field. -JSS(DID); // ledger type. -JSS(DIDDelete); // transaction type. -JSS(DIDSet); // transaction type. -JSS(DeliverMax); // out: alias to Amount -JSS(DeliverMin); // in: TransactionSign -JSS(DepositPreauth); // transaction and ledger type. -JSS(Destination); // in: TransactionSign; field. -JSS(DirectoryNode); // ledger type. -JSS(EnableAmendment); // transaction type. -JSS(EPrice); // in: AMM Deposit option -JSS(Escrow); // ledger type. -JSS(EscrowCancel); // transaction type. -JSS(EscrowCreate); // transaction type. -JSS(EscrowFinish); // transaction type. -JSS(Fee); // in/out: TransactionSign; field. -JSS(FeeSettings); // ledger type. -JSS(Flags); // in/out: TransactionSign; field. -JSS(Invalid); // -JSS(LastLedgerSequence); // in: TransactionSign; field -JSS(LastUpdateTime); // field. -JSS(LedgerHashes); // ledger type. -JSS(LimitAmount); // field. -JSS(BidMax); // in: AMM Bid -JSS(BidMin); // in: AMM Bid -JSS(NetworkID); // field. -JSS(NFTokenBurn); // transaction type. -JSS(NFTokenMint); // transaction type. -JSS(NFTokenOffer); // ledger type. -JSS(NFTokenAcceptOffer); // transaction type. -JSS(NFTokenCancelOffer); // transaction type. -JSS(NFTokenCreateOffer); // transaction type. -JSS(NFTokenPage); // ledger type. -JSS(LedgerStateFix); // transaction type. -JSS(LPTokenOut); // in: AMM Liquidity Provider deposit tokens -JSS(LPTokenIn); // in: AMM Liquidity Provider withdraw tokens -JSS(LPToken); // out: AMM Liquidity Provider tokens info -JSS(Offer); // ledger type. -JSS(OfferCancel); // transaction type. -JSS(OfferCreate); // transaction type. -JSS(OfferSequence); // field. -JSS(Oracle); // ledger type. -JSS(OracleDelete); // transaction type. -JSS(OracleDocumentID); // field -JSS(OracleSet); // transaction type. -JSS(Owner); // field -JSS(Paths); // in/out: TransactionSign -JSS(PayChannel); // ledger type. -JSS(Payment); // transaction type. -JSS(PaymentChannelClaim); // transaction type. -JSS(PaymentChannelCreate); // transaction type. -JSS(PaymentChannelFund); // transaction type. -JSS(PriceDataSeries); // field. -JSS(PriceData); // field. -JSS(Provider); // field. -JSS(QuoteAsset); // in: Oracle. -JSS(RippleState); // ledger type. -JSS(SLE_hit_rate); // out: GetCounts. -JSS(SetFee); // transaction type. -JSS(UNLModify); // transaction type. -JSS(Scale); // field. -JSS(SettleDelay); // in: TransactionSign -JSS(SendMax); // in: TransactionSign -JSS(Sequence); // in/out: TransactionSign; field. -JSS(SetFlag); // field. -JSS(SetRegularKey); // transaction type. -JSS(SignerList); // ledger type. -JSS(SignerListSet); // transaction type. -JSS(SigningPubKey); // field. -JSS(TakerGets); // field. -JSS(TakerPays); // field. -JSS(Ticket); // ledger type. -JSS(TicketCreate); // transaction type. -JSS(TxnSignature); // field. -JSS(TradingFee); // in/out: AMM trading fee -JSS(TransactionType); // in: TransactionSign. -JSS(TransferRate); // in: TransferRate. -JSS(TrustSet); // transaction type. -JSS(URI); // field. -JSS(VoteSlots); // out: AMM Vote +JSS(AL_size); // out: GetCounts +JSS(AL_hit_rate); // out: GetCounts +JSS(Account); // in: TransactionSign; field. +JSS(AccountDelete); // transaction type. +JSS(AccountRoot); // ledger type. +JSS(AccountSet); // transaction type. +JSS(AMM); // ledger type +JSS(AMMBid); // transaction type +JSS(AMMID); // field +JSS(AMMCreate); // transaction type +JSS(AMMDeposit); // transaction type +JSS(AMMDelete); // transaction type +JSS(AMMVote); // transaction type +JSS(AMMWithdraw); // transaction type +JSS(Amendments); // ledger type. +JSS(Amount); // in: TransactionSign; field. +JSS(Amount2); // in/out: AMM IOU/XRP pool, deposit, withdraw amount +JSS(Asset); // in: AMM Asset1 +JSS(Asset2); // in: AMM Asset2 +JSS(AssetClass); // in: Oracle +JSS(AssetPrice); // in: Oracle +JSS(AuthAccount); // in: AMM Auction Slot +JSS(AuthAccounts); // in: AMM Auction Slot +JSS(BaseAsset); // in: Oracle +JSS(BidMax); // in: AMM Bid +JSS(BidMin); // in: AMM Bid +JSS(Bridge); // ledger type. +JSS(Check); // ledger type. +JSS(CheckCancel); // transaction type. +JSS(CheckCash); // transaction type. +JSS(CheckCreate); // transaction type. +JSS(Clawback); // transaction type. +JSS(ClearFlag); // field. +JSS(DID); // ledger type. +JSS(DIDDelete); // transaction type. +JSS(DIDSet); // transaction type. +JSS(DeliverMax); // out: alias to Amount +JSS(DeliverMin); // in: TransactionSign +JSS(DepositPreauth); // transaction and ledger type. +JSS(Destination); // in: TransactionSign; field. +JSS(DirectoryNode); // ledger type. +JSS(EnableAmendment); // transaction type. +JSS(EPrice); // in: AMM Deposit option +JSS(Escrow); // ledger type. +JSS(EscrowCancel); // transaction type. +JSS(EscrowCreate); // transaction type. +JSS(EscrowFinish); // transaction type. +JSS(Fee); // in/out: TransactionSign; field. +JSS(FeeSettings); // ledger type. +JSS(Flags); // in/out: TransactionSign; field. +JSS(Invalid); // +JSS(LastLedgerSequence); // in: TransactionSign; field +JSS(LastUpdateTime); // field. +JSS(LedgerHashes); // ledger type. +JSS(LimitAmount); // field. +JSS(MPToken); // ledger type. +JSS(MPTokenIssuance); // ledger type. +JSS(MPTokenIssuanceCreate); // transaction type. +JSS(MPTokenIssuanceDestroy); // transaction type. +JSS(MPTokenAuthorize); // transaction type. +JSS(MPTokenIssuanceSet); // transaction type. +JSS(MPTokenIssuanceID); // in: MPTokenIssuanceDestroy, MPTokenAuthorize +JSS(NetworkID); // field. +JSS(NFTokenBurn); // transaction type. +JSS(NFTokenMint); // transaction type. +JSS(NFTokenOffer); // ledger type. +JSS(NFTokenAcceptOffer); // transaction type. +JSS(NFTokenCancelOffer); // transaction type. +JSS(NFTokenCreateOffer); // transaction type. +JSS(NFTokenPage); // ledger type. +JSS(LedgerStateFix); // transaction type. +JSS(LPTokenOut); // in: AMM Liquidity Provider deposit tokens +JSS(LPTokenIn); // in: AMM Liquidity Provider withdraw tokens +JSS(LPToken); // out: AMM Liquidity Provider tokens info +JSS(Offer); // ledger type. +JSS(OfferCancel); // transaction type. +JSS(OfferCreate); // transaction type. +JSS(OfferSequence); // field. +JSS(Oracle); // ledger type. +JSS(OracleDelete); // transaction type. +JSS(OracleDocumentID); // field +JSS(OracleSet); // transaction type. +JSS(Owner); // field +JSS(Paths); // in/out: TransactionSign +JSS(PayChannel); // ledger type. +JSS(Payment); // transaction type. +JSS(PaymentChannelClaim); // transaction type. +JSS(PaymentChannelCreate); // transaction type. +JSS(PaymentChannelFund); // transaction type. +JSS(PriceDataSeries); // field. +JSS(PriceData); // field. +JSS(Provider); // field. +JSS(QuoteAsset); // in: Oracle. +JSS(RippleState); // ledger type. +JSS(SLE_hit_rate); // out: GetCounts. +JSS(SetFee); // transaction type. +JSS(UNLModify); // transaction type. +JSS(Scale); // field. +JSS(SettleDelay); // in: TransactionSign +JSS(SendMax); // in: TransactionSign +JSS(Sequence); // in/out: TransactionSign; field. +JSS(SetFlag); // field. +JSS(SetRegularKey); // transaction type. +JSS(SignerList); // ledger type. +JSS(SignerListSet); // transaction type. +JSS(SigningPubKey); // field. +JSS(TakerGets); // field. +JSS(TakerPays); // field. +JSS(Ticket); // ledger type. +JSS(TicketCreate); // transaction type. +JSS(TxnSignature); // field. +JSS(TradingFee); // in/out: AMM trading fee +JSS(TransactionType); // in: TransactionSign. +JSS(TransferRate); // in: TransferRate. +JSS(TrustSet); // transaction type. +JSS(URI); // field. +JSS(VoteSlots); // out: AMM Vote JSS(XChainAddAccountCreateAttestation); // transaction type. JSS(XChainAddClaimAttestation); // transaction type. JSS(XChainAccountCreateCommit); // transaction type. @@ -236,6 +243,11 @@ JSS(build_path); // in: TransactionSign JSS(build_version); // out: NetworkOPs JSS(cancel_after); // out: AccountChannels JSS(can_delete); // out: CanDelete +JSS(mpt_amount); // out: mpt_holders +JSS(mpt_issuance); // in: LedgerEntry, AccountObjects +JSS(mpt_issuance_id); // in: Payment, mpt_holders +JSS(mptoken); // in: LedgerEntry, AccountObjects +JSS(mptoken_index); // out: mpt_holders JSS(changes); // out: BookChanges JSS(channel_id); // out: AccountChannels JSS(channels); // out: AccountChannels @@ -363,6 +375,7 @@ JSS(high); // out: BookChanges JSS(highest_sequence); // out: AccountInfo JSS(highest_ticket); // out: AccountInfo JSS(historical_perminute); // historical_perminute. +JSS(holders); // out: MPTHolders JSS(hostid); // out: NetworkOPs JSS(hotwallet); // in: GatewayBalances JSS(id); // websocket. @@ -448,6 +461,7 @@ JSS(load_fee); // out: LoadFeeTrackImp, NetworkOPs JSS(local); // out: resource/Logic.h JSS(local_txs); // out: GetCounts JSS(local_static_keys); // out: ValidatorList +JSS(locked_amount); // out: MPTHolders JSS(low); // out: BookChanges JSS(lowest_sequence); // out: AccountInfo JSS(lowest_ticket); // out: AccountInfo diff --git a/src/libxrpl/basics/MPTAmount.cpp b/src/libxrpl/basics/MPTAmount.cpp new file mode 100644 index 00000000000..6c9a50e4730 --- /dev/null +++ b/src/libxrpl/basics/MPTAmount.cpp @@ -0,0 +1,85 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +namespace ripple { + +MPTAmount& +MPTAmount::operator+=(MPTAmount const& other) +{ + value_ += other.value(); + return *this; +} + +MPTAmount& +MPTAmount::operator-=(MPTAmount const& other) +{ + value_ -= other.value(); + return *this; +} + +MPTAmount +MPTAmount::operator-() const +{ + return MPTAmount{-value_}; +} + +bool +MPTAmount::operator==(MPTAmount const& other) const +{ + return value_ == other.value_; +} + +bool +MPTAmount::operator==(value_type other) const +{ + return value_ == other; +} + +bool +MPTAmount::operator<(MPTAmount const& other) const +{ + return value_ < other.value_; +} + +Json::Value +MPTAmount::jsonClipped() const +{ + static_assert( + std::is_signed_v && std::is_integral_v, + "Expected MPTAmount to be a signed integral type"); + + constexpr auto min = std::numeric_limits::min(); + constexpr auto max = std::numeric_limits::max(); + + if (value_ < min) + return min; + if (value_ > max) + return max; + return static_cast(value_); +} + +MPTAmount +MPTAmount::minPositiveAmount() +{ + return MPTAmount{1}; +} + +} // namespace ripple diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 14260b653a2..ebbfa0023c9 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -504,6 +504,11 @@ Number::operator XRPAmount() const return XRPAmount{static_cast(*this)}; } +Number::operator MPTAmount() const +{ + return MPTAmount{static_cast(*this)}; +} + std::string to_string(Number const& amount) { diff --git a/src/libxrpl/protocol/Asset.cpp b/src/libxrpl/protocol/Asset.cpp new file mode 100644 index 00000000000..d458644d948 --- /dev/null +++ b/src/libxrpl/protocol/Asset.cpp @@ -0,0 +1,93 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace ripple { + +Asset::Asset(Issue const& issue) : issue_(issue) +{ +} + +Asset::Asset(MPTIssue const& mpt) : issue_(mpt) +{ +} + +Asset::Asset(MPTID const& mpt) : issue_(MPTIssue{mpt}) +{ +} + +Asset::operator Issue() const +{ + return get(); +} + +Asset::operator MPTIssue() const +{ + return get(); +} + +AccountID const& +Asset::getIssuer() const +{ + if (holds()) + return get().getIssuer(); + return get().getIssuer(); +} + +std::string +Asset::getText() const +{ + if (holds()) + return get().getText(); + return to_string(get().getMptID()); +} + +void +Asset::setJson(Json::Value& jv) const +{ + if (holds()) + jv[jss::mpt_issuance_id] = to_string(get().getMptID()); + else + { + jv[jss::currency] = to_string(get().currency); + if (!isXRP(get().currency)) + jv[jss::issuer] = toBase58(get().account); + } +} + +std::string +to_string(Asset const& asset) +{ + if (asset.holds()) + return to_string(asset.get()); + return to_string(asset.get().getMptID()); +} + +bool +validJSONAsset(Json::Value const& jv) +{ + if (jv.isMember(jss::mpt_issuance_id)) + return !(jv.isMember(jss::currency) || jv.isMember(jss::issuer)); + return jv.isMember(jss::currency); +} + +} // namespace ripple \ No newline at end of file diff --git a/src/libxrpl/protocol/Feature.cpp b/src/libxrpl/protocol/Feature.cpp index 078369bf20c..4b37633b408 100644 --- a/src/libxrpl/protocol/Feature.cpp +++ b/src/libxrpl/protocol/Feature.cpp @@ -501,6 +501,7 @@ REGISTER_FIX (fixNFTokenPageLinks, Supported::yes, VoteBehavior::De // InvariantsV1_1 will be changes to Supported::yes when all the // invariants expected to be included under it are complete. REGISTER_FEATURE(InvariantsV1_1, Supported::no, VoteBehavior::DefaultNo); +REGISTER_FEATURE(MPTokensV1, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 30d97416cfa..8014d8d4dcd 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -73,6 +73,8 @@ enum class LedgerNameSpace : std::uint16_t { XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K', DID = 'I', ORACLE = 'R', + MPTOKEN_ISSUANCE = '~', + MPTOKEN = 't', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -135,6 +137,16 @@ getTicketIndex(AccountID const& account, SeqProxy ticketSeq) return getTicketIndex(account, ticketSeq.value()); } +MPTID +getMptID(AccountID const& account, std::uint32_t sequence) +{ + MPTID u; + sequence = boost::endian::native_to_big(sequence); + memcpy(u.data(), &sequence, sizeof(sequence)); + memcpy(u.data() + sizeof(sequence), account.data(), sizeof(account)); + return u; +} + //------------------------------------------------------------------------------ namespace keylet { @@ -451,6 +463,31 @@ oracle(AccountID const& account, std::uint32_t const& documentID) noexcept return {ltORACLE, indexHash(LedgerNameSpace::ORACLE, account, documentID)}; } +Keylet +mptIssuance(AccountID const& issuer, std::uint32_t seq) noexcept +{ + return mptIssuance(getMptID(issuer, seq)); +} + +Keylet +mptIssuance(MPTID const& id) noexcept +{ + return { + ltMPTOKEN_ISSUANCE, indexHash(LedgerNameSpace::MPTOKEN_ISSUANCE, id)}; +} + +Keylet +mptoken(MPTID const& issuanceID, AccountID const& holder) noexcept +{ + return mptoken(mptIssuance(issuanceID).key, holder); +} + +Keylet +mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept +{ + return { + ltMPTOKEN, indexHash(LedgerNameSpace::MPTOKEN, issuanceKey, holder)}; +} } // namespace keylet } // namespace ripple diff --git a/src/libxrpl/protocol/Issue.cpp b/src/libxrpl/protocol/Issue.cpp index 70d2c013d7b..8aff535fde7 100644 --- a/src/libxrpl/protocol/Issue.cpp +++ b/src/libxrpl/protocol/Issue.cpp @@ -26,6 +26,16 @@ namespace ripple { +Issue::Issue(Currency const& c, AccountID const& a) : currency(c), account(a) +{ +} + +AccountID const& +Issue::getIssuer() const +{ + return account; +} + std::string Issue::getText() const { diff --git a/src/libxrpl/protocol/LedgerFormats.cpp b/src/libxrpl/protocol/LedgerFormats.cpp index 9401c00278b..71f04cd8d10 100644 --- a/src/libxrpl/protocol/LedgerFormats.cpp +++ b/src/libxrpl/protocol/LedgerFormats.cpp @@ -364,6 +364,36 @@ LedgerFormats::LedgerFormats() {sfPreviousTxnLgrSeq, soeREQUIRED} }, commonFields); + + add(jss::MPTokenIssuance, + ltMPTOKEN_ISSUANCE, + { + {sfIssuer, soeREQUIRED}, + {sfSequence, soeREQUIRED}, + {sfTransferFee, soeDEFAULT}, + {sfOwnerNode, soeREQUIRED}, + {sfAssetScale, soeDEFAULT}, + {sfMaximumAmount, soeOPTIONAL}, + {sfOutstandingAmount, soeREQUIRED}, + {sfLockedAmount, soeDEFAULT}, + {sfMPTokenMetadata, soeOPTIONAL}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED} + }, + commonFields); + + add(jss::MPToken, + ltMPTOKEN, + { + {sfAccount, soeREQUIRED}, + {sfMPTokenIssuanceID, soeREQUIRED}, + {sfMPTAmount, soeDEFAULT}, + {sfLockedAmount, soeDEFAULT}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED} + }, + commonFields); // clang-format on } diff --git a/src/libxrpl/protocol/MPTIssue.cpp b/src/libxrpl/protocol/MPTIssue.cpp new file mode 100644 index 00000000000..f46e6d9b2d8 --- /dev/null +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -0,0 +1,60 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace ripple { + +MPTIssue::MPTIssue(MPTID const& id) : mptID_(id) +{ +} + +AccountID const& +MPTIssue::getIssuer() const +{ + // copy from id skipping the sequence + AccountID const* account = reinterpret_cast( + mptID_.data() + sizeof(std::uint32_t)); + + return *account; +} + +MPTID const& +MPTIssue::getMptID() const +{ + return mptID_; +} + +Json::Value +to_json(MPTIssue const& issue) +{ + Json::Value jv; + jv[jss::mpt_issuance_id] = to_string(issue.getMptID()); + return jv; +} + +std::string +to_string(MPTIssue const& mptIssue) +{ + return to_string(mptIssue.getMptID()); +} + +} // namespace ripple diff --git a/src/libxrpl/protocol/Quality.cpp b/src/libxrpl/protocol/Quality.cpp index 38b641328b0..c6464eba9d2 100644 --- a/src/libxrpl/protocol/Quality.cpp +++ b/src/libxrpl/protocol/Quality.cpp @@ -65,7 +65,7 @@ Quality::operator--(int) } template + *DivRoundFunc)(STAmount const&, STAmount const&, Asset const&, bool)> static Amounts ceil_in_impl( Amounts const& amount, @@ -77,7 +77,7 @@ ceil_in_impl( { Amounts result( limit, - DivRoundFunc(limit, quality.rate(), amount.out.issue(), roundUp)); + DivRoundFunc(limit, quality.rate(), amount.out.asset(), roundUp)); // Clamp out if (result.out > amount.out) result.out = amount.out; @@ -104,7 +104,7 @@ Quality::ceil_in_strict( } template + *MulRoundFunc)(STAmount const&, STAmount const&, Asset const&, bool)> static Amounts ceil_out_impl( Amounts const& amount, @@ -115,7 +115,7 @@ ceil_out_impl( if (amount.out > limit) { Amounts result( - MulRoundFunc(limit, quality.rate(), amount.in.issue(), roundUp), + MulRoundFunc(limit, quality.rate(), amount.in.asset(), roundUp), limit); // Clamp in if (result.in > amount.in) @@ -151,7 +151,7 @@ composed_quality(Quality const& lhs, Quality const& rhs) STAmount const rhs_rate(rhs.rate()); assert(rhs_rate != beast::zero); - STAmount const rate(mulRound(lhs_rate, rhs_rate, lhs_rate.issue(), true)); + STAmount const rate(mulRound(lhs_rate, rhs_rate, lhs_rate.asset(), true)); std::uint64_t const stored_exponent(rate.exponent() + 100); std::uint64_t const stored_mantissa(rate.mantissa()); diff --git a/src/libxrpl/protocol/Rate2.cpp b/src/libxrpl/protocol/Rate2.cpp index d85a49a5958..01a3e7deca5 100644 --- a/src/libxrpl/protocol/Rate2.cpp +++ b/src/libxrpl/protocol/Rate2.cpp @@ -51,7 +51,7 @@ multiply(STAmount const& amount, Rate const& rate) if (rate == parityRate) return amount; - return multiply(amount, detail::as_amount(rate), amount.issue()); + return multiply(amount, detail::as_amount(rate), amount.asset()); } STAmount @@ -62,7 +62,7 @@ multiplyRound(STAmount const& amount, Rate const& rate, bool roundUp) if (rate == parityRate) return amount; - return mulRound(amount, detail::as_amount(rate), amount.issue(), roundUp); + return mulRound(amount, detail::as_amount(rate), amount.asset(), roundUp); } STAmount diff --git a/src/libxrpl/protocol/SField.cpp b/src/libxrpl/protocol/SField.cpp index f8eb2d6f877..dc4bfb69a21 100644 --- a/src/libxrpl/protocol/SField.cpp +++ b/src/libxrpl/protocol/SField.cpp @@ -98,6 +98,7 @@ CONSTRUCT_TYPED_SFIELD(sfTickSize, "TickSize", UINT8, CONSTRUCT_TYPED_SFIELD(sfUNLModifyDisabling, "UNLModifyDisabling", UINT8, 17); CONSTRUCT_TYPED_SFIELD(sfHookResult, "HookResult", UINT8, 18); CONSTRUCT_TYPED_SFIELD(sfWasLockingChainSend, "WasLockingChainSend", UINT8, 19); +CONSTRUCT_TYPED_SFIELD(sfAssetScale, "AssetScale", UINT8, 20); // 16-bit integers CONSTRUCT_TYPED_SFIELD(sfLedgerEntryType, "LedgerEntryType", UINT16, 1, SField::sMD_Never); @@ -193,6 +194,10 @@ CONSTRUCT_TYPED_SFIELD(sfXChainClaimID, "XChainClaimID", U CONSTRUCT_TYPED_SFIELD(sfXChainAccountCreateCount, "XChainAccountCreateCount", UINT64, 21); CONSTRUCT_TYPED_SFIELD(sfXChainAccountClaimCount, "XChainAccountClaimCount", UINT64, 22); CONSTRUCT_TYPED_SFIELD(sfAssetPrice, "AssetPrice", UINT64, 23); +CONSTRUCT_TYPED_SFIELD(sfMaximumAmount, "MaximumAmount", UINT64, 24); +CONSTRUCT_TYPED_SFIELD(sfOutstandingAmount, "OutstandingAmount", UINT64, 25); +CONSTRUCT_TYPED_SFIELD(sfLockedAmount, "LockedAmount", UINT64, 26); +CONSTRUCT_TYPED_SFIELD(sfMPTAmount, "MPTAmount", UINT64, 27); // 128-bit CONSTRUCT_TYPED_SFIELD(sfEmailHash, "EmailHash", UINT128, 1); @@ -203,6 +208,9 @@ CONSTRUCT_TYPED_SFIELD(sfTakerPaysIssuer, "TakerPaysIssuer", UINT160, CONSTRUCT_TYPED_SFIELD(sfTakerGetsCurrency, "TakerGetsCurrency", UINT160, 3); CONSTRUCT_TYPED_SFIELD(sfTakerGetsIssuer, "TakerGetsIssuer", UINT160, 4); +// 192-bit (common) +CONSTRUCT_TYPED_SFIELD(sfMPTokenIssuanceID, "MPTokenIssuanceID", UINT192, 1); + // 256-bit (common) CONSTRUCT_TYPED_SFIELD(sfLedgerHash, "LedgerHash", UINT256, 1); CONSTRUCT_TYPED_SFIELD(sfParentHash, "ParentHash", UINT256, 2); @@ -307,6 +315,7 @@ CONSTRUCT_TYPED_SFIELD(sfDIDDocument, "DIDDocument", VL, CONSTRUCT_TYPED_SFIELD(sfData, "Data", VL, 27); CONSTRUCT_TYPED_SFIELD(sfAssetClass, "AssetClass", VL, 28); CONSTRUCT_TYPED_SFIELD(sfProvider, "Provider", VL, 29); +CONSTRUCT_TYPED_SFIELD(sfMPTokenMetadata, "MPTokenMetadata", VL, 30); // account CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1); @@ -319,6 +328,7 @@ CONSTRUCT_TYPED_SFIELD(sfUnauthorize, "Unauthorize", ACCOUNT, CONSTRUCT_TYPED_SFIELD(sfRegularKey, "RegularKey", ACCOUNT, 8); CONSTRUCT_TYPED_SFIELD(sfNFTokenMinter, "NFTokenMinter", ACCOUNT, 9); CONSTRUCT_TYPED_SFIELD(sfEmitCallback, "EmitCallback", ACCOUNT, 10); +CONSTRUCT_TYPED_SFIELD(sfMPTokenHolder, "MPTokenHolder", ACCOUNT, 11); // account (uncommon) CONSTRUCT_TYPED_SFIELD(sfHookAccount, "HookAccount", ACCOUNT, 16); diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 236603d6cb8..735e3853c9d 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -78,26 +79,56 @@ getSNValue(STAmount const& amount) return ret; } +static std::int64_t +getMPTValue(STAmount const& amount) +{ + if (!amount.holds()) + Throw("amount is not MPT!"); + + auto ret = static_cast(amount.mantissa()); + + assert(static_cast(ret) == amount.mantissa()); + + if (amount.negative()) + ret = -ret; + + return ret; +} + static bool areComparable(STAmount const& v1, STAmount const& v2) { - return v1.native() == v2.native() && - v1.issue().currency == v2.issue().currency; + if (v1.holds() && v2.holds()) + return v1.native() == v2.native() && + v1.get().currency == v2.get().currency; + if (v1.holds() && v2.holds()) + return v1.get() == v2.get(); + return false; } STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name) { std::uint64_t value = sit.get64(); - // native - if ((value & cNotNative) == 0) + // native or MPT + if ((value & cIssuedCurrency) == 0) { + if ((value & cMPToken) != 0) + { + // is MPT + mOffset = 0; + mIsNegative = (value & cPositive) == 0; + mValue = (value << 8) | sit.get8(); + mAsset = sit.get192(); + return; + } + // else is XRP + mAsset = xrpIssue(); // positive - if ((value & cPosNative) != 0) + if ((value & cPositive) != 0) { - mValue = value & ~cPosNative; + mValue = value & cValueMask; mOffset = 0; - mIsNative = true; mIsNegative = false; return; } @@ -106,9 +137,8 @@ STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name) if (value == 0) Throw("negative zero is not canonical"); - mValue = value; + mValue = value & cValueMask; mOffset = 0; - mIsNative = true; mIsNegative = true; return; } @@ -140,7 +170,7 @@ STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name) Throw("invalid currency value"); } - mIssue = issue; + mAsset = issue; mValue = value; mOffset = offset; mIsNegative = isNegative; @@ -151,97 +181,32 @@ STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name) if (offset != 512) Throw("invalid currency value"); - mIssue = issue; + mAsset = issue; mValue = 0; mOffset = 0; mIsNegative = false; canonicalize(); } -STAmount::STAmount( - SField const& name, - Issue const& issue, - mantissa_type mantissa, - exponent_type exponent, - bool native, - bool negative, - unchecked) - : STBase(name) - , mIssue(issue) - , mValue(mantissa) - , mOffset(exponent) - , mIsNative(native) - , mIsNegative(negative) -{ -} - -STAmount::STAmount( - Issue const& issue, - mantissa_type mantissa, - exponent_type exponent, - bool native, - bool negative, - unchecked) - : mIssue(issue) - , mValue(mantissa) - , mOffset(exponent) - , mIsNative(native) - , mIsNegative(negative) -{ -} - -STAmount::STAmount( - SField const& name, - Issue const& issue, - mantissa_type mantissa, - exponent_type exponent, - bool native, - bool negative) - : STBase(name) - , mIssue(issue) - , mValue(mantissa) - , mOffset(exponent) - , mIsNative(native) - , mIsNegative(negative) -{ - canonicalize(); -} - STAmount::STAmount(SField const& name, std::int64_t mantissa) - : STBase(name), mOffset(0), mIsNative(true) + : STBase(name), mAsset(xrpIssue()), mOffset(0) { set(mantissa); } STAmount::STAmount(SField const& name, std::uint64_t mantissa, bool negative) : STBase(name) + , mAsset(xrpIssue()) , mValue(mantissa) , mOffset(0) - , mIsNative(true) , mIsNegative(negative) { assert(mValue <= std::numeric_limits::max()); } -STAmount::STAmount( - SField const& name, - Issue const& issue, - std::uint64_t mantissa, - int exponent, - bool negative) - : STBase(name) - , mIssue(issue) - , mValue(mantissa) - , mOffset(exponent) - , mIsNegative(negative) -{ - assert(mValue <= std::numeric_limits::max()); - canonicalize(); -} - STAmount::STAmount(SField const& name, STAmount const& from) : STBase(name) - , mIssue(from.mIssue) + , mAsset(from.mAsset) , mValue(from.mValue) , mOffset(from.mOffset) , mIsNegative(from.mIsNegative) @@ -253,62 +218,16 @@ STAmount::STAmount(SField const& name, STAmount const& from) //------------------------------------------------------------------------------ STAmount::STAmount(std::uint64_t mantissa, bool negative) - : mValue(mantissa) + : mAsset(xrpIssue()) + , mValue(mantissa) , mOffset(0) - , mIsNative(true) , mIsNegative(mantissa != 0 && negative) { assert(mValue <= std::numeric_limits::max()); } -STAmount::STAmount( - Issue const& issue, - std::uint64_t mantissa, - int exponent, - bool negative) - : mIssue(issue), mValue(mantissa), mOffset(exponent), mIsNegative(negative) -{ - canonicalize(); -} - -STAmount::STAmount(Issue const& issue, std::int64_t mantissa, int exponent) - : mIssue(issue), mOffset(exponent) -{ - set(mantissa); - canonicalize(); -} - -STAmount::STAmount( - Issue const& issue, - std::uint32_t mantissa, - int exponent, - bool negative) - : STAmount(issue, safe_cast(mantissa), exponent, negative) -{ -} - -STAmount::STAmount(Issue const& issue, int mantissa, int exponent) - : STAmount(issue, safe_cast(mantissa), exponent) -{ -} - -// Legacy support for new-style amounts -STAmount::STAmount(IOUAmount const& amount, Issue const& issue) - : mIssue(issue) - , mOffset(amount.exponent()) - , mIsNative(false) - , mIsNegative(amount < beast::zero) -{ - if (mIsNegative) - mValue = static_cast(-amount.mantissa()); - else - mValue = static_cast(amount.mantissa()); - - canonicalize(); -} - STAmount::STAmount(XRPAmount const& amount) - : mOffset(0), mIsNative(true), mIsNegative(amount < beast::zero) + : mAsset(xrpIssue()), mOffset(0), mIsNegative(amount < beast::zero) { if (mIsNegative) mValue = unsafe_cast(-amount.drops()); @@ -344,7 +263,7 @@ STAmount::move(std::size_t n, void* buf) XRPAmount STAmount::xrp() const { - if (!mIsNative) + if (!native()) Throw( "Cannot return non-native STAmount as XRPAmount"); @@ -359,7 +278,7 @@ STAmount::xrp() const IOUAmount STAmount::iou() const { - if (mIsNative) + if (native() || holds()) Throw("Cannot return native STAmount as IOUAmount"); auto mantissa = static_cast(mValue); @@ -371,10 +290,24 @@ STAmount::iou() const return {mantissa, exponent}; } +MPTAmount +STAmount::mpt() const +{ + if (!holds()) + Throw("Cannot return STAmount as MPTAmount"); + + auto value = static_cast(mValue); + + if (mIsNegative) + value = -value; + + return MPTAmount{value}; +} + STAmount& STAmount::operator=(IOUAmount const& iou) { - assert(mIsNative == false); + assert(native() == false); mOffset = iou.exponent(); mIsNegative = iou < beast::zero; if (mIsNegative) @@ -418,7 +351,7 @@ operator+(STAmount const& v1, STAmount const& v2) // Result must be in terms of v1 currency and issuer. return { v1.getFName(), - v1.issue(), + v1.asset(), v2.mantissa(), v2.exponent(), v2.negative()}; @@ -426,6 +359,8 @@ operator+(STAmount const& v1, STAmount const& v2) if (v1.native()) return {v1.getFName(), getSNValue(v1) + getSNValue(v2)}; + if (v1.holds()) + return {v1.mAsset, v1.mpt().value() + v2.mpt().value()}; if (getSTNumberSwitchover()) { @@ -462,18 +397,18 @@ operator+(STAmount const& v1, STAmount const& v2) std::int64_t fv = vv1 + vv2; if ((fv >= -10) && (fv <= 10)) - return {v1.getFName(), v1.issue()}; + return {v1.getFName(), v1.asset()}; if (fv >= 0) return STAmount{ v1.getFName(), - v1.issue(), + v1.asset(), static_cast(fv), ov1, false}; return STAmount{ - v1.getFName(), v1.issue(), static_cast(-fv), ov1, true}; + v1.getFName(), v1.asset(), static_cast(-fv), ov1, true}; } STAmount @@ -487,10 +422,9 @@ operator-(STAmount const& v1, STAmount const& v2) std::uint64_t const STAmount::uRateOne = getRate(STAmount(1), STAmount(1)); void -STAmount::setIssue(Issue const& issue) +STAmount::setIssue(Asset const& asset) { - mIssue = issue; - mIsNative = isXRP(*this); + mAsset = asset; } // Convert an offer into an index amount so they sort by rate. @@ -529,13 +463,12 @@ STAmount::setJson(Json::Value& elem) const { elem = Json::objectValue; - if (!mIsNative) + if (!native()) { // It is an error for currency or issuer not to be specified for valid // json. elem[jss::value] = getText(); - elem[jss::currency] = to_string(mIssue.currency); - elem[jss::issuer] = to_string(mIssue.account); + mAsset.setJson(elem); } else { @@ -561,7 +494,7 @@ STAmount::getFullText() const std::string ret; ret.reserve(64); - ret = getText() + "/" + mIssue.getText(); + ret = getText() + "/" + mAsset.getText(); return ret; } @@ -581,7 +514,7 @@ STAmount::getText() const bool const scientific( (mOffset != 0) && ((mOffset < -25) || (mOffset > -5))); - if (mIsNative || scientific) + if (native() || mAsset.holds() || scientific) { ret.append(raw_value); @@ -660,19 +593,28 @@ Json::Value STAmount::getJson(JsonOptions) const void STAmount::add(Serializer& s) const { - if (mIsNative) + if (native()) { assert(mOffset == 0); if (!mIsNegative) - s.add64(mValue | cPosNative); + s.add64(mValue | cPositive); else s.add64(mValue); } + else if (mAsset.holds()) + { + auto u8 = static_cast(cMPToken >> 56); + if (!mIsNegative) + u8 |= static_cast(cPositive >> 56); + s.add8(u8); + s.add64(mValue); + s.addBitString(mAsset.get().getMptID()); + } else { if (*this == beast::zero) - s.add64(cNotNative); + s.add64(cIssuedCurrency); else if (mIsNegative) // 512 = not native s.add64( mValue | @@ -682,9 +624,8 @@ STAmount::add(Serializer& s) const mValue | (static_cast(mOffset + 512 + 256 + 97) << (64 - 10))); - - s.addBitString(mIssue.currency); - s.addBitString(mIssue.account); + s.addBitString(mAsset.get().currency); + s.addBitString(mAsset.get().account); } } @@ -698,7 +639,7 @@ STAmount::isEquivalent(const STBase& t) const bool STAmount::isDefault() const { - return (mValue == 0) && mIsNative; + return (mValue == 0) && native(); } //------------------------------------------------------------------------------ @@ -722,11 +663,8 @@ STAmount::isDefault() const void STAmount::canonicalize() { - if (isXRP(*this)) + if (isXRP(*this) || mAsset.holds()) { - // native currency amounts should always have an offset of zero - mIsNative = true; - // log(2^64,10) ~ 19.2 if (mValue == 0 || mOffset <= -20) { @@ -748,9 +686,18 @@ STAmount::canonicalize() { Number num( mIsNegative ? -mValue : mValue, mOffset, Number::unchecked{}); - XRPAmount xrp{num}; - mIsNegative = xrp.drops() < 0; - mValue = mIsNegative ? -xrp.drops() : xrp.drops(); + if (native()) + { + XRPAmount xrp{num}; + mIsNegative = xrp.drops() < 0; + mValue = mIsNegative ? -xrp.drops() : xrp.drops(); + } + else + { + MPTAmount c{num}; + mIsNegative = c.value() < 0; + mValue = mIsNegative ? -c.value() : c.value(); + } mOffset = 0; } else @@ -767,23 +714,32 @@ STAmount::canonicalize() { // N.B. do not move the overflow check to after the // multiplication - if (mValue > cMaxNativeN) - Throw( - "Native currency amount out of range"); + if (isXRP(*this)) + { + if (mValue > cMaxNativeN) + Throw( + "Native currency amount out of range"); + } + else if (mValue > maxMPTokenAmount) + Throw("MPT amount out of range"); } mValue *= 10; --mOffset; } } - if (mValue > cMaxNativeN) - Throw("Native currency amount out of range"); + if (isXRP(*this)) + { + if (mValue > cMaxNativeN) + Throw( + "Native currency amount out of range"); + } + else if (mValue > maxMPTokenAmount) + Throw("MPT amount out of range"); return; } - mIsNative = false; - if (getSTNumberSwitchover()) { *this = iou(); @@ -859,7 +815,7 @@ amountFromQuality(std::uint64_t rate) } STAmount -amountFromString(Issue const& issue, std::string const& amount) +amountFromString(Asset const& asset, std::string const& amount) { static boost::regex const reNumber( "^" // the beginning of the string @@ -891,9 +847,10 @@ amountFromString(Issue const& issue, std::string const& amount) bool negative = (match[1].matched && (match[1] == "-")); - // Can't specify XRP using fractional representation - if (isXRP(issue) && match[3].matched) - Throw("XRP must be specified in integral drops."); + // Can't specify XRP or MPT using fractional representation + if ((isXRP(asset) || asset.holds()) && match[3].matched) + Throw( + "XRP and MPT must be specified as integral amount."); std::uint64_t mantissa; int exponent; @@ -920,7 +877,7 @@ amountFromString(Issue const& issue, std::string const& amount) exponent += beast::lexicalCastThrow(std::string(match[7])); } - return {issue, mantissa, exponent, negative}; + return {asset, mantissa, exponent, negative}; } STAmount @@ -929,11 +886,12 @@ amountFromJson(SField const& name, Json::Value const& v) STAmount::mantissa_type mantissa = 0; STAmount::exponent_type exponent = 0; bool negative = false; - Issue issue; + Asset asset; Json::Value value; - Json::Value currency; + Json::Value currencyOrMPTID; Json::Value issuer; + bool isMPT = false; if (v.isNull()) { @@ -942,14 +900,25 @@ amountFromJson(SField const& name, Json::Value const& v) } else if (v.isObject()) { + if (!validJSONAsset(v)) + Throw("Invalid Asset's Json specification"); + value = v[jss::value]; - currency = v[jss::currency]; - issuer = v[jss::issuer]; + if (v.isMember(jss::mpt_issuance_id)) + { + isMPT = true; + currencyOrMPTID = v[jss::mpt_issuance_id]; + } + else + { + currencyOrMPTID = v[jss::currency]; + issuer = v[jss::issuer]; + } } else if (v.isArray()) { value = v.get(Json::UInt(0), 0); - currency = v.get(Json::UInt(1), Json::nullValue); + currencyOrMPTID = v.get(Json::UInt(1), Json::nullValue); issuer = v.get(Json::UInt(2), Json::nullValue); } else if (v.isString()) @@ -964,7 +933,7 @@ amountFromJson(SField const& name, Json::Value const& v) value = elements[0]; if (elements.size() > 1) - currency = elements[1]; + currencyOrMPTID = elements[1]; if (elements.size() > 2) issuer = elements[2]; @@ -974,26 +943,38 @@ amountFromJson(SField const& name, Json::Value const& v) value = v; } - bool const native = !currency.isString() || currency.asString().empty() || - (currency.asString() == systemCurrencyCode()); + bool const native = !currencyOrMPTID.isString() || + currencyOrMPTID.asString().empty() || + (currencyOrMPTID.asString() == systemCurrencyCode()); if (native) { if (v.isObjectOrNull()) Throw("XRP may not be specified as an object"); - issue = xrpIssue(); + asset = xrpIssue(); } else { - // non-XRP - if (!to_currency(issue.currency, currency.asString())) - Throw("invalid currency"); - - if (!issuer.isString() || !to_issuer(issue.account, issuer.asString())) - Throw("invalid issuer"); - - if (isXRP(issue.currency)) - Throw("invalid issuer"); + if (isMPT) + { + // sequence (32 bits) + account (160 bits) + uint192 u; + if (!u.parseHex(currencyOrMPTID.asString())) + Throw("invalid MPTokenIssuanceID"); + asset = u; + } + else + { + Issue issue; + if (!to_currency(issue.currency, currencyOrMPTID.asString())) + Throw("invalid currency"); + if (!issuer.isString() || + !to_issuer(issue.account, issuer.asString())) + Throw("invalid issuer"); + if (isXRP(issue)) + Throw("invalid issuer"); + asset = issue; + } } if (value.isInt()) @@ -1014,7 +995,7 @@ amountFromJson(SField const& name, Json::Value const& v) } else if (value.isString()) { - auto const ret = amountFromString(issue, value.asString()); + auto const ret = amountFromString(asset, value.asString()); mantissa = ret.mantissa(); exponent = ret.exponent(); @@ -1025,7 +1006,7 @@ amountFromJson(SField const& name, Json::Value const& v) Throw("invalid amount type"); } - return {name, issue, mantissa, exponent, native, negative}; + return {name, asset, mantissa, exponent, negative}; } bool @@ -1099,10 +1080,9 @@ operator-(STAmount const& value) return value; return STAmount( value.getFName(), - value.issue(), + value.asset(), value.mantissa(), value.exponent(), - value.native(), !value.negative(), STAmount::unchecked{}); } @@ -1161,20 +1141,20 @@ muldiv_round( } STAmount -divide(STAmount const& num, STAmount const& den, Issue const& issue) +divide(STAmount const& num, STAmount const& den, Asset const& asset) { if (den == beast::zero) Throw("division by zero"); if (num == beast::zero) - return {issue}; + return {asset}; std::uint64_t numVal = num.mantissa(); std::uint64_t denVal = den.mantissa(); int numOffset = num.exponent(); int denOffset = den.exponent(); - if (num.native()) + if (num.native() || num.holds()) { while (numVal < STAmount::cMinValue) { @@ -1184,7 +1164,7 @@ divide(STAmount const& num, STAmount const& den, Issue const& issue) } } - if (den.native()) + if (den.native() || den.holds()) { while (denVal < STAmount::cMinValue) { @@ -1199,19 +1179,19 @@ divide(STAmount const& num, STAmount const& den, Issue const& issue) // 10^32 to 10^33) followed by a division, so the result // is in the range of 10^16 to 10^15. return STAmount( - issue, + asset, muldiv(numVal, tenTo17, denVal) + 5, numOffset - denOffset - 17, num.negative() != den.negative()); } STAmount -multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) +multiply(STAmount const& v1, STAmount const& v2, Asset const& asset) { if (v1 == beast::zero || v2 == beast::zero) - return STAmount(issue); + return STAmount(asset); - if (v1.native() && v2.native() && isXRP(issue)) + if (v1.native() && v2.native() && isXRP(asset)) { std::uint64_t const minV = getSNValue(v1) < getSNValue(v2) ? getSNValue(v1) : getSNValue(v2); @@ -1226,16 +1206,36 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) return STAmount(v1.getFName(), minV * maxV); } + if (v1.holds() && v2.holds() && asset.holds()) + { + std::uint64_t const minV = getMPTValue(v1) < getMPTValue(v2) + ? getMPTValue(v1) + : getMPTValue(v2); + std::uint64_t const maxV = getMPTValue(v1) < getMPTValue(v2) + ? getMPTValue(v2) + : getMPTValue(v1); + + if (minV > 3000000000ull) // sqrt(cMaxNative) + Throw("Asset value overflow"); + + if (((maxV >> 32) * minV) > 2095475792ull) // cMaxNative / 2^32 + Throw("Asset value overflow"); + + return STAmount(asset, minV * maxV); + } if (getSTNumberSwitchover()) - return {IOUAmount{Number{v1} * Number{v2}}, issue}; + { + auto const r = Number{v1} * Number{v2}; + return STAmount{asset, r.mantissa(), r.exponent()}; + } std::uint64_t value1 = v1.mantissa(); std::uint64_t value2 = v2.mantissa(); int offset1 = v1.exponent(); int offset2 = v2.exponent(); - if (v1.native()) + if (v1.native() || v1.holds()) { while (value1 < STAmount::cMinValue) { @@ -1244,7 +1244,7 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) } } - if (v2.native()) + if (v2.native() || v2.holds()) { while (value2 < STAmount::cMinValue) { @@ -1258,7 +1258,7 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) // range. Dividing their product by 10^14 maintains the // precision, by scaling the result to 10^16 to 10^18. return STAmount( - issue, + asset, muldiv(value1, value2, tenTo14) + 7, offset1 + offset2 + 14, v1.negative() != v2.negative()); @@ -1395,14 +1395,15 @@ static STAmount mulRoundImpl( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp) { if (v1 == beast::zero || v2 == beast::zero) - return {issue}; + return {asset}; - bool const xrp = isXRP(issue); + bool const xrp = isXRP(asset); + // TODO MPT if (v1.native() && v2.native() && xrp) { std::uint64_t minV = @@ -1419,10 +1420,28 @@ mulRoundImpl( return STAmount(v1.getFName(), minV * maxV); } + if (v1.holds() && v2.holds() && asset.holds()) + { + std::uint64_t minV = (getMPTValue(v1) < getMPTValue(v2)) + ? getMPTValue(v1) + : getMPTValue(v2); + std::uint64_t maxV = (getMPTValue(v1) < getMPTValue(v2)) + ? getMPTValue(v2) + : getMPTValue(v1); + + if (minV > 3000000000ull) // sqrt(cMaxNative) + Throw("Asset value overflow"); + + if (((maxV >> 32) * minV) > 2095475792ull) // cMaxNative / 2^32 + Throw("Asset value overflow"); + + return STAmount(asset, minV * maxV); + } + std::uint64_t value1 = v1.mantissa(), value2 = v2.mantissa(); int offset1 = v1.exponent(), offset2 = v2.exponent(); - if (v1.native()) + if (v1.native() || v1.holds()) { while (value1 < STAmount::cMinValue) { @@ -1431,7 +1450,7 @@ mulRoundImpl( } } - if (v2.native()) + if (v2.native() || v2.holds()) { while (value2 < STAmount::cMinValue) { @@ -1462,7 +1481,7 @@ mulRoundImpl( // If appropriate, tell Number to round down. This gives the desired // result from STAmount::canonicalize. MightSaveRound const savedRound(Number::towards_zero); - return STAmount(issue, amount, offset, resultNegative); + return STAmount(asset, amount, offset, resultNegative); }(); if (roundUp && !resultNegative && !result) @@ -1479,7 +1498,7 @@ mulRoundImpl( amount = STAmount::cMinValue; offset = STAmount::cMinOffset; } - return STAmount(issue, amount, offset, resultNegative); + return STAmount(asset, amount, offset, resultNegative); } return result; } @@ -1488,22 +1507,22 @@ STAmount mulRound( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp) { return mulRoundImpl( - v1, v2, issue, roundUp); + v1, v2, asset, roundUp); } STAmount mulRoundStrict( STAmount const& v1, STAmount const& v2, - Issue const& issue, + Asset const& asset, bool roundUp) { return mulRoundImpl( - v1, v2, issue, roundUp); + v1, v2, asset, roundUp); } // We might need to use NumberRoundModeGuard. Allow the caller @@ -1513,19 +1532,19 @@ static STAmount divRoundImpl( STAmount const& num, STAmount const& den, - Issue const& issue, + Asset const& asset, bool roundUp) { if (den == beast::zero) Throw("division by zero"); if (num == beast::zero) - return {issue}; + return {asset}; std::uint64_t numVal = num.mantissa(), denVal = den.mantissa(); int numOffset = num.exponent(), denOffset = den.exponent(); - if (num.native()) + if (num.native() || num.holds()) { while (numVal < STAmount::cMinValue) { @@ -1534,7 +1553,7 @@ divRoundImpl( } } - if (den.native()) + if (den.native() || den.holds()) { while (denVal < STAmount::cMinValue) { @@ -1559,7 +1578,8 @@ divRoundImpl( int offset = numOffset - denOffset - 17; if (resultNegative != roundUp) - canonicalizeRound(isXRP(issue), amount, offset, roundUp); + canonicalizeRound( + isXRP(asset) || asset.holds(), amount, offset, roundUp); STAmount result = [&]() { // If appropriate, tell Number the rounding mode we are using. @@ -1568,12 +1588,12 @@ divRoundImpl( using enum Number::rounding_mode; MightSaveRound const savedRound( roundUp ^ resultNegative ? upward : downward); - return STAmount(issue, amount, offset, resultNegative); + return STAmount(asset, amount, offset, resultNegative); }(); if (roundUp && !resultNegative && !result) { - if (isXRP(issue)) + if (isXRP(asset) || asset.holds()) { // return the smallest value above zero amount = 1; @@ -1585,7 +1605,7 @@ divRoundImpl( amount = STAmount::cMinValue; offset = STAmount::cMinOffset; } - return STAmount(issue, amount, offset, resultNegative); + return STAmount(asset, amount, offset, resultNegative); } return result; } @@ -1594,20 +1614,20 @@ STAmount divRound( STAmount const& num, STAmount const& den, - Issue const& issue, + Asset const& asset, bool roundUp) { - return divRoundImpl(num, den, issue, roundUp); + return divRoundImpl(num, den, asset, roundUp); } STAmount divRoundStrict( STAmount const& num, STAmount const& den, - Issue const& issue, + Asset const& asset, bool roundUp) { - return divRoundImpl(num, den, issue, roundUp); + return divRoundImpl(num, den, asset, roundUp); } } // namespace ripple diff --git a/src/libxrpl/protocol/STInteger.cpp b/src/libxrpl/protocol/STInteger.cpp index 7b7420006f9..148a05d2ab6 100644 --- a/src/libxrpl/protocol/STInteger.cpp +++ b/src/libxrpl/protocol/STInteger.cpp @@ -194,11 +194,25 @@ STUInt64::getText() const template <> Json::Value STUInt64::getJson(JsonOptions) const { - std::string str(16, 0); - auto ret = std::to_chars(str.data(), str.data() + str.size(), value_, 16); - assert(ret.ec == std::errc()); - str.resize(std::distance(str.data(), ret.ptr)); - return str; + auto convertToString = [](uint64_t const value, int const base) { + assert(base == 10 || base == 16); + std::string str( + base == 10 ? 20 : 16, 0); // Allocate space depending on base + auto ret = + std::to_chars(str.data(), str.data() + str.size(), value, base); + assert(ret.ec == std::errc()); + str.resize(std::distance(str.data(), ret.ptr)); + return str; + }; + + if (auto const& fName = getFName(); fName == sfMaximumAmount || + fName == sfOutstandingAmount || fName == sfLockedAmount || + fName == sfMPTAmount) + { + return convertToString(value_, 10); // Convert to base 10 + } + + return convertToString(value_, 16); // Convert to base 16 } } // namespace ripple diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index bde83ec31a1..7e62fc25bd6 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -604,6 +604,12 @@ STObject::getFieldH160(SField const& field) const return getFieldByValue(field); } +uint192 +STObject::getFieldH192(SField const& field) const +{ + return getFieldByValue(field); +} + uint256 STObject::getFieldH256(SField const& field) const { diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index dec5e87eaee..7e6b8ff5975 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -398,8 +398,16 @@ parseLeaf( std::uint64_t val; + bool const useBase10 = field == sfMaximumAmount || + field == sfOutstandingAmount || + field == sfLockedAmount || field == sfMPTAmount; + + // if the field is amount, serialize as base 10 auto [p, ec] = std::from_chars( - str.data(), str.data() + str.size(), val, 16); + str.data(), + str.data() + str.size(), + val, + useBase10 ? 10 : 16); if (ec != std::errc() || (p != str.data() + str.size())) Throw("invalid data"); @@ -454,6 +462,30 @@ parseLeaf( break; } + case STI_UINT192: { + if (!value.isString()) + { + error = bad_type(json_name, fieldName); + return ret; + } + + uint192 num; + + if (auto const s = value.asString(); !num.parseHex(s)) + { + if (!s.empty()) + { + error = invalid_data(json_name, fieldName); + return ret; + } + + num.zero(); + } + + ret = detail::make_stvar(field, num); + break; + } + case STI_UINT160: { if (!value.isString()) { diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 149186d43ce..cb0d0d1495e 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -143,9 +143,14 @@ STTx::getMentionedAccounts() const } else if (auto samt = dynamic_cast(&it)) { - auto const& issuer = samt->getIssuer(); - if (!isXRP(issuer)) - list.insert(issuer); + if (samt->holds()) + { + auto const& issuer = samt->getIssuer(); + if (!isXRP(issuer)) + list.insert(issuer); + } + else + list.insert(samt->getIssuer()); } } @@ -543,6 +548,32 @@ isAccountFieldOkay(STObject const& st) return true; } +static bool +invalidMPTAmountInTx(STObject const& tx) +{ + auto const txType = tx[~sfTransactionType]; + if (!txType) + return false; + if (auto const* item = + TxFormats::getInstance().findByType(safe_cast(*txType))) + { + for (auto const& e : item->getSOTemplate()) + { + if (tx.isFieldPresent(e.sField()) && e.supportMPT() != soeMPTNone) + { + if (auto const& field = tx.peekAtField(e.sField()); + field.getSType() == STI_AMOUNT && + static_cast(field).holds()) + { + if (e.supportMPT() == soeMPTNotSupported) + return true; + } + } + } + } + return false; +} + bool passesLocalChecks(STObject const& st, std::string& reason) { @@ -560,6 +591,13 @@ passesLocalChecks(STObject const& st, std::string& reason) reason = "Cannot submit pseudo transactions."; return false; } + + if (invalidMPTAmountInTx(st)) + { + reason = "Amount can not be MPT."; + return false; + } + return true; } diff --git a/src/libxrpl/protocol/STVar.cpp b/src/libxrpl/protocol/STVar.cpp index c8466259f32..0cb52b5d24e 100644 --- a/src/libxrpl/protocol/STVar.cpp +++ b/src/libxrpl/protocol/STVar.cpp @@ -141,6 +141,9 @@ STVar::STVar(SerialIter& sit, SField const& name, int depth) case STI_UINT160: construct(sit, name); return; + case STI_UINT192: + construct(sit, name); + return; case STI_UINT256: construct(sit, name); return; @@ -205,6 +208,9 @@ STVar::STVar(SerializedTypeID id, SField const& name) case STI_UINT160: construct(name); return; + case STI_UINT192: + construct(name); + return; case STI_UINT256: construct(name); return; diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 917bbf26a9f..1803d836625 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -85,6 +85,7 @@ transResults() MAKE_ERROR(tecHAS_OBLIGATIONS, "The account cannot be deleted since it has obligations."), MAKE_ERROR(tecTOO_SOON, "It is too early to attempt the requested operation. Please wait."), MAKE_ERROR(tecMAX_SEQUENCE_REACHED, "The maximum sequence number was reached."), + MAKE_ERROR(tecMPT_NOT_SUPPORTED, "MPT is not supported."), MAKE_ERROR(tecNO_SUITABLE_NFTOKEN_PAGE, "A suitable NFToken page could not be located."), MAKE_ERROR(tecNFTOKEN_BUY_SELL_MISMATCH, "The 'Buy' and 'Sell' NFToken offers are mismatched."), MAKE_ERROR(tecNFTOKEN_OFFER_TYPE_MISMATCH, "The type of NFToken offer is incorrect."), @@ -115,6 +116,10 @@ transResults() MAKE_ERROR(tecTOKEN_PAIR_NOT_FOUND, "Token pair is not found in Oracle object."), MAKE_ERROR(tecARRAY_EMPTY, "Array is empty."), MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."), + MAKE_ERROR(tecMPTOKEN_EXISTS, "The account already owns the MPToken object."), + MAKE_ERROR(tecMPT_MAX_AMOUNT_EXCEEDED, "The MPT's maximum amount is exceeded."), + MAKE_ERROR(tecMPT_LOCKED, "MPT is locked by the issuer."), + MAKE_ERROR(tecMPT_ISSUANCE_NOT_FOUND, "The MPTokenIssuance object is not found"), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), @@ -197,6 +202,7 @@ transResults() MAKE_ERROR(temINVALID_COUNT, "Malformed: Count field outside valid range."), MAKE_ERROR(temSEQ_AND_TICKET, "Transaction contains a TicketSequence and a non-zero Sequence."), MAKE_ERROR(temBAD_NFTOKEN_TRANSFER_FEE, "Malformed: The NFToken transfer fee must be between 1 and 5000, inclusive."), + MAKE_ERROR(temBAD_MPTOKEN_TRANSFER_FEE, "Malformed: The MPToken transfer fee must be between 1 and 5000, inclusive."), MAKE_ERROR(temXCHAIN_EQUAL_DOOR_ACCOUNTS, "Malformed: Bridge must have unique door accounts."), MAKE_ERROR(temXCHAIN_BAD_PROOF, "Malformed: Bad cross-chain claim proof."), MAKE_ERROR(temXCHAIN_BRIDGE_BAD_ISSUES, "Malformed: Bad bridge issues."), diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index 8a93232604e..92e8ff3b690 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -162,7 +162,7 @@ TxFormats::TxFormats() ttPAYMENT, { {sfDestination, soeREQUIRED}, - {sfAmount, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, {sfSendMax, soeOPTIONAL}, {sfPaths, soeDEFAULT}, {sfInvoiceID, soeOPTIONAL}, @@ -377,7 +377,8 @@ TxFormats::TxFormats() add(jss::Clawback, ttCLAWBACK, { - {sfAmount, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, + {sfMPTokenHolder, soeOPTIONAL}, }, commonFields); @@ -513,6 +514,39 @@ TxFormats::TxFormats() {sfOwner, soeOPTIONAL}, }, commonFields); + + add(jss::MPTokenIssuanceCreate, + ttMPTOKEN_ISSUANCE_CREATE, + { + {sfAssetScale, soeOPTIONAL}, + {sfTransferFee, soeOPTIONAL}, + {sfMaximumAmount, soeOPTIONAL}, + {sfMPTokenMetadata, soeOPTIONAL}, + }, + commonFields); + + add(jss::MPTokenIssuanceDestroy, + ttMPTOKEN_ISSUANCE_DESTROY, + { + {sfMPTokenIssuanceID, soeREQUIRED}, + }, + commonFields); + + add(jss::MPTokenAuthorize, + ttMPTOKEN_AUTHORIZE, + { + {sfMPTokenIssuanceID, soeREQUIRED}, + {sfMPTokenHolder, soeOPTIONAL}, + }, + commonFields); + + add(jss::MPTokenIssuanceSet, + ttMPTOKEN_ISSUANCE_SET, + { + {sfMPTokenIssuanceID, soeREQUIRED}, + {sfMPTokenHolder, soeOPTIONAL}, + }, + commonFields); } TxFormats const& diff --git a/src/test/app/Clawback_test.cpp b/src/test/app/Clawback_test.cpp index a6909bb2f62..8a42d4c38ef 100644 --- a/src/test/app/Clawback_test.cpp +++ b/src/test/app/Clawback_test.cpp @@ -965,6 +965,7 @@ class Clawback_test : public beast::unit_test::suite using namespace test::jtx; FeatureBitset const all{supported_amendments()}; + testWithFeats(all - featureMPTokensV1); testWithFeats(all); } }; diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index 9d1257d16bf..4d1397eab83 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -1023,14 +1023,12 @@ struct Flow_test : public beast::unit_test::suite 9000000000000000ll, -17, false, - false, STAmount::unchecked{}}; STAmount tinyAmt3{ USD.issue(), 9000000000000003ll, -17, false, - false, STAmount::unchecked{}}; env(offer(gw, drops(9000000000), tinyAmt3)); @@ -1058,14 +1056,12 @@ struct Flow_test : public beast::unit_test::suite 9000000000000000ll, -17, false, - false, STAmount::unchecked{}}; STAmount tinyAmt3{ USD.issue(), 9000000000000003ll, -17, false, - false, STAmount::unchecked{}}; env(pay(gw, alice, tinyAmt1)); diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp new file mode 100644 index 00000000000..6ed9c66fb9b --- /dev/null +++ b/src/test/app/MPToken_test.cpp @@ -0,0 +1,1695 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +namespace ripple { + +class MPToken_test : public beast::unit_test::suite +{ + void + testCreateValidation(FeatureBitset features) + { + testcase("Create Validate"); + using namespace test::jtx; + Account const alice("alice"); + + // test preflight of MPTokenIssuanceCreate + { + // If the MPT amendment is not enabled, you should not be able to + // create MPTokenIssuances + Env env{*this, features - featureMPTokensV1}; + MPTTester mptAlice(env, alice); + + mptAlice.create({.ownerCount = 0, .err = temDISABLED}); + } + + // test preflight of MPTokenIssuanceCreate + { + Env env{*this, features}; + MPTTester mptAlice(env, alice); + + mptAlice.create({.flags = 0x00000001, .err = temINVALID_FLAG}); + + // tries to set a txfee while not enabling in the flag + mptAlice.create( + {.maxAmt = "100", + .assetScale = 0, + .transferFee = 1, + .metadata = "test", + .err = temMALFORMED}); + + // tries to set a txfee greater than max + mptAlice.create( + {.maxAmt = "100", + .assetScale = 0, + .transferFee = maxTransferFee + 1, + .metadata = "test", + .flags = tfMPTCanTransfer, + .err = temBAD_MPTOKEN_TRANSFER_FEE}); + + // tries to set a txfee while not enabling transfer + mptAlice.create( + {.maxAmt = "100", + .assetScale = 0, + .transferFee = maxTransferFee, + .metadata = "test", + .err = temMALFORMED}); + + // empty metadata returns error + mptAlice.create( + {.maxAmt = "100", + .assetScale = 0, + .transferFee = 0, + .metadata = "", + .err = temMALFORMED}); + + // MaximumAmout of 0 returns error + mptAlice.create( + {.maxAmt = "0", + .assetScale = 1, + .transferFee = 1, + .metadata = "test", + .err = temMALFORMED}); + + // MaximumAmount larger than 63 bit returns error + mptAlice.create( + {.maxAmt = "18446744073709551600", // FFFFFFFFFFFFFFF0 + .assetScale = 0, + .transferFee = 0, + .metadata = "test", + .err = temMALFORMED}); + mptAlice.create( + {.maxAmt = "9223372036854775808", // 8000000000000000 + .assetScale = 0, + .transferFee = 0, + .metadata = "test", + .err = temMALFORMED}); + } + } + + void + testCreateEnabled(FeatureBitset features) + { + testcase("Create Enabled"); + + using namespace test::jtx; + Account const alice("alice"); + + { + // If the MPT amendment IS enabled, you should be able to create + // MPTokenIssuances + Env env{*this, features}; + MPTTester mptAlice(env, alice); + mptAlice.create( + {.maxAmt = "9223372036854775807", // 7FFFFFFFFFFFFFFF + .assetScale = 1, + .transferFee = 10, + .metadata = "123", + .ownerCount = 1, + .flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | + tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback}); + + // Get the hash for the most recent transaction. + std::string const txHash{ + env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; + + Json::Value const result = env.rpc("tx", txHash)[jss::result]; + BEAST_EXPECT( + result[sfMaximumAmount.getJsonName()] == "9223372036854775807"); + } + } + + void + testDestroyValidation(FeatureBitset features) + { + testcase("Destroy Validate"); + + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + // MPTokenIssuanceDestroy (preflight) + { + Env env{*this, features - featureMPTokensV1}; + MPTTester mptAlice(env, alice); + auto const id = getMptID(alice, env.seq(alice)); + mptAlice.destroy({.id = id, .ownerCount = 0, .err = temDISABLED}); + + env.enableFeature(featureMPTokensV1); + + mptAlice.destroy( + {.id = id, .flags = 0x00000001, .err = temINVALID_FLAG}); + } + + // MPTokenIssuanceDestroy (preclaim) + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.destroy( + {.id = getMptID(alice.id(), env.seq(alice)), + .ownerCount = 0, + .err = tecOBJECT_NOT_FOUND}); + + mptAlice.create({.ownerCount = 1}); + + // a non-issuer tries to destroy a mptissuance they didn't issue + mptAlice.destroy({.issuer = &bob, .err = tecNO_PERMISSION}); + + // Make sure that issuer can't delete issuance when it still has + // outstanding balance + { + // bob now holds a mptoken object + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + mptAlice.destroy({.err = tecHAS_OBLIGATIONS}); + } + } + } + + void + testDestroyEnabled(FeatureBitset features) + { + testcase("Destroy Enabled"); + + using namespace test::jtx; + Account const alice("alice"); + + // If the MPT amendment IS enabled, you should be able to destroy + // MPTokenIssuances + Env env{*this, features}; + MPTTester mptAlice(env, alice); + + mptAlice.create({.ownerCount = 1}); + + mptAlice.destroy({.ownerCount = 0}); + } + + void + testAuthorizeValidation(FeatureBitset features) + { + testcase("Validate authorize transaction"); + + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const cindy("cindy"); + // Validate amendment enable in MPTokenAuthorize (preflight) + { + Env env{*this, features - featureMPTokensV1}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.authorize( + {.account = &bob, + .id = getMptID(alice, env.seq(alice)), + .err = temDISABLED}); + } + + // Validate fields in MPTokenAuthorize (preflight) + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1}); + + mptAlice.authorize( + {.account = &bob, .flags = 0x00000002, .err = temINVALID_FLAG}); + + mptAlice.authorize( + {.account = &bob, .holder = &bob, .err = temMALFORMED}); + + mptAlice.authorize({.holder = &alice, .err = temMALFORMED}); + } + + // Try authorizing when MPTokenIssuance doesn't exist in + // MPTokenAuthorize (preclaim) + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + auto const id = getMptID(alice, env.seq(alice)); + + mptAlice.authorize( + {.holder = &bob, .id = id, .err = tecOBJECT_NOT_FOUND}); + + mptAlice.authorize( + {.account = &bob, .id = id, .err = tecOBJECT_NOT_FOUND}); + } + + // Test bad scenarios without allowlisting in MPTokenAuthorize + // (preclaim) + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1}); + + // bob submits a tx with a holder field + mptAlice.authorize( + {.account = &bob, .holder = &alice, .err = tecNO_PERMISSION}); + + // alice tries to hold onto her own token + mptAlice.authorize({.account = &alice, .err = tecNO_PERMISSION}); + + // the mpt does not enable allowlisting + mptAlice.authorize({.holder = &bob, .err = tecNO_AUTH}); + + // bob now holds a mptoken object + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // bob cannot create the mptoken the second time + mptAlice.authorize({.account = &bob, .err = tecMPTOKEN_EXISTS}); + + // Check that bob cannot delete MPToken when his balance is + // non-zero + { + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + // bob tries to delete his MPToken, but fails since he still + // holds tokens + mptAlice.authorize( + {.account = &bob, + .flags = tfMPTUnauthorize, + .err = tecHAS_OBLIGATIONS}); + + // bob pays back alice 100 tokens + mptAlice.pay(bob, alice, 100); + } + + // bob deletes/unauthorizes his MPToken + mptAlice.authorize({.account = &bob, .flags = tfMPTUnauthorize}); + + // bob receives error when he tries to delete his MPToken that has + // already been deleted + mptAlice.authorize( + {.account = &bob, + .holderCount = 0, + .flags = tfMPTUnauthorize, + .err = tecOBJECT_NOT_FOUND}); + } + + // Test bad scenarios with allow-listing in MPTokenAuthorize (preclaim) + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth}); + + // alice submits a tx without specifying a holder's account + mptAlice.authorize({.err = tecNO_PERMISSION}); + + // alice submits a tx to authorize a holder that hasn't created + // a mptoken yet + mptAlice.authorize({.holder = &bob, .err = tecOBJECT_NOT_FOUND}); + + // alice specifys a holder acct that doesn't exist + mptAlice.authorize({.holder = &cindy, .err = tecNO_DST}); + + // bob now holds a mptoken object + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // alice tries to unauthorize bob. + // although tx is successful, + // but nothing happens because bob hasn't been authorized yet + mptAlice.authorize({.holder = &bob, .flags = tfMPTUnauthorize}); + + // alice authorizes bob + // make sure bob's mptoken has set lsfMPTAuthorized + mptAlice.authorize({.holder = &bob}); + + // alice tries authorizes bob again. + // tx is successful, but bob is already authorized, + // so no changes + mptAlice.authorize({.holder = &bob}); + + // bob deletes his mptoken + mptAlice.authorize( + {.account = &bob, .holderCount = 0, .flags = tfMPTUnauthorize}); + } + + // Test mptoken reserve requirement - first two mpts free (doApply) + { + Env env{*this, features}; + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + + MPTTester mptAlice1( + env, + alice, + {.holders = {&bob}, + .xrpHolders = acctReserve + XRP(1).value().xrp()}); + mptAlice1.create(); + + MPTTester mptAlice2(env, alice, {.fund = false}); + mptAlice2.create(); + + MPTTester mptAlice3(env, alice, {.fund = false}); + mptAlice3.create({.ownerCount = 3}); + + // first mpt for free + mptAlice1.authorize({.account = &bob, .holderCount = 1}); + + // second mpt free + mptAlice2.authorize({.account = &bob, .holderCount = 2}); + + mptAlice3.authorize( + {.account = &bob, .err = tecINSUFFICIENT_RESERVE}); + + env(pay( + env.master, bob, drops(incReserve + incReserve + incReserve))); + env.close(); + + mptAlice3.authorize({.account = &bob, .holderCount = 3}); + } + } + + void + testAuthorizeEnabled(FeatureBitset features) + { + testcase("Authorize Enabled"); + + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + // Basic authorization without allowlisting + { + Env env{*this, features}; + + // alice create mptissuance without allowisting + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1}); + + // bob creates a mptoken + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // bob deletes his mptoken + mptAlice.authorize( + {.account = &bob, .holderCount = 0, .flags = tfMPTUnauthorize}); + } + + // With allowlisting + { + Env env{*this, features}; + + // alice creates a mptokenissuance that requires authorization + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth}); + + // bob creates a mptoken + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // alice authorizes bob + mptAlice.authorize({.account = &alice, .holder = &bob}); + + // Unauthorize bob's mptoken + mptAlice.authorize( + {.account = &alice, + .holder = &bob, + .holderCount = 1, + .flags = tfMPTUnauthorize}); + + mptAlice.authorize( + {.account = &bob, .holderCount = 0, .flags = tfMPTUnauthorize}); + } + + // Holder can have dangling MPToken even if issuance has been destroyed. + // Make sure they can still delete/unauthorize the MPToken + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1}); + + // bob creates a mptoken + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // alice deletes her issuance + mptAlice.destroy({.ownerCount = 0}); + + // bob can delete his mptoken even though issuance is no longer + // existent + mptAlice.authorize( + {.account = &bob, .holderCount = 0, .flags = tfMPTUnauthorize}); + } + } + + void + testSetValidation(FeatureBitset features) + { + testcase("Validate set transaction"); + + using namespace test::jtx; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + Account const cindy("cindy"); + // Validate fields in MPTokenIssuanceSet (preflight) + { + Env env{*this, features - featureMPTokensV1}; + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.set( + {.account = &bob, + .id = getMptID(alice, env.seq(alice)), + .err = temDISABLED}); + + env.enableFeature(featureMPTokensV1); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // test invalid flag + mptAlice.set( + {.account = &alice, + .flags = 0x00000008, + .err = temINVALID_FLAG}); + + // set both lock and unlock flags at the same time will fail + mptAlice.set( + {.account = &alice, + .flags = tfMPTLock | tfMPTUnlock, + .err = temINVALID_FLAG}); + + // if the holder is the same as the acct that submitted the tx, + // tx fails + mptAlice.set( + {.account = &alice, + .holder = &alice, + .flags = tfMPTLock, + .err = temMALFORMED}); + } + + // Validate fields in MPTokenIssuanceSet (preclaim) + // test when a mptokenissuance has disabled locking + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1}); + + // alice tries to lock a mptissuance that has disabled locking + mptAlice.set( + {.account = &alice, + .flags = tfMPTLock, + .err = tecNO_PERMISSION}); + + // alice tries to unlock mptissuance that has disabled locking + mptAlice.set( + {.account = &alice, + .flags = tfMPTUnlock, + .err = tecNO_PERMISSION}); + + // issuer tries to lock a bob's mptoken that has disabled + // locking + mptAlice.set( + {.account = &alice, + .holder = &bob, + .flags = tfMPTLock, + .err = tecNO_PERMISSION}); + + // issuer tries to unlock a bob's mptoken that has disabled + // locking + mptAlice.set( + {.account = &alice, + .holder = &bob, + .flags = tfMPTUnlock, + .err = tecNO_PERMISSION}); + } + + // Validate fields in MPTokenIssuanceSet (preclaim) + // test when mptokenissuance has enabled locking + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice trying to set when the mptissuance doesn't exist yet + mptAlice.set( + {.id = getMptID(alice.id(), env.seq(alice)), + .flags = tfMPTLock, + .err = tecOBJECT_NOT_FOUND}); + + // create a mptokenissuance with locking + mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock}); + + // a non-issuer acct tries to set the mptissuance + mptAlice.set( + {.account = &bob, .flags = tfMPTLock, .err = tecNO_PERMISSION}); + + // trying to set a holder who doesn't have a mptoken + mptAlice.set( + {.holder = &bob, + .flags = tfMPTLock, + .err = tecOBJECT_NOT_FOUND}); + + // trying to set a holder who doesn't exist + mptAlice.set( + {.holder = &cindy, .flags = tfMPTLock, .err = tecNO_DST}); + } + } + + void + testSetEnabled(FeatureBitset features) + { + testcase("Enabled set transaction"); + + using namespace test::jtx; + + // Test locking and unlocking + Env env{*this, features}; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // create a mptokenissuance with locking + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock}); + + mptAlice.authorize({.account = &bob, .holderCount = 1}); + + // locks bob's mptoken + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTLock}); + + // trying to lock bob's mptoken again will still succeed + // but no changes to the objects + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTLock}); + + // alice locks the mptissuance + mptAlice.set({.account = &alice, .flags = tfMPTLock}); + + // alice tries to lock up both mptissuance and mptoken again + // it will not change the flags and both will remain locked. + mptAlice.set({.account = &alice, .flags = tfMPTLock}); + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTLock}); + + // alice unlocks bob's mptoken + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTUnlock}); + + // locks up bob's mptoken again + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTLock}); + + // alice unlocks mptissuance + mptAlice.set({.account = &alice, .flags = tfMPTUnlock}); + + // alice unlocks bob's mptoken + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTUnlock}); + + // alice unlocks mptissuance and bob's mptoken again despite that + // they are already unlocked. Make sure this will not change the + // flags + mptAlice.set({.account = &alice, .holder = &bob, .flags = tfMPTUnlock}); + mptAlice.set({.account = &alice, .flags = tfMPTUnlock}); + } + + void + testPayment(FeatureBitset features) + { + testcase("Payment"); + + using namespace test::jtx; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + Account const carol("carol"); // holder + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + + // env(mpt::authorize(alice, id.key, std::nullopt)); + // env.close(); + + mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = &carol}); + + // issuer to holder + mptAlice.pay(alice, bob, 100); + + // holder to issuer + mptAlice.pay(bob, alice, 100); + + // holder to holder + mptAlice.pay(alice, bob, 100); + mptAlice.pay(bob, carol, 50); + } + + // Holder is not authorized + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + + // issuer to holder + mptAlice.pay(alice, bob, 100, tecNO_AUTH); + + // holder to issuer + mptAlice.pay(bob, alice, 100, tecNO_AUTH); + + // holder to holder + mptAlice.pay(bob, carol, 50, tecNO_AUTH); + } + + // If allowlisting is enabled, Payment fails if the receiver is not + // authorized + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTRequireAuth | tfMPTCanTransfer}); + + mptAlice.authorize({.account = &bob}); + + mptAlice.pay(alice, bob, 100, tecNO_AUTH); + } + + // If allowlisting is enabled, Payment fails if the sender is not + // authorized + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTRequireAuth | tfMPTCanTransfer}); + + // bob creates an empty MPToken + mptAlice.authorize({.account = &bob}); + + // alice authorizes bob to hold funds + mptAlice.authorize({.account = &alice, .holder = &bob}); + + // alice sends 100 MPT to bob + mptAlice.pay(alice, bob, 100); + + // alice UNAUTHORIZES bob + mptAlice.authorize( + {.account = &alice, .holder = &bob, .flags = tfMPTUnauthorize}); + + // bob fails to send back to alice because he is no longer + // authorize to move his funds! + mptAlice.pay(bob, alice, 100, tecNO_AUTH); + } + + // Payer doesn't have enough funds + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer}); + + mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = &carol}); + + mptAlice.pay(alice, bob, 100); + + // Pay to another holder + mptAlice.pay(bob, carol, 101, tecINSUFFICIENT_FUNDS); + + // Pay to the issuer + mptAlice.pay(bob, alice, 101, tecINSUFFICIENT_FUNDS); + } + + // MPT is locked + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create( + {.ownerCount = 1, .flags = tfMPTCanLock | tfMPTCanTransfer}); + + mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = &carol}); + + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 100); + + // Global lock + mptAlice.set({.account = &alice, .flags = tfMPTLock}); + // Can't send between holders + mptAlice.pay(bob, carol, 1, tecMPT_LOCKED); + mptAlice.pay(carol, bob, 2, tecMPT_LOCKED); + // Issuer can send + mptAlice.pay(alice, bob, 3); + // Holder can send back to issuer + mptAlice.pay(bob, alice, 4); + + // Global unlock + mptAlice.set({.account = &alice, .flags = tfMPTUnlock}); + // Individual lock + mptAlice.set( + {.account = &alice, .holder = &bob, .flags = tfMPTLock}); + // Can't send between holders + mptAlice.pay(bob, carol, 5, tecMPT_LOCKED); + mptAlice.pay(carol, bob, 6, tecMPT_LOCKED); + // Issuer can send + mptAlice.pay(alice, bob, 7); + // Holder can send back to issuer + mptAlice.pay(bob, alice, 8); + } + + // Issuer fails trying to send more than the maximum amount allowed + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create( + {.maxAmt = "100", + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer}); + + mptAlice.authorize({.account = &bob}); + + // issuer sends holder the max amount allowed + mptAlice.pay(alice, bob, 100); + + // issuer tries to exceed max amount + mptAlice.pay(alice, bob, 1, tecMPT_MAX_AMOUNT_EXCEEDED); + } + + // Issuer fails trying to send more than the default maximum + // amount allowed + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &bob}); + + // issuer sends holder the default max amount allowed + mptAlice.pay(alice, bob, maxMPTokenAmount); + + // issuer tries to exceed max amount + mptAlice.pay(alice, bob, 1, tecMPT_MAX_AMOUNT_EXCEEDED); + } + + // Can't pay negative amount + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &bob}); + + mptAlice.pay(alice, bob, -1, temBAD_AMOUNT); + } + + // pay more than max amount + // fails in the json parser before + // transactor is called + { + Env env{*this, features}; + env.fund(XRP(1'000), alice, bob); + STAmount mpt{MPTIssue{getMptID(alice.id(), 1)}, UINT64_C(100)}; + Json::Value jv; + jv[jss::secret] = alice.name(); + jv[jss::tx_json] = pay(alice, bob, mpt); + jv[jss::tx_json][jss::Amount][jss::value] = + to_string(maxMPTokenAmount + 1); + auto const jrr = env.rpc("json", "submit", to_string(jv)); + BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); + } + + // Transfer fee + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + // Transfer fee is 10% + mptAlice.create( + {.transferFee = 10'000, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer}); + + // Holders create MPToken + mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = &carol}); + + // Payment between the issuer and the holder, no transfer fee. + mptAlice.pay(alice, bob, 2'000); + + // Payment between the holder and the issuer, no transfer fee. + mptAlice.pay(alice, bob, 1'000); + + // Payment between the holders. The sender doesn't have + // enough funds to cover the transfer fee. + mptAlice.pay(bob, carol, 1'000); + + // Payment between the holders. The sender pays 10% transfer fee. + mptAlice.pay(bob, carol, 100); + } + + // Test that non-issuer cannot send to each other if MPTCanTransfer + // isn't set + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const cindy{"cindy"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob, &cindy}}); + + // alice creates issuance without MPTCanTransfer + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // cindy creates a MPToken + mptAlice.authorize({.account = &cindy}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + // bob tries to send cindy 10 tokens, but fails because canTransfer + // is off + mptAlice.pay(bob, cindy, 10, tecNO_AUTH); + + // bob can send back to alice(issuer) just fine + mptAlice.pay(bob, alice, 10); + } + + // MPT is disabled + { + Env env{*this, features - featureMPTokensV1}; + Account const alice("alice"); + Account const bob("bob"); + + env.fund(XRP(1'000), alice); + env.fund(XRP(1'000), bob); + STAmount mpt{MPTIssue{getMptID(alice.id(), 1)}, UINT64_C(100)}; + + env(pay(alice, bob, mpt), ter(temDISABLED)); + } + + // MPT is disabled, unsigned request + { + Env env{*this, features - featureMPTokensV1}; + Account const alice("alice"); // issuer + Account const carol("carol"); + auto const USD = alice["USD"]; + + env.fund(XRP(1'000), alice); + env.fund(XRP(1'000), carol); + STAmount mpt{MPTIssue{getMptID(alice.id(), 1)}, UINT64_C(100)}; + + Json::Value jv; + jv[jss::secret] = alice.name(); + jv[jss::tx_json][jss::Fee] = to_string(env.current()->fees().base); + jv[jss::tx_json] = pay(alice, carol, mpt); + auto const jrr = env.rpc("json", "submit", to_string(jv)); + BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "temDISABLED"); + } + + // Invalid combination of send, sendMax, deliverMin + { + Env env{*this, features}; + Account const alice("alice"); + Account const carol("carol"); + + MPTTester mptAlice(env, alice, {.holders = {&carol}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &carol}); + + // sendMax and DeliverMin are valid XRP amount, + // but is invalid combination with MPT amount + env(pay(alice, carol, mptAlice.mpt(100)), + sendmax(XRP(100)), + ter(temMALFORMED)); + env(pay(alice, carol, mptAlice.mpt(100)), + delivermin(XRP(100)), + ter(temMALFORMED)); + } + + // build_path is invalid if MPT + { + Env env{*this, features}; + Account const alice("alice"); + Account const carol("carol"); + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &carol}); + + Json::Value payment; + payment[jss::secret] = alice.name(); + payment[jss::tx_json] = pay(alice, carol, mptAlice.mpt(100)); + + payment[jss::build_path] = true; + auto jrr = env.rpc("json", "submit", to_string(payment)); + BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); + BEAST_EXPECT( + jrr[jss::result][jss::error_message] == + "Field 'build_path' not allowed in this context."); + } + + // Issuer fails trying to send fund after issuance was destroyed + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &bob}); + + // alice destroys issuance + mptAlice.destroy({.ownerCount = 0}); + + // alice tries to send bob fund after issuance is destroy, should + // fail. + mptAlice.pay(alice, bob, 100, tecMPT_ISSUANCE_NOT_FOUND); + } + + // Issuer fails trying to send to some who doesn't own MPT for a + // issuance that was destroyed + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + // alice destroys issuance + mptAlice.destroy({.ownerCount = 0}); + + // alice tries to send bob who doesn't own the MPT after issuance is + // destroyed, it should fail + mptAlice.pay(alice, bob, 100, tecMPT_ISSUANCE_NOT_FOUND); + } + + // Issuers issues maximum amount of MPT to a holder, the holder should + // be able to transfer the max amount to someone else + { + Env env{*this, features}; + Account const alice("alice"); + Account const carol("bob"); + Account const bob("carol"); + + MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); + + mptAlice.create( + {.maxAmt = "100", .ownerCount = 1, .flags = tfMPTCanTransfer}); + + mptAlice.authorize({.account = &bob}); + mptAlice.authorize({.account = &carol}); + + mptAlice.pay(alice, bob, 100); + + // transfer max amount to another holder + mptAlice.pay(bob, carol, 100); + } + } + + void + testMPTInvalidInTx(FeatureBitset features) + { + testcase("MPT Amount Invalid in Transaction"); + using namespace test::jtx; + + std::set txWithAmounts; + for (auto const& format : TxFormats::getInstance()) + { + for (auto const& e : format.getSOTemplate()) + { + // Transaction has amount fields. + // Exclude Clawback, which only supports sfAmount and is checked + // in the transactor for amendment enable/disable. Exclude + // pseudo-transaction SetFee. Don't consider the Fee field since + // it's included in every transaction. + if (e.supportMPT() != soeMPTNone && + e.sField().getName() != jss::Fee && + format.getName() != jss::Clawback && + format.getName() != jss::SetFee) + { + txWithAmounts.insert(format.getName()); + break; + } + } + } + + Account const alice("alice"); + auto const USD = alice["USD"]; + Account const carol("carol"); + MPTIssue issue(getMptID(alice.id(), 1)); + STAmount mpt{issue, UINT64_C(100)}; + auto const jvb = bridge(alice, USD, alice, USD); + for (auto const& feature : {features, features - featureMPTokensV1}) + { + Env env{*this, feature}; + env.fund(XRP(1'000), alice); + env.fund(XRP(1'000), carol); + auto test = [&](Json::Value const& jv) { + txWithAmounts.erase(jv[jss::TransactionType].asString()); + + // tx is signed + auto jtx = env.jt(jv); + Serializer s; + jtx.stx->add(s); + auto jrr = env.rpc("submit", strHex(s.slice())); + BEAST_EXPECT( + jrr[jss::result][jss::error] == "invalidTransaction"); + + // tx is unsigned + Json::Value jv1; + jv1[jss::secret] = alice.name(); + jv1[jss::tx_json] = jv; + jrr = env.rpc("json", "submit", to_string(jv1)); + BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); + }; + // All transactions with sfAmount, which don't support MPT + // and transactions with amount fields, which can't be MPT + + // AMMCreate + auto ammCreate = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::AMMCreate; + jv[jss::Account] = alice.human(); + jv[jss::Amount] = (field.fieldName == sfAmount.fieldName) + ? mpt.getJson(JsonOptions::none) + : "100000000"; + jv[jss::Amount2] = (field.fieldName == sfAmount2.fieldName) + ? mpt.getJson(JsonOptions::none) + : "100000000"; + jv[jss::TradingFee] = 0; + test(jv); + }; + ammCreate(sfAmount); + ammCreate(sfAmount2); + // AMMDeposit + auto ammDeposit = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::AMMDeposit; + jv[jss::Account] = alice.human(); + jv[jss::Asset] = to_json(xrpIssue()); + jv[jss::Asset2] = to_json(USD.issue()); + jv[field.fieldName] = mpt.getJson(JsonOptions::none); + jv[jss::Flags] = tfSingleAsset; + test(jv); + }; + ammDeposit(sfAmount); + for (SField const& field : + {std::ref(sfAmount2), + std::ref(sfEPrice), + std::ref(sfLPTokenOut)}) + ammDeposit(field); + // AMMWithdraw + auto ammWithdraw = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::AMMWithdraw; + jv[jss::Account] = alice.human(); + jv[jss::Asset] = to_json(xrpIssue()); + jv[jss::Asset2] = to_json(USD.issue()); + jv[jss::Flags] = tfSingleAsset; + jv[field.fieldName] = mpt.getJson(JsonOptions::none); + test(jv); + }; + ammWithdraw(sfAmount); + for (SField const& field : + {std::ref(sfAmount2), + std::ref(sfEPrice), + std::ref(sfLPTokenIn)}) + ammWithdraw(field); + // AMMBid + auto ammBid = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::AMMBid; + jv[jss::Account] = alice.human(); + jv[jss::Asset] = to_json(xrpIssue()); + jv[jss::Asset2] = to_json(USD.issue()); + jv[field.fieldName] = mpt.getJson(JsonOptions::none); + test(jv); + }; + ammBid(sfBidMin); + ammBid(sfBidMax); + // CheckCash + auto checkCash = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::CheckCash; + jv[jss::Account] = alice.human(); + jv[sfCheckID.fieldName] = to_string(uint256{1}); + jv[field.fieldName] = mpt.getJson(JsonOptions::none); + test(jv); + }; + checkCash(sfAmount); + checkCash(sfDeliverMin); + // CheckCreate + { + Json::Value jv; + jv[jss::TransactionType] = jss::CheckCreate; + jv[jss::Account] = alice.human(); + jv[jss::Destination] = carol.human(); + jv[jss::SendMax] = mpt.getJson(JsonOptions::none); + test(jv); + } + // EscrowCreate + { + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowCreate; + jv[jss::Account] = alice.human(); + jv[jss::Destination] = carol.human(); + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + test(jv); + } + // OfferCreate + { + Json::Value const jv = offer(alice, USD(100), mpt); + test(jv); + } + // PaymentChannelCreate + { + Json::Value jv; + jv[jss::TransactionType] = jss::PaymentChannelCreate; + jv[jss::Account] = alice.human(); + jv[jss::Destination] = carol.human(); + jv[jss::SettleDelay] = 1; + jv[sfPublicKey.fieldName] = strHex(alice.pk().slice()); + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + test(jv); + } + // PaymentChannelFund + { + Json::Value jv; + jv[jss::TransactionType] = jss::PaymentChannelFund; + jv[jss::Account] = alice.human(); + jv[sfChannel.fieldName] = to_string(uint256{1}); + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + test(jv); + } + // PaymentChannelClaim + { + Json::Value jv; + jv[jss::TransactionType] = jss::PaymentChannelClaim; + jv[jss::Account] = alice.human(); + jv[sfChannel.fieldName] = to_string(uint256{1}); + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + test(jv); + } + // Payment + auto payment = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::Payment; + jv[jss::Account] = alice.human(); + jv[jss::Destination] = carol.human(); + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + if (field == sfSendMax) + jv[jss::SendMax] = mpt.getJson(JsonOptions::none); + else + jv[jss::DeliverMin] = mpt.getJson(JsonOptions::none); + test(jv); + }; + payment(sfSendMax); + payment(sfDeliverMin); + // NFTokenCreateOffer + { + Json::Value jv; + jv[jss::TransactionType] = jss::NFTokenCreateOffer; + jv[jss::Account] = alice.human(); + jv[sfNFTokenID.fieldName] = to_string(uint256{1}); + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + test(jv); + } + // NFTokenAcceptOffer + { + Json::Value jv; + jv[jss::TransactionType] = jss::NFTokenAcceptOffer; + jv[jss::Account] = alice.human(); + jv[sfNFTokenBrokerFee.fieldName] = + mpt.getJson(JsonOptions::none); + test(jv); + } + // NFTokenMint + { + Json::Value jv; + jv[jss::TransactionType] = jss::NFTokenMint; + jv[jss::Account] = alice.human(); + jv[sfNFTokenTaxon.fieldName] = 1; + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + test(jv); + } + // TrustSet + auto trustSet = [&](SField const& field) { + Json::Value jv; + jv[jss::TransactionType] = jss::TrustSet; + jv[jss::Account] = alice.human(); + jv[jss::Flags] = 0; + jv[field.fieldName] = mpt.getJson(JsonOptions::none); + test(jv); + }; + trustSet(sfLimitAmount); + trustSet(sfFee); + // XChainCommit + { + Json::Value const jv = xchain_commit(alice, jvb, 1, mpt); + test(jv); + } + // XChainClaim + { + Json::Value const jv = xchain_claim(alice, jvb, 1, mpt, alice); + test(jv); + } + // XChainCreateClaimID + { + Json::Value const jv = + xchain_create_claim_id(alice, jvb, mpt, alice); + test(jv); + } + // XChainAddClaimAttestation + { + Json::Value const jv = claim_attestation( + alice, + jvb, + alice, + mpt, + alice, + true, + 1, + alice, + signer(alice)); + test(jv); + } + // XChainAddAccountCreateAttestation + { + Json::Value const jv = create_account_attestation( + alice, + jvb, + alice, + mpt, + XRP(10), + alice, + false, + 1, + alice, + signer(alice)); + test(jv); + } + // XChainAccountCreateCommit + { + Json::Value const jv = sidechain_xchain_account_create( + alice, jvb, alice, mpt, XRP(10)); + test(jv); + } + // XChain[Create|Modify]Bridge + auto bridgeTx = [&](Json::StaticString const& tt, + bool minAmount = false) { + Json::Value jv; + jv[jss::TransactionType] = tt; + jv[jss::Account] = alice.human(); + jv[sfXChainBridge.fieldName] = jvb; + jv[sfSignatureReward.fieldName] = + mpt.getJson(JsonOptions::none); + if (minAmount) + jv[sfMinAccountCreateAmount.fieldName] = + mpt.getJson(JsonOptions::none); + test(jv); + }; + bridgeTx(jss::XChainCreateBridge); + bridgeTx(jss::XChainCreateBridge, true); + bridgeTx(jss::XChainModifyBridge); + bridgeTx(jss::XChainModifyBridge, true); + } + BEAST_EXPECT(txWithAmounts.empty()); + } + + void + testTxJsonMetaFields(FeatureBitset features) + { + // checks synthetically parsed mptissuanceid from `tx` response + // it checks the parsing logic + testcase("Test synthetic fields from tx response"); + + using namespace test::jtx; + + Account const alice{"alice"}; + + Env env{*this, features}; + MPTTester mptAlice(env, alice); + + mptAlice.create(); + + std::string const txHash{ + env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; + + Json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta]; + + // Expect mpt_issuance_id field + BEAST_EXPECT(meta.isMember(jss::mpt_issuance_id)); + BEAST_EXPECT( + meta[jss::mpt_issuance_id] == to_string(mptAlice.issuanceID())); + } + + void + testClawbackValidation(FeatureBitset features) + { + testcase("MPT clawback validations"); + using namespace test::jtx; + + // Make sure clawback cannot work when featureMPTokensV1 is disabled + { + Env env(*this, features - featureMPTokensV1); + Account const alice{"alice"}; + Account const bob{"bob"}; + + env.fund(XRP(1000), alice, bob); + env.close(); + + auto const USD = alice["USD"]; + auto const mpt = ripple::test::jtx::MPT( + alice.name(), getMptID(alice.id(), env.seq(alice))); + + env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED)); + env.close(); + + env(claw(alice, mpt(5)), ter(temDISABLED)); + env.close(); + + env(claw(alice, mpt(5), bob), ter(temDISABLED)); + env.close(); + } + + // Test preflight + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + env.fund(XRP(1000), alice, bob); + env.close(); + + auto const USD = alice["USD"]; + auto const mpt = ripple::test::jtx::MPT( + alice.name(), getMptID(alice.id(), env.seq(alice))); + + // clawing back IOU from a MPT holder fails + env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED)); + env.close(); + + // clawing back MPT without specifying a holder fails + env(claw(alice, mpt(5)), ter(temMALFORMED)); + env.close(); + + // clawing back zero amount fails + env(claw(alice, mpt(0), bob), ter(temBAD_AMOUNT)); + env.close(); + + // alice can't claw back from herself + env(claw(alice, mpt(5), alice), ter(temMALFORMED)); + env.close(); + + // can't clawback negative amount + env(claw(alice, mpt(-1), bob), ter(temBAD_AMOUNT)); + env.close(); + } + + // Preclaim - clawback fails when MPTCanClawback is disabled on issuance + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // enable asfAllowTrustLineClawback for alice + env(fset(alice, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(alice, asfAllowTrustLineClawback)); + + // Create issuance without enabling clawback + mptAlice.create({.ownerCount = 1, .holderCount = 0}); + + mptAlice.authorize({.account = &bob}); + + mptAlice.pay(alice, bob, 100); + + // alice cannot clawback before she didn't enable MPTCanClawback + // asfAllowTrustLineClawback has no effect + mptAlice.claw(alice, bob, 1, tecNO_PERMISSION); + } + + // Preclaim - test various scenarios + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + env.fund(XRP(1000), carol); + env.close(); + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + auto const fakeMpt = ripple::test::jtx::MPT( + alice.name(), getMptID(alice.id(), env.seq(alice))); + + // issuer tries to clawback MPT where issuance doesn't exist + env(claw(alice, fakeMpt(5), bob), ter(tecOBJECT_NOT_FOUND)); + env.close(); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback}); + + // alice tries to clawback from someone who doesn't have MPToken + mptAlice.claw(alice, bob, 1, tecOBJECT_NOT_FOUND); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // clawback fails because bob currently has a balance of zero + mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + // carol fails tries to clawback from bob because he is not the + // issuer + mptAlice.claw(carol, bob, 1, tecNO_PERMISSION); + } + + // clawback more than max amount + // fails in the json parser before + // transactor is called + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + env.fund(XRP(1000), alice, bob); + env.close(); + + auto const mpt = ripple::test::jtx::MPT( + alice.name(), getMptID(alice.id(), env.seq(alice))); + + Json::Value jv = claw(alice, mpt(1), bob); + jv[jss::Amount][jss::value] = to_string(maxMPTokenAmount + 1); + Json::Value jv1; + jv1[jss::secret] = alice.name(); + jv1[jss::tx_json] = jv; + auto const jrr = env.rpc("json", "submit", to_string(jv1)); + BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); + } + } + + void + testClawback(FeatureBitset features) + { + testcase("MPT Clawback"); + using namespace test::jtx; + + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + mptAlice.claw(alice, bob, 1); + + mptAlice.claw(alice, bob, 1000); + + // clawback fails because bob currently has a balance of zero + mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS); + } + + // Test that globally locked funds can be clawed + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanLock | tfMPTCanClawback}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + mptAlice.set({.account = &alice, .flags = tfMPTLock}); + + mptAlice.claw(alice, bob, 100); + } + + // Test that individually locked funds can be clawed + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanLock | tfMPTCanClawback}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + mptAlice.set( + {.account = &alice, .holder = &bob, .flags = tfMPTLock}); + + mptAlice.claw(alice, bob, 100); + } + + // Test that unauthorized funds can be clawed back + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {&bob}}); + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanClawback | tfMPTRequireAuth}); + + // bob creates a MPToken + mptAlice.authorize({.account = &bob}); + + // alice authorizes bob + mptAlice.authorize({.account = &alice, .holder = &bob}); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + // alice unauthorizes bob + mptAlice.authorize( + {.account = &alice, .holder = &bob, .flags = tfMPTUnauthorize}); + + mptAlice.claw(alice, bob, 100); + } + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + + // MPTokenIssuanceCreate + testCreateValidation(all); + testCreateEnabled(all); + + // MPTokenIssuanceDestroy + testDestroyValidation(all); + testDestroyEnabled(all); + + // MPTokenAuthorize + testAuthorizeValidation(all); + testAuthorizeEnabled(all); + + // MPTokenIssuanceSet + testSetValidation(all); + testSetEnabled(all); + + // MPT clawback + testClawbackValidation(all); + testClawback(all); + + // Test Direct Payment + testPayment(all); + + // Test MPT Amount is invalid in Tx, which don't support MPT + testMPTInvalidInTx(all); + + // Test parsed MPTokenIssuanceID in API response metadata + testTxJsonMetaFields(all); + } +}; + +BEAST_DEFINE_TESTSUITE_PRIO(MPToken, tx, ripple, 2); + +} // namespace ripple diff --git a/src/test/app/SetAuth_test.cpp b/src/test/app/SetAuth_test.cpp index adb909314d3..3dd8ab590a4 100644 --- a/src/test/app/SetAuth_test.cpp +++ b/src/test/app/SetAuth_test.cpp @@ -38,8 +38,8 @@ struct SetAuth_test : public beast::unit_test::suite using namespace jtx; Json::Value jv; jv[jss::Account] = account.human(); - jv[jss::LimitAmount] = - STAmount({to_currency(currency), dest}).getJson(JsonOptions::none); + jv[jss::LimitAmount] = STAmount(Issue{to_currency(currency), dest}) + .getJson(JsonOptions::none); jv[jss::TransactionType] = jss::TrustSet; jv[jss::Flags] = tfSetfAuth; return jv; diff --git a/src/test/app/TrustAndBalance_test.cpp b/src/test/app/TrustAndBalance_test.cpp index bf7c8629b69..b438d797276 100644 --- a/src/test/app/TrustAndBalance_test.cpp +++ b/src/test/app/TrustAndBalance_test.cpp @@ -400,7 +400,6 @@ class TrustAndBalance_test : public beast::unit_test::suite carol["USD"].issue(), 6500000000000000ull, -14, - false, true, STAmount::unchecked{}))); env.require(balance(carol, gw["USD"](35))); diff --git a/src/test/jtx.h b/src/test/jtx.h index 6de7cd480fa..49790e34022 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 2c5f2f37062..d90d2bc1228 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -446,6 +446,12 @@ class Env PrettyAmount balance(Account const& account, Issue const& issue) const; + /** Return the number of objects owned by an account. + * Returns 0 if the account does not exist. + */ + std::uint32_t + ownerCount(Account const& account) const; + /** Return an account root. @return empty if the account does not exist. */ diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index c8e0d0c3701..122e21726f5 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -305,15 +305,19 @@ class IOU return {currency, account.id()}; } - /** Implicit conversion to Issue. + /** Implicit conversion to Issue or Asset. This allows passing an IOU - value where an Issue is expected. + value where an Issue or Asset is expected. */ operator Issue() const { return issue(); } + operator Asset() const + { + return issue(); + } template < class T, @@ -351,6 +355,77 @@ operator<<(std::ostream& os, IOU const& iou); //------------------------------------------------------------------------------ +/** Converts to MPT Issue or STAmount. + + Examples: + MPT Converts to the underlying Issue + MPT(10) Returns STAmount of 10 of + the underlying MPT +*/ +class MPT +{ +public: + std::string name; + ripple::MPTID mptID; + + MPT(std::string const& n, ripple::MPTID const& mptID_) + : name(n), mptID(mptID_) + { + } + + ripple::MPTID const& + mpt() const + { + return mptID; + } + + /** Implicit conversion to MPTIssue. + + This allows passing an MPT + value where an MPTIssue is expected. + */ + operator ripple::MPTIssue() const + { + return mpt(); + } + + template + requires(sizeof(T) >= sizeof(int) && std::is_arithmetic_v) PrettyAmount + operator()(T v) const + { + // VFALCO NOTE Should throw if the + // representation of v is not exact. + return {amountFromString(mpt(), std::to_string(v)), name}; + } + + PrettyAmount operator()(epsilon_t) const; + PrettyAmount operator()(detail::epsilon_multiple) const; + + // VFALCO TODO + // STAmount operator()(char const* s) const; + + /** Returns None-of-Issue */ +#if 0 + None operator()(none_t) const + { + return {Issue{}}; + } +#endif + + friend BookSpec + operator~(MPT const& mpt) + { + assert(false); + Throw("MPT is not supported"); + return BookSpec{beast::zero, noCurrency()}; + } +}; + +std::ostream& +operator<<(std::ostream& os, MPT const& mpt); + +//------------------------------------------------------------------------------ + struct any_t { inline AnyAmount diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp new file mode 100644 index 00000000000..7cd60afed67 --- /dev/null +++ b/src/test/jtx/impl/mpt.cpp @@ -0,0 +1,398 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +void +mptflags::operator()(Env& env) const +{ + env.test.expect(tester_.checkFlags(flags_, holder_)); +} + +void +mptpay::operator()(Env& env) const +{ + env.test.expect(amount_ == tester_.getAmount(account_)); +} + +void +requireAny::operator()(Env& env) const +{ + env.test.expect(cb_()); +} + +std::unordered_map +MPTTester::makeHolders(std::vector const& holders) +{ + std::unordered_map accounts; + for (auto const& h : holders) + { + assert(h && holders_.find(h->human()) == accounts.cend()); + accounts.emplace(h->human(), h); + } + return accounts; +} + +MPTTester::MPTTester(Env& env, Account const& issuer, MPTConstr const& arg) + : env_(env) + , issuer_(issuer) + , holders_(makeHolders(arg.holders)) + , close_(arg.close) +{ + if (arg.fund) + { + env_.fund(arg.xrp, issuer_); + for (auto it : holders_) + env_.fund(arg.xrpHolders, *it.second); + } + if (close_) + env.close(); + if (arg.fund) + { + env_.require(owners(issuer_, 0)); + for (auto it : holders_) + { + assert(issuer_.id() != it.second->id()); + env_.require(owners(*it.second, 0)); + } + } +} + +void +MPTTester::create(const MPTCreate& arg) +{ + if (issuanceKey_) + Throw("MPT can't be reused"); + id_ = getMptID(issuer_.id(), env_.seq(issuer_)); + issuanceKey_ = keylet::mptIssuance(*id_).key; + Json::Value jv; + jv[sfAccount.jsonName] = issuer_.human(); + jv[sfTransactionType.jsonName] = jss::MPTokenIssuanceCreate; + if (arg.assetScale) + jv[sfAssetScale.jsonName] = *arg.assetScale; + if (arg.transferFee) + jv[sfTransferFee.jsonName] = *arg.transferFee; + if (arg.metadata) + jv[sfMPTokenMetadata.jsonName] = strHex(*arg.metadata); + if (arg.maxAmt) + jv[sfMaximumAmount.jsonName] = *arg.maxAmt; + if (submit(arg, jv) != tesSUCCESS) + { + // Verify issuance doesn't exist + env_.require(requireAny([&]() -> bool { + return env_.le(keylet::mptIssuance(*id_)) == nullptr; + })); + + id_.reset(); + issuanceKey_.reset(); + } + else if (arg.flags) + env_.require(mptflags(*this, *arg.flags)); +} + +void +MPTTester::destroy(MPTDestroy const& arg) +{ + Json::Value jv; + if (arg.issuer) + jv[sfAccount.jsonName] = arg.issuer->human(); + else + jv[sfAccount.jsonName] = issuer_.human(); + if (arg.id) + jv[sfMPTokenIssuanceID.jsonName] = to_string(*arg.id); + else + { + assert(id_); + jv[sfMPTokenIssuanceID.jsonName] = to_string(*id_); + } + jv[sfTransactionType.jsonName] = jss::MPTokenIssuanceDestroy; + submit(arg, jv); +} + +Account const& +MPTTester::holder(std::string const& holder_) const +{ + auto const& it = holders_.find(holder_); + assert(it != holders_.cend()); + if (it == holders_.cend()) + Throw("Holder is not found"); + return *it->second; +} + +void +MPTTester::authorize(MPTAuthorize const& arg) +{ + Json::Value jv; + if (arg.account) + jv[sfAccount.jsonName] = arg.account->human(); + else + jv[sfAccount.jsonName] = issuer_.human(); + jv[sfTransactionType.jsonName] = jss::MPTokenAuthorize; + if (arg.id) + jv[sfMPTokenIssuanceID.jsonName] = to_string(*arg.id); + else + { + assert(id_); + jv[sfMPTokenIssuanceID.jsonName] = to_string(*id_); + } + if (arg.holder) + jv[sfMPTokenHolder.jsonName] = arg.holder->human(); + if (auto const result = submit(arg, jv); result == tesSUCCESS) + { + // Issuer authorizes + if (arg.account == nullptr || *arg.account == issuer_) + { + auto const flags = getFlags(arg.holder); + // issuer un-authorizes the holder + if (arg.flags.value_or(0) == tfMPTUnauthorize) + env_.require(mptflags(*this, flags, arg.holder)); + // issuer authorizes the holder + else + env_.require( + mptflags(*this, flags | lsfMPTAuthorized, arg.holder)); + } + // Holder authorizes + else if (arg.flags.value_or(0) == 0) + { + auto const flags = getFlags(arg.account); + // holder creates a token + env_.require(mptflags(*this, flags, arg.account)); + env_.require(mptpay(*this, *arg.account, 0)); + } + } + else if ( + arg.account != nullptr && *arg.account != issuer_ && + arg.flags.value_or(0) == 0 && issuanceKey_) + { + if (result == tecMPTOKEN_EXISTS) + { + // Verify that MPToken already exists + env_.require(requireAny([&]() -> bool { + return env_.le(keylet::mptoken( + *issuanceKey_, arg.account->id())) != nullptr; + })); + } + else + { + // Verify MPToken doesn't exist if holder failed authorizing(unless + // it already exists) + env_.require(requireAny([&]() -> bool { + return env_.le(keylet::mptoken( + *issuanceKey_, arg.account->id())) == nullptr; + })); + } + } +} + +void +MPTTester::set(MPTSet const& arg) +{ + Json::Value jv; + if (arg.account) + jv[sfAccount.jsonName] = arg.account->human(); + else + jv[sfAccount.jsonName] = issuer_.human(); + jv[sfTransactionType.jsonName] = jss::MPTokenIssuanceSet; + if (arg.id) + jv[sfMPTokenIssuanceID.jsonName] = to_string(*arg.id); + else + { + assert(id_); + jv[sfMPTokenIssuanceID.jsonName] = to_string(*id_); + } + if (arg.holder) + jv[sfMPTokenHolder.jsonName] = arg.holder->human(); + if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0)) + { + auto require = [&](AccountP holder, bool unchanged) { + auto flags = getFlags(holder); + if (!unchanged) + { + if (*arg.flags & tfMPTLock) + flags |= lsfMPTLocked; + else if (*arg.flags & tfMPTUnlock) + flags &= ~lsfMPTLocked; + else + assert(0); + } + env_.require(mptflags(*this, flags, holder)); + }; + if (arg.account) + require(nullptr, arg.holder != nullptr); + if (arg.holder) + require(arg.holder, false); + } +} + +bool +MPTTester::forObject( + std::function const& cb, + AccountP holder_) const +{ + assert(issuanceKey_); + auto const key = [&]() { + if (holder_) + return keylet::mptoken(*issuanceKey_, holder_->id()); + return keylet::mptIssuance(*issuanceKey_); + }(); + if (auto const sle = env_.le(key)) + return cb(sle); + return false; +} + +[[nodiscard]] bool +MPTTester::checkMPTokenAmount( + Account const& holder_, + std::int64_t expectedAmount) const +{ + return forObject( + [&](SLEP const& sle) { return expectedAmount == (*sle)[sfMPTAmount]; }, + &holder_); +} + +[[nodiscard]] bool +MPTTester::checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const +{ + return forObject([&](SLEP const& sle) { + return expectedAmount == (*sle)[sfOutstandingAmount]; + }); +} + +[[nodiscard]] bool +MPTTester::checkFlags(uint32_t const expectedFlags, AccountP holder) const +{ + return expectedFlags == getFlags(holder); +} + +void +MPTTester::pay( + Account const& src, + Account const& dest, + std::int64_t amount, + std::optional err) +{ + assert(id_); + auto const srcAmt = getAmount(src); + auto const destAmt = getAmount(dest); + auto const outstnAmt = getAmount(issuer_); + if (err) + env_(jtx::pay(src, dest, mpt(amount)), ter(*err)); + else + env_(jtx::pay(src, dest, mpt(amount))); + if (env_.ter() != tesSUCCESS) + amount = 0; + if (close_) + env_.close(); + if (src == issuer_) + { + env_.require(mptpay(*this, src, srcAmt + amount)); + env_.require(mptpay(*this, dest, destAmt + amount)); + } + else if (dest == issuer_) + { + env_.require(mptpay(*this, src, srcAmt - amount)); + env_.require(mptpay(*this, dest, destAmt - amount)); + } + else + { + STAmount const saAmount = {*id_, amount}; + STAmount const saActual = + multiply(saAmount, transferRate(*env_.current(), *id_)); + // Sender pays the transfer fee if any + env_.require(mptpay(*this, src, srcAmt - saActual.mpt().value())); + env_.require(mptpay(*this, dest, destAmt + amount)); + // Outstanding amount is reduced by the transfer fee if any + env_.require(mptpay( + *this, issuer_, outstnAmt - (saActual - saAmount).mpt().value())); + } +} + +void +MPTTester::claw( + Account const& issuer, + Account const& holder, + std::int64_t amount, + std::optional err) +{ + assert(id_); + auto const issuerAmt = getAmount(issuer); + auto const holderAmt = getAmount(holder); + if (err) + env_(jtx::claw(issuer, mpt(amount), holder), ter(*err)); + else + env_(jtx::claw(issuer, mpt(amount), holder)); + if (env_.ter() != tesSUCCESS) + amount = 0; + if (close_) + env_.close(); + + env_.require( + mptpay(*this, issuer, issuerAmt - std::min(holderAmt, amount))); + env_.require( + mptpay(*this, holder, holderAmt - std::min(holderAmt, amount))); +} + +PrettyAmount +MPTTester::mpt(std::int64_t amount) const +{ + assert(id_); + return ripple::test::jtx::MPT(issuer_.name(), *id_)(amount); +} + +std::int64_t +MPTTester::getAmount(Account const& account) const +{ + assert(issuanceKey_); + if (account == issuer_) + { + if (auto const sle = env_.le(keylet::mptIssuance(*issuanceKey_))) + return sle->getFieldU64(sfOutstandingAmount); + } + else + { + if (auto const sle = + env_.le(keylet::mptoken(*issuanceKey_, account.id()))) + return sle->getFieldU64(sfMPTAmount); + } + return 0; +} + +std::uint32_t +MPTTester::getFlags(ripple::test::jtx::AccountP holder) const +{ + std::uint32_t flags = 0; + if (!forObject( + [&](SLEP const& sle) { + flags = sle->getFlags(); + return true; + }, + holder)) + Throw("Failed to get the flags"); + return flags; +} + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/trust.cpp b/src/test/jtx/impl/trust.cpp index 641a0f79f28..320c7d05c7c 100644 --- a/src/test/jtx/impl/trust.cpp +++ b/src/test/jtx/impl/trust.cpp @@ -64,13 +64,19 @@ trust( } Json::Value -claw(Account const& account, STAmount const& amount) +claw( + Account const& account, + STAmount const& amount, + std::optional const& mptHolder) { Json::Value jv; jv[jss::Account] = account.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); jv[jss::TransactionType] = jss::Clawback; + if (mptHolder) + jv[sfMPTokenHolder.jsonName] = mptHolder->human(); + return jv; } diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h new file mode 100644 index 00000000000..e66433260eb --- /dev/null +++ b/src/test/jtx/mpt.h @@ -0,0 +1,264 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_MPT_H_INCLUDED +#define RIPPLE_TEST_JTX_MPT_H_INCLUDED + +#include +#include +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace { +using AccountP = Account const*; +} + +class MPTTester; + +// Check flags settings on MPT create +class mptflags +{ +private: + MPTTester& tester_; + std::uint32_t flags_; + AccountP holder_; + +public: + mptflags(MPTTester& tester, std::uint32_t flags, AccountP holder = nullptr) + : tester_(tester), flags_(flags), holder_(holder) + { + } + + void + operator()(Env& env) const; +}; + +// Check mptissuance or mptoken amount balances on payment +class mptpay +{ +private: + MPTTester const& tester_; + Account const& account_; + std::int64_t const amount_; + +public: + mptpay(MPTTester& tester, Account const& account, std::int64_t amount) + : tester_(tester), account_(account), amount_(amount) + { + } + + void + operator()(Env& env) const; +}; + +class requireAny +{ +private: + std::function cb_; + +public: + requireAny(std::function const& cb) : cb_(cb) + { + } + + void + operator()(Env& env) const; +}; + +struct MPTConstr +{ + std::vector holders = {}; + PrettyAmount const& xrp = XRP(10'000); + PrettyAmount const& xrpHolders = XRP(10'000); + bool fund = true; + bool close = true; +}; + +struct MPTCreate +{ + std::optional maxAmt = std::nullopt; + std::optional assetScale = std::nullopt; + std::optional transferFee = std::nullopt; + std::optional metadata = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + bool fund = true; + std::optional flags = {0}; + std::optional err = std::nullopt; +}; + +struct MPTDestroy +{ + AccountP issuer = nullptr; + std::optional id = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; + std::optional err = std::nullopt; +}; + +struct MPTAuthorize +{ + AccountP account = nullptr; + AccountP holder = nullptr; + std::optional id = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; + std::optional err = std::nullopt; +}; + +struct MPTSet +{ + AccountP account = nullptr; + AccountP holder = nullptr; + std::optional id = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; + std::optional err = std::nullopt; +}; + +class MPTTester +{ + Env& env_; + Account const& issuer_; + std::unordered_map const holders_; + std::optional id_; + std::optional issuanceKey_; + bool close_; + +public: + MPTTester(Env& env, Account const& issuer, MPTConstr const& constr = {}); + + void + create(MPTCreate const& arg = MPTCreate{}); + + void + destroy(MPTDestroy const& arg = MPTDestroy{}); + + void + authorize(MPTAuthorize const& arg = MPTAuthorize{}); + + void + set(MPTSet const& set = {}); + + [[nodiscard]] bool + checkMPTokenAmount(Account const& holder, std::int64_t expectedAmount) + const; + + [[nodiscard]] bool + checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const; + + [[nodiscard]] bool + checkFlags(uint32_t const expectedFlags, AccountP holder = nullptr) const; + + Account const& + issuer() const + { + return issuer_; + } + Account const& + holder(std::string const& h) const; + + void + pay(Account const& src, + Account const& dest, + std::int64_t amount, + std::optional err = std::nullopt); + + void + claw( + Account const& issuer, + Account const& holder, + std::int64_t amount, + std::optional err = std::nullopt); + + PrettyAmount + mpt(std::int64_t amount) const; + + uint256 const& + issuanceKey() const + { + assert(issuanceKey_); + return *issuanceKey_; + } + + MPTID const& + issuanceID() const + { + assert(id_); + return *id_; + } + + std::int64_t + getAmount(Account const& account) const; + +private: + using SLEP = std::shared_ptr; + bool + forObject( + std::function const& cb, + AccountP holder = nullptr) const; + + template + TER + submit(A const& arg, Json::Value const& jv) + { + if (arg.err) + { + if (arg.flags && arg.flags > 0) + env_(jv, txflags(*arg.flags), ter(*arg.err)); + else + env_(jv, ter(*arg.err)); + } + else if (arg.flags && arg.flags > 0) + env_(jv, txflags(*arg.flags)); + else + env_(jv); + auto const err = env_.ter(); + if (close_) + env_.close(); + if (arg.ownerCount) + env_.require(owners(issuer_, *arg.ownerCount)); + if (arg.holderCount) + { + for (auto it : holders_) + env_.require(owners(*it.second, *arg.holderCount)); + } + return err; + } + + std::unordered_map + makeHolders(std::vector const& holders); + + std::uint32_t + getFlags(AccountP holder) const; +}; + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/trust.h b/src/test/jtx/trust.h index f9fddf4871a..0d02c6e76c4 100644 --- a/src/test/jtx/trust.h +++ b/src/test/jtx/trust.h @@ -41,7 +41,10 @@ trust( std::uint32_t flags); Json::Value -claw(Account const& account, STAmount const& amount); +claw( + Account const& account, + STAmount const& amount, + std::optional const& mptHolder = std::nullopt); } // namespace jtx } // namespace test diff --git a/src/test/ledger/PaymentSandbox_test.cpp b/src/test/ledger/PaymentSandbox_test.cpp index e3ede19b4b6..dd9b5c5d88b 100644 --- a/src/test/ledger/PaymentSandbox_test.cpp +++ b/src/test/ledger/PaymentSandbox_test.cpp @@ -316,14 +316,12 @@ class PaymentSandbox_test : public beast::unit_test::suite STAmount::cMinValue, STAmount::cMinOffset + 1, false, - false, STAmount::unchecked{}); STAmount hugeAmt( issue, STAmount::cMaxValue, STAmount::cMaxOffset - 1, false, - false, STAmount::unchecked{}); ApplyViewImpl av(&*env.current(), tapNONE); diff --git a/src/test/protocol/Quality_test.cpp b/src/test/protocol/Quality_test.cpp index 741a341d980..64cf0c71b3a 100644 --- a/src/test/protocol/Quality_test.cpp +++ b/src/test/protocol/Quality_test.cpp @@ -29,7 +29,7 @@ class Quality_test : public beast::unit_test::suite // Create a raw, non-integral amount from mantissa and exponent STAmount static raw(std::uint64_t mantissa, int exponent) { - return STAmount({Currency(3), AccountID(3)}, mantissa, exponent); + return STAmount(Issue{Currency(3), AccountID(3)}, mantissa, exponent); } template diff --git a/src/test/protocol/STAmount_test.cpp b/src/test/protocol/STAmount_test.cpp index e48d0500ba6..b512c42a643 100644 --- a/src/test/protocol/STAmount_test.cpp +++ b/src/test/protocol/STAmount_test.cpp @@ -62,7 +62,6 @@ class STAmount_test : public beast::unit_test::suite amount.issue(), mantissa, amount.exponent(), - amount.native(), amount.negative(), STAmount::unchecked{}}; } @@ -82,7 +81,6 @@ class STAmount_test : public beast::unit_test::suite amount.issue(), mantissa, amount.exponent(), - amount.native(), amount.negative(), STAmount::unchecked{}}; } diff --git a/src/test/protocol/STTx_test.cpp b/src/test/protocol/STTx_test.cpp index e0f6796af33..934bd8050c0 100644 --- a/src/test/protocol/STTx_test.cpp +++ b/src/test/protocol/STTx_test.cpp @@ -1344,22 +1344,22 @@ class STTx_test : public beast::unit_test::suite 0x73, 0x00, 0x81, 0x14, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe5, 0xfe, 0xf3, 0xe7, 0xe5, 0x65, 0x24, 0x00, 0x00, 0x00, 0x00, - 0x20, 0x1e, 0x00, 0x6f, 0x00, 0x00, 0x20, 0x1f, 0x03, 0xf6, 0x00, - 0x00, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0x59, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x40, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x02, 0x00, 0x12, 0x00, 0x65, 0x24, 0x00, 0x00, 0x00, 0x00, - 0x20, 0x1e, 0x00, 0x4f, 0x00, 0x00, 0x20, 0x1f, 0x03, 0xf6, 0x00, - 0x00, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x35, 0x24, 0x59, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x40, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x02, 0x00, 0x54, 0x72, 0x61, 0x6e, 0x00, 0x10, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x65, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xe5, 0xfe, 0xf3, 0xe7, 0xe5, 0x65, 0x24, 0x00, - 0x00, 0x00, 0x00, 0x20, 0x1e, 0x00, 0x6f, 0x00, 0x00, 0x20, 0xf6, - 0x00, 0x00, 0x03, 0x1f, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x35, - 0x00, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x40, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x73, 0x00, 0x81, 0x14, 0x00, - 0x10, 0x00, 0x73, 0x00, 0x81, 0x14, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0xe5, 0xfe}; + 0x20, 0x1e, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x20, 0x1f, 0x03, 0xf6, + 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0x59, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x12, 0x00, 0x65, 0x24, 0x00, 0x00, 0x00, + 0x00, 0x20, 0x1e, 0x00, 0x4f, 0x00, 0x00, 0x20, 0x1f, 0x03, 0xf6, + 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x35, 0x24, 0x59, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x54, 0x72, 0x61, 0x6e, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x24, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xe5, 0xfe, 0xf3, 0xe7, 0xe5, 0x65, 0x24, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x1e, 0x00, 0x6f, 0x00, 0x00, 0x20, + 0xf6, 0x00, 0x00, 0x03, 0x1f, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x35, 0x00, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x73, 0x00, 0x81, 0x14, + 0x00, 0x10, 0x00, 0x73, 0x00, 0x81, 0x14, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0xe5, 0xfe}; // Construct an STObject with 11 levels of object nesting so the // maximum nesting level exception is thrown. diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp b/src/xrpld/app/ledger/detail/LedgerToJson.cpp index 9824b31d794..3f6869df1d8 100644 --- a/src/xrpld/app/ledger/detail/LedgerToJson.cpp +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -156,6 +157,12 @@ fillJsonTx( fill.ledger, txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); + + // If applicable, insert mpt issuance id + RPC::insertMPTokenIssuanceID( + txJson[jss::meta], + txn, + {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } if (!fill.ledger.open()) @@ -187,6 +194,12 @@ fillJsonTx( fill.ledger, txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); + + // If applicable, insert mpt issuance id + RPC::insertMPTokenIssuanceID( + txJson[jss::metaData], + txn, + {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } } diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 208aab05aa1..7868807c52a 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -2946,6 +2947,8 @@ NetworkOPsImp::transJson( jvObj[jss::meta] = meta->get().getJson(JsonOptions::none); RPC::insertDeliveredAmount( jvObj[jss::meta], *ledger, transaction, meta->get()); + RPC::insertMPTokenIssuanceID( + jvObj[jss::meta], transaction, meta->get()); } if (!ledger->open()) diff --git a/src/xrpld/app/paths/Credit.cpp b/src/xrpld/app/paths/Credit.cpp index c11f628a11d..b3870937367 100644 --- a/src/xrpld/app/paths/Credit.cpp +++ b/src/xrpld/app/paths/Credit.cpp @@ -31,7 +31,7 @@ creditLimit( AccountID const& issuer, Currency const& currency) { - STAmount result({currency, account}); + STAmount result(Issue{currency, account}); auto sleRippleState = view.read(keylet::line(account, issuer, currency)); @@ -64,7 +64,7 @@ creditBalance( AccountID const& issuer, Currency const& currency) { - STAmount result({currency, account}); + STAmount result(Issue{currency, account}); auto sleRippleState = view.read(keylet::line(account, issuer, currency)); diff --git a/src/xrpld/app/paths/PathRequest.cpp b/src/xrpld/app/paths/PathRequest.cpp index 4cd9f7d71f7..bb6a104bca2 100644 --- a/src/xrpld/app/paths/PathRequest.cpp +++ b/src/xrpld/app/paths/PathRequest.cpp @@ -562,7 +562,7 @@ PathRequest::findPaths( }(); STAmount saMaxAmount = saSendMax.value_or( - STAmount({issue.currency, sourceAccount}, 1u, 0, true)); + STAmount(Issue{issue.currency, sourceAccount}, 1u, 0, true)); JLOG(m_journal.debug()) << iIdentifier << " Paths found, calling rippleCalc"; diff --git a/src/xrpld/app/paths/Pathfinder.cpp b/src/xrpld/app/paths/Pathfinder.cpp index 885a8ae9b47..a5fe2afe949 100644 --- a/src/xrpld/app/paths/Pathfinder.cpp +++ b/src/xrpld/app/paths/Pathfinder.cpp @@ -176,9 +176,10 @@ Pathfinder::Pathfinder( , mSrcCurrency(uSrcCurrency) , mSrcIssuer(uSrcIssuer) , mSrcAmount(srcAmount.value_or(STAmount( - {uSrcCurrency, - uSrcIssuer.value_or( - isXRP(uSrcCurrency) ? xrpAccount() : uSrcAccount)}, + Issue{ + uSrcCurrency, + uSrcIssuer.value_or( + isXRP(uSrcCurrency) ? xrpAccount() : uSrcAccount)}, 1u, 0, true))) diff --git a/src/xrpld/app/tx/detail/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index 15d76526094..c11dfbd8c11 100644 --- a/src/xrpld/app/tx/detail/Clawback.cpp +++ b/src/xrpld/app/tx/detail/Clawback.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -27,8 +28,13 @@ namespace ripple { +template +static NotTEC +preflightHelper(PreflightContext const& ctx); + +template <> NotTEC -Clawback::preflight(PreflightContext const& ctx) +preflightHelper(PreflightContext const& ctx) { if (!ctx.rules.enabled(featureClawback)) return temDISABLED; @@ -39,6 +45,9 @@ Clawback::preflight(PreflightContext const& ctx) if (ctx.tx.getFlags() & tfClawbackMask) return temINVALID_FLAG; + if (ctx.tx.isFieldPresent(sfMPTokenHolder)) + return temMALFORMED; + AccountID const issuer = ctx.tx[sfAccount]; STAmount const clawAmount = ctx.tx[sfAmount]; @@ -51,8 +60,46 @@ Clawback::preflight(PreflightContext const& ctx) return preflight2(ctx); } +template <> +NotTEC +preflightHelper(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureClawback)) + return temDISABLED; + + auto const mptHolder = ctx.tx[~sfMPTokenHolder]; + auto const clawAmount = ctx.tx[sfAmount]; + + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (!mptHolder) + return temMALFORMED; + + if (ctx.tx.getFlags() & tfClawbackMask) + return temINVALID_FLAG; + + // issuer is the same as holder + if (ctx.tx[sfAccount] == *mptHolder) + return temMALFORMED; + + if (clawAmount.mpt() > MPTAmount{maxMPTokenAmount} || + clawAmount <= beast::zero) + return temBAD_AMOUNT; + + return preflight2(ctx); +} + +template +static TER +preclaimHelper(PreclaimContext const& ctx); + +template <> TER -Clawback::preclaim(PreclaimContext const& ctx) +preclaimHelper(PreclaimContext const& ctx) { AccountID const issuer = ctx.tx[sfAccount]; STAmount const clawAmount = ctx.tx[sfAmount]; @@ -110,11 +157,59 @@ Clawback::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +template <> TER -Clawback::doApply() +preclaimHelper(PreclaimContext const& ctx) +{ + AccountID const issuer = ctx.tx[sfAccount]; + auto const clawAmount = ctx.tx[sfAmount]; + AccountID const& holder = ctx.tx[sfMPTokenHolder]; + + auto const sleIssuer = ctx.view.read(keylet::account(issuer)); + auto const sleHolder = ctx.view.read(keylet::account(holder)); + if (!sleIssuer || !sleHolder) + return terNO_ACCOUNT; + + if (sleHolder->isFieldPresent(sfAMMID)) + return tecAMM_ACCOUNT; + + auto const issuanceKey = + keylet::mptIssuance(clawAmount.get().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + if (!((*sleIssuance)[sfFlags] & lsfMPTCanClawback)) + return tecNO_PERMISSION; + + if (sleIssuance->getAccountID(sfIssuer) != issuer) + return tecNO_PERMISSION; + + if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, holder))) + return tecOBJECT_NOT_FOUND; + + if (accountHolds( + ctx.view, + holder, + clawAmount.get(), + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + ctx.j) <= beast::zero) + return tecINSUFFICIENT_FUNDS; + + return tesSUCCESS; +} + +template +static TER +applyHelper(ApplyContext& ctx); + +template <> +TER +applyHelper(ApplyContext& ctx) { - AccountID const& issuer = account_; - STAmount clawAmount = ctx_.tx[sfAmount]; + AccountID const& issuer = ctx.tx[sfAccount]; + STAmount clawAmount = ctx.tx[sfAmount]; AccountID const holder = clawAmount.getIssuer(); // cannot be reference // Replace the `issuer` field with issuer's account @@ -124,20 +219,69 @@ Clawback::doApply() // Get the spendable balance. Must use `accountHolds`. STAmount const spendableAmount = accountHolds( - view(), + ctx.view(), holder, clawAmount.getCurrency(), clawAmount.getIssuer(), fhIGNORE_FREEZE, - j_); + ctx.journal); return rippleCredit( - view(), + ctx.view(), holder, issuer, std::min(spendableAmount, clawAmount), true, - j_); + ctx.journal); +} + +template <> +TER +applyHelper(ApplyContext& ctx) +{ + AccountID const& issuer = ctx.tx[sfAccount]; + auto clawAmount = ctx.tx[sfAmount]; + AccountID const holder = ctx.tx[sfMPTokenHolder]; + + // Get the spendable balance. Must use `accountHolds`. + STAmount const spendableAmount = accountHolds( + ctx.view(), + holder, + clawAmount.get(), + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + ctx.journal); + + return rippleMPTCredit( + ctx.view(), + holder, + issuer, + std::min(spendableAmount, clawAmount), + ctx.journal); +} + +NotTEC +Clawback::preflight(PreflightContext const& ctx) +{ + return std::visit( + [&](T const&) { return preflightHelper(ctx); }, + ctx.tx[sfAmount].asset().value()); +} + +TER +Clawback::preclaim(PreclaimContext const& ctx) +{ + return std::visit( + [&](T const&) { return preclaimHelper(ctx); }, + ctx.tx[sfAmount].asset().value()); +} + +TER +Clawback::doApply() +{ + return std::visit( + [&](T const&) { return applyHelper(ctx_); }, + ctx_.tx[sfAmount].asset().value()); } } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index f855ad8578c..625f8c7b284 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -478,6 +478,8 @@ LedgerEntryTypesMatch::visitEntry( case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: case ltDID: case ltORACLE: + case ltMPTOKEN_ISSUANCE: + case ltMPTOKEN: break; default: invalidTypeAdded_ = true; @@ -882,6 +884,9 @@ ValidClawback::visitEntry( { if (before && before->getType() == ltRIPPLE_STATE) trustlinesChanged++; + + if (before && before->getType() == ltMPTOKEN) + mptokensChanged++; } bool @@ -904,18 +909,28 @@ ValidClawback::finalize( return false; } - AccountID const issuer = tx.getAccountID(sfAccount); - STAmount const amount = tx.getFieldAmount(sfAmount); - AccountID const& holder = amount.getIssuer(); - STAmount const holderBalance = accountHolds( - view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j); - - if (holderBalance.signum() < 0) + if (mptokensChanged > 1) { JLOG(j.fatal()) - << "Invariant failed: trustline balance is negative"; + << "Invariant failed: more than one mptokens changed."; return false; } + + if (trustlinesChanged == 1) + { + AccountID const issuer = tx.getAccountID(sfAccount); + STAmount const& amount = tx.getFieldAmount(sfAmount); + AccountID const& holder = amount.getIssuer(); + STAmount const holderBalance = accountHolds( + view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j); + + if (holderBalance.signum() < 0) + { + JLOG(j.fatal()) + << "Invariant failed: trustline balance is negative"; + return false; + } + } } else { @@ -925,9 +940,182 @@ ValidClawback::finalize( "despite failure of the transaction."; return false; } + + if (mptokensChanged != 0) + { + JLOG(j.fatal()) << "Invariant failed: some mptokens were changed " + "despite failure of the transaction."; + return false; + } } return true; } +//------------------------------------------------------------------------------ + +void +ValidMPTIssuance::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltMPTOKEN_ISSUANCE) + { + if (isDelete) + mptIssuancesDeleted_++; + else if (!before) + mptIssuancesCreated_++; + } + + if (after && after->getType() == ltMPTOKEN) + { + if (isDelete) + mptokensDeleted_++; + else if (!before) + mptokensCreated_++; + } +} + +bool +ValidMPTIssuance::finalize( + STTx const& tx, + TER const result, + XRPAmount const _fee, + ReadView const& _view, + beast::Journal const& j) +{ + if (result == tesSUCCESS) + { + if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_CREATE) + { + if (mptIssuancesCreated_ == 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance creation " + "succeeded without creating a MPT issuance"; + } + else if (mptIssuancesDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance creation " + "succeeded while removing MPT issuances"; + } + else if (mptIssuancesCreated_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance creation " + "succeeded but created multiple issuances"; + } + + return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0; + } + + if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_DESTROY) + { + if (mptIssuancesDeleted_ == 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " + "succeeded without removing a MPT issuance"; + } + else if (mptIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " + "succeeded while creating MPT issuances"; + } + else if (mptIssuancesDeleted_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " + "succeeded but deleted multiple issuances"; + } + + return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1; + } + + if (tx.getTxnType() == ttMPTOKEN_AUTHORIZE) + { + bool const submittedByIssuer = tx.isFieldPresent(sfMPTokenHolder); + + if (mptIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT authorize " + "succeeded but created MPT issuances"; + return false; + } + else if (mptIssuancesDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT authorize " + "succeeded but deleted issuances"; + return false; + } + else if ( + submittedByIssuer && + (mptokensCreated_ > 0 || mptokensDeleted_ > 0)) + { + JLOG(j.fatal()) + << "Invariant failed: MPT authorize submitted by issuer " + "succeeded but created/deleted mptokens"; + return false; + } + else if ( + !submittedByIssuer && + (mptokensCreated_ + mptokensDeleted_ != 1)) + { + // if the holder submitted this tx, then a mptoken must be + // either created or deleted. + JLOG(j.fatal()) + << "Invariant failed: MPT authorize submitted by holder " + "succeeded but created/deleted bad number of mptokens"; + return false; + } + + return true; + } + + if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_SET) + { + if (mptIssuancesDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while removing MPT issuances"; + } + else if (mptIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while creating MPT issuances"; + } + else if (mptokensDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while removing MPTokens"; + } + else if (mptokensCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance set " + "succeeded while creating MPTokens"; + } + + return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && + mptokensCreated_ == 0 && mptokensDeleted_ == 0; + } + } + + if (mptIssuancesCreated_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPT issuance was created"; + } + else if (mptIssuancesDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPT issuance was deleted"; + } + else if (mptokensCreated_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPToken was created"; + } + else if (mptokensDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPToken was deleted"; + } + + return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && + mptokensCreated_ == 0 && mptokensDeleted_ == 0; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 1b3234bae69..23ec8005556 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -433,6 +433,31 @@ class NFTokenCountTracking class ValidClawback { std::uint32_t trustlinesChanged = 0; + std::uint32_t mptokensChanged = 0; + +public: + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); +}; + +class ValidMPTIssuance +{ + std::uint32_t mptIssuancesCreated_ = 0; + std::uint32_t mptIssuancesDeleted_ = 0; + + std::uint32_t mptokensCreated_ = 0; + std::uint32_t mptokensDeleted_ = 0; public: void @@ -465,7 +490,8 @@ using InvariantChecks = std::tuple< ValidNewAccountRoot, ValidNFTokenPage, NFTokenCountTracking, - ValidClawback>; + ValidClawback, + ValidMPTIssuance>; /** * @brief get a tuple of all invariant checks diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp new file mode 100644 index 00000000000..1e9871e0ae3 --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -0,0 +1,251 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +MPTokenAuthorize::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfMPTokenAuthorizeMask) + return temINVALID_FLAG; + + if (ctx.tx[sfAccount] == ctx.tx[~sfMPTokenHolder]) + return temMALFORMED; + + return preflight2(ctx); +} + +TER +MPTokenAuthorize::preclaim(PreclaimContext const& ctx) +{ + auto const accountID = ctx.tx[sfAccount]; + auto const holderID = ctx.tx[~sfMPTokenHolder]; + + if (holderID && !(ctx.view.exists(keylet::account(*holderID)))) + return tecNO_DST; + + // if non-issuer account submits this tx, then they are trying either: + // 1. Unauthorize/delete MPToken + // 2. Use/create MPToken + // + // Note: `accountID` is holder's account + // `holderID` is NOT used + if (!holderID) + { + std::shared_ptr sleMpt = ctx.view.read( + keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], accountID)); + + // There is an edge case where holder deletes MPT after issuance has + // already been destroyed. So we must check for unauthorize before + // fetching the MPTIssuance object(since it doesn't exist) + + // if holder wants to delete/unauthorize a mpt + if (ctx.tx.getFlags() & tfMPTUnauthorize) + { + if (!sleMpt) + return tecOBJECT_NOT_FOUND; + + if ((*sleMpt)[sfMPTAmount] != 0) + return tecHAS_OBLIGATIONS; + + return tesSUCCESS; + } + + // Now test when the holder wants to hold/create/authorize a new MPT + auto const sleMptIssuance = + ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + + if (!sleMptIssuance) + return tecOBJECT_NOT_FOUND; + + if (accountID == (*sleMptIssuance)[sfIssuer]) + return tecNO_PERMISSION; + + // if holder wants to use and create a mpt + if (sleMpt) + return tecMPTOKEN_EXISTS; + + return tesSUCCESS; + } + + auto const sleMptIssuance = + ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + if (!sleMptIssuance) + return tecOBJECT_NOT_FOUND; + + std::uint32_t const mptIssuanceFlags = sleMptIssuance->getFieldU32(sfFlags); + + // If tx is submitted by issuer, they would either try to do the following + // for allowlisting: + // 1. authorize an account + // 2. unauthorize an account + // + // Note: `accountID` is issuer's account + // `holderID` is holder's account + if (accountID != (*sleMptIssuance)[sfIssuer]) + return tecNO_PERMISSION; + + // If tx is submitted by issuer, it only applies for MPT with + // lsfMPTRequireAuth set + if (!(mptIssuanceFlags & lsfMPTRequireAuth)) + return tecNO_AUTH; + + if (!ctx.view.exists( + keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], *holderID))) + return tecOBJECT_NOT_FOUND; + + return tesSUCCESS; +} + +TER +MPTokenAuthorize::authorize( + ApplyView& view, + beast::Journal journal, + MPTAuthorizeArgs const& args) +{ + auto const sleAcct = view.peek(keylet::account(args.account)); + if (!sleAcct) + return tecINTERNAL; + + // If the account that submitted the tx is a holder + // Note: `account_` is holder's account + // `holderID` is NOT used + if (!args.holderID) + { + // When a holder wants to unauthorize/delete a MPT, the ledger must + // - delete mptokenKey from owner directory + // - delete the MPToken + if (args.flags & tfMPTUnauthorize) + { + auto const mptokenKey = + keylet::mptoken(args.mptIssuanceID, args.account); + auto const sleMpt = view.peek(mptokenKey); + if (!sleMpt) + return tecINTERNAL; + + if (!view.dirRemove( + keylet::ownerDir(args.account), + (*sleMpt)[sfOwnerNode], + sleMpt->key(), + false)) + return tecINTERNAL; + + adjustOwnerCount(view, sleAcct, -1, journal); + + view.erase(sleMpt); + return tesSUCCESS; + } + + // A potential holder wants to authorize/hold a mpt, the ledger must: + // - add the new mptokenKey to the owner directory + // - create the MPToken object for the holder + std::uint32_t const uOwnerCount = sleAcct->getFieldU32(sfOwnerCount); + XRPAmount const reserveCreate( + (uOwnerCount < 2) ? XRPAmount(beast::zero) + : view.fees().accountReserve(uOwnerCount + 1)); + + if (args.priorBalance < reserveCreate) + return tecINSUFFICIENT_RESERVE; + + auto const mptokenKey = + keylet::mptoken(args.mptIssuanceID, args.account); + + auto const ownerNode = view.dirInsert( + keylet::ownerDir(args.account), + mptokenKey, + describeOwnerDir(args.account)); + + if (!ownerNode) + return tecDIR_FULL; + + auto mptoken = std::make_shared(mptokenKey); + (*mptoken)[sfAccount] = args.account; + (*mptoken)[sfMPTokenIssuanceID] = args.mptIssuanceID; + (*mptoken)[sfFlags] = 0; + (*mptoken)[sfOwnerNode] = *ownerNode; + view.insert(mptoken); + + // Update owner count. + adjustOwnerCount(view, sleAcct, 1, journal); + + return tesSUCCESS; + } + + auto const sleMptIssuance = + view.read(keylet::mptIssuance(args.mptIssuanceID)); + if (!sleMptIssuance) + return tecINTERNAL; + + // If the account that submitted this tx is the issuer of the MPT + // Note: `account_` is issuer's account + // `holderID` is holder's account + if (args.account != (*sleMptIssuance)[sfIssuer]) + return tecINTERNAL; + + auto const sleMpt = + view.peek(keylet::mptoken(args.mptIssuanceID, *args.holderID)); + if (!sleMpt) + return tecINTERNAL; + + std::uint32_t const flagsIn = sleMpt->getFieldU32(sfFlags); + std::uint32_t flagsOut = flagsIn; + + // Issuer wants to unauthorize the holder, unset lsfMPTAuthorized on + // their MPToken + if (args.flags & tfMPTUnauthorize) + flagsOut &= ~lsfMPTAuthorized; + // Issuer wants to authorize a holder, set lsfMPTAuthorized on their + // MPToken + else + flagsOut |= lsfMPTAuthorized; + + if (flagsIn != flagsOut) + sleMpt->setFieldU32(sfFlags, flagsOut); + + view.update(sleMpt); + return tesSUCCESS; +} + +TER +MPTokenAuthorize::doApply() +{ + auto const& tx = ctx_.tx; + return authorize( + ctx_.view(), + ctx_.journal, + {.priorBalance = mPriorBalance, + .mptIssuanceID = tx[sfMPTokenIssuanceID], + .account = account_, + .flags = tx.getFlags(), + .holderID = tx[~sfMPTokenHolder]}); +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h new file mode 100644 index 00000000000..94451a61c88 --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_MPTOKENAUTHORIZE_H_INCLUDED +#define RIPPLE_TX_MPTOKENAUTHORIZE_H_INCLUDED + +#include + +namespace ripple { + +struct MPTAuthorizeArgs +{ + XRPAmount const& priorBalance; + uint192 const& mptIssuanceID; + AccountID const& account; + std::uint32_t flags; + std::optional holderID; +}; + +class MPTokenAuthorize : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit MPTokenAuthorize(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + static TER + authorize( + ApplyView& view, + beast::Journal journal, + MPTAuthorizeArgs const& args); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp new file mode 100644 index 00000000000..de01341e296 --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -0,0 +1,142 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfMPTokenIssuanceCreateMask) + return temINVALID_FLAG; + + if (auto const fee = ctx.tx[~sfTransferFee]) + { + if (fee > maxTransferFee) + return temBAD_MPTOKEN_TRANSFER_FEE; + + // If a non-zero TransferFee is set then the tfTransferable flag + // must also be set. + if (fee > 0u && !ctx.tx.isFlag(tfMPTCanTransfer)) + return temMALFORMED; + } + + if (auto const metadata = ctx.tx[~sfMPTokenMetadata]) + { + if (metadata->length() == 0 || + metadata->length() > maxMPTokenMetadataLength) + return temMALFORMED; + } + + // Check if maximumAmount is within 63 bit range + if (auto const maxAmt = ctx.tx[~sfMaximumAmount]) + { + if (maxAmt == 0) + return temMALFORMED; + + if (maxAmt > maxMPTokenAmount) + return temMALFORMED; + } + return preflight2(ctx); +} + +TER +MPTokenIssuanceCreate::create( + ApplyView& view, + beast::Journal journal, + MPTCreateArgs const& args) +{ + auto const acct = view.peek(keylet::account(args.account)); + if (!acct) + return tecINTERNAL; + + if (args.priorBalance < + view.fees().accountReserve((*acct)[sfOwnerCount] + 1)) + return tecINSUFFICIENT_RESERVE; + + auto const mptIssuanceKeylet = + keylet::mptIssuance(args.account, args.sequence); + + // create the MPTokenIssuance + { + auto const ownerNode = view.dirInsert( + keylet::ownerDir(args.account), + mptIssuanceKeylet, + describeOwnerDir(args.account)); + + if (!ownerNode) + return tecDIR_FULL; + + auto mptIssuance = std::make_shared(mptIssuanceKeylet); + (*mptIssuance)[sfFlags] = args.flags & ~tfUniversal; + (*mptIssuance)[sfIssuer] = args.account; + (*mptIssuance)[sfOutstandingAmount] = 0; + (*mptIssuance)[sfOwnerNode] = *ownerNode; + (*mptIssuance)[sfSequence] = args.sequence; + + if (args.maxAmount) + (*mptIssuance)[sfMaximumAmount] = *args.maxAmount; + + if (args.assetScale) + (*mptIssuance)[sfAssetScale] = *args.assetScale; + + if (args.transferFee) + (*mptIssuance)[sfTransferFee] = *args.transferFee; + + if (args.metadata) + (*mptIssuance)[sfMPTokenMetadata] = *args.metadata; + + view.insert(mptIssuance); + } + + // Update owner count. + adjustOwnerCount(view, acct, 1, journal); + + return tesSUCCESS; +} + +TER +MPTokenIssuanceCreate::doApply() +{ + auto const& tx = ctx_.tx; + return create( + ctx_.view(), + ctx_.journal, + {.priorBalance = mPriorBalance, + .account = account_, + .sequence = tx.getSeqProxy().value(), + .flags = tx.getFlags(), + .maxAmount = tx[~sfMaximumAmount], + .assetScale = tx[~sfAssetScale], + .transferFee = tx[~sfTransferFee], + .metadata = tx[~sfMPTokenMetadata]}); +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h new file mode 100644 index 00000000000..2fdf2bdd152 --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h @@ -0,0 +1,60 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_MPTOKENISSUANCECREATE_H_INCLUDED +#define RIPPLE_TX_MPTOKENISSUANCECREATE_H_INCLUDED + +#include + +namespace ripple { + +struct MPTCreateArgs +{ + XRPAmount const& priorBalance; + AccountID const& account; + std::uint32_t sequence; + std::uint32_t flags; + std::optional maxAmount; + std::optional assetScale; + std::optional transferFee; + std::optional const& metadata; +}; + +class MPTokenIssuanceCreate : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit MPTokenIssuanceCreate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + TER + doApply() override; + + static TER + create(ApplyView& view, beast::Journal journal, MPTCreateArgs const& args); +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp new file mode 100644 index 00000000000..4eb6225c0b4 --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +MPTokenIssuanceDestroy::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + // check flags + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfMPTokenIssuanceDestroyMask) + return temINVALID_FLAG; + + return preflight2(ctx); +} + +TER +MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx) +{ + // ensure that issuance exists + auto const sleMPT = + ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + if (!sleMPT) + return tecOBJECT_NOT_FOUND; + + // ensure it is issued by the tx submitter + if ((*sleMPT)[sfIssuer] != ctx.tx[sfAccount]) + return tecNO_PERMISSION; + + // ensure it has no outstanding balances + if ((*sleMPT)[~sfOutstandingAmount] != 0) + return tecHAS_OBLIGATIONS; + + return tesSUCCESS; +} + +TER +MPTokenIssuanceDestroy::doApply() +{ + auto const mpt = + view().peek(keylet::mptIssuance(ctx_.tx[sfMPTokenIssuanceID])); + auto const issuer = (*mpt)[sfIssuer]; + + if (!view().dirRemove( + keylet::ownerDir(issuer), (*mpt)[sfOwnerNode], mpt->key(), false)) + return tefBAD_LEDGER; + + view().erase(mpt); + + adjustOwnerCount(view(), view().peek(keylet::account(issuer)), -1, j_); + + return tesSUCCESS; +} + +} // namespace ripple \ No newline at end of file diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h new file mode 100644 index 00000000000..3e9f9b7e5cf --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_MPTOKENISSUANCEDESTROY_H_INCLUDED +#define RIPPLE_TX_MPTOKENISSUANCEDESTROY_H_INCLUDED + +#include + +namespace ripple { + +class MPTokenIssuanceDestroy : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit MPTokenIssuanceDestroy(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif \ No newline at end of file diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp new file mode 100644 index 00000000000..2b4ff2bcb8a --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -0,0 +1,118 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +MPTokenIssuanceSet::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const txFlags = ctx.tx.getFlags(); + + // check flags + if (txFlags & tfMPTokenIssuanceSetMask) + return temINVALID_FLAG; + // fails if both flags are set + else if ((txFlags & tfMPTLock) && (txFlags & tfMPTUnlock)) + return temINVALID_FLAG; + + auto const accountID = ctx.tx[sfAccount]; + auto const holderID = ctx.tx[~sfMPTokenHolder]; + if (holderID && accountID == holderID) + return temMALFORMED; + + return preflight2(ctx); +} + +TER +MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) +{ + // ensure that issuance exists + auto const sleMptIssuance = + ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + if (!sleMptIssuance) + return tecOBJECT_NOT_FOUND; + + // if the mpt has disabled locking + if (!((*sleMptIssuance)[sfFlags] & lsfMPTCanLock)) + return tecNO_PERMISSION; + + // ensure it is issued by the tx submitter + if ((*sleMptIssuance)[sfIssuer] != ctx.tx[sfAccount]) + return tecNO_PERMISSION; + + if (auto const holderID = ctx.tx[~sfMPTokenHolder]) + { + // make sure holder account exists + if (!ctx.view.exists(keylet::account(*holderID))) + return tecNO_DST; + + // the mptoken must exist + if (!ctx.view.exists( + keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], *holderID))) + return tecOBJECT_NOT_FOUND; + } + + return tesSUCCESS; +} + +TER +MPTokenIssuanceSet::doApply() +{ + auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; + auto const txFlags = ctx_.tx.getFlags(); + auto const holderID = ctx_.tx[~sfMPTokenHolder]; + std::shared_ptr sle; + + if (holderID) + sle = view().peek(keylet::mptoken(mptIssuanceID, *holderID)); + else + sle = view().peek(keylet::mptIssuance(mptIssuanceID)); + + if (!sle) + return tecINTERNAL; + + std::uint32_t const flagsIn = sle->getFieldU32(sfFlags); + std::uint32_t flagsOut = flagsIn; + + if (txFlags & tfMPTLock) + flagsOut |= lsfMPTLocked; + else if (txFlags & tfMPTUnlock) + flagsOut &= ~lsfMPTLocked; + + if (flagsIn != flagsOut) + sle->setFieldU32(sfFlags, flagsOut); + + view().update(sle); + + return tesSUCCESS; +} + +} // namespace ripple \ No newline at end of file diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h new file mode 100644 index 00000000000..36080d46667 --- /dev/null +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_MPTOKENISSUANCESET_H_INCLUDED +#define RIPPLE_TX_MPTOKENISSUANCESET_H_INCLUDED + +#include + +namespace ripple { + +class MPTokenIssuanceSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit MPTokenIssuanceSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 309e9d4a498..6f68789357e 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -43,8 +44,13 @@ Payment::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)}; } +template +static NotTEC +preflightHelper(PreflightContext const& ctx); + +template <> NotTEC -Payment::preflight(PreflightContext const& ctx) +preflightHelper(PreflightContext const& ctx) { if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -78,7 +84,7 @@ Payment::preflight(PreflightContext const& ctx) maxSourceAmount = saDstAmount; else maxSourceAmount = STAmount( - {saDstAmount.getCurrency(), account}, + Issue{saDstAmount.getCurrency(), account}, saDstAmount.mantissa(), saDstAmount.exponent(), saDstAmount < beast::zero); @@ -203,8 +209,83 @@ Payment::preflight(PreflightContext const& ctx) return preflight2(ctx); } +template <> +NotTEC +preflightHelper(PreflightContext const& ctx) +{ + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + if (ctx.tx.isFieldPresent(sfDeliverMin) || + ctx.tx.isFieldPresent(sfSendMax) || ctx.tx.isFieldPresent(sfPaths)) + return temMALFORMED; + + auto& tx = ctx.tx; + auto& j = ctx.j; + + std::uint32_t const uTxFlags = tx.getFlags(); + + if (uTxFlags & tfPaymentMask) + { + JLOG(j.trace()) << "Malformed transaction: " + << "Invalid flags set."; + return temINVALID_FLAG; + } + + STAmount const saDstAmount(tx.getFieldAmount(sfAmount)); + + auto const account = tx.getAccountID(sfAccount); + + auto const& uDstMptID = saDstAmount.get().getMptID(); + + auto const uDstAccountID = tx.getAccountID(sfDestination); + + if (!uDstAccountID) + { + JLOG(j.trace()) << "Malformed transaction: " + << "Payment destination account not specified."; + return temDST_NEEDED; + } + if (saDstAmount <= beast::zero) + { + JLOG(j.trace()) << "Malformed transaction: " + << "bad dst amount: " << saDstAmount.getFullText(); + return temBAD_AMOUNT; + } + if (account == uDstAccountID) + { + // You're signing yourself a payment. + JLOG(j.trace()) << "Malformed transaction: " + << "Redundant payment from " << to_string(account) + << " to self without path for " << to_string(uDstMptID); + return temREDUNDANT; + } + if (uTxFlags & (tfPartialPayment | tfLimitQuality | tfNoRippleDirect)) + { + JLOG(j.trace()) << "Malformed transaction: invalid MPT flags: " + << uTxFlags; + return temMALFORMED; + } + + return preflight2(ctx); +} + +template +static TER +preclaimHelper( + PreclaimContext const& ctx, + std::size_t maxPathSize, + std::size_t maxPathLength); + +template <> TER -Payment::preclaim(PreclaimContext const& ctx) +preclaimHelper( + PreclaimContext const& ctx, + std::size_t maxPathSize, + std::size_t maxPathLength) { // Ripple if source or destination is non-native or if there are paths. std::uint32_t const uTxFlags = ctx.tx.getFlags(); @@ -275,9 +356,9 @@ Payment::preclaim(PreclaimContext const& ctx) { STPathSet const& paths = ctx.tx.getFieldPathSet(sfPaths); - if (paths.size() > MaxPathSize || - std::any_of(paths.begin(), paths.end(), [](STPath const& path) { - return path.size() > MaxPathLength; + if (paths.size() > maxPathSize || + std::any_of(paths.begin(), paths.end(), [&](STPath const& path) { + return path.size() > maxPathLength; })) { return telBAD_PATH_COUNT; @@ -287,21 +368,69 @@ Payment::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +template <> TER -Payment::doApply() +preclaimHelper(PreclaimContext const& ctx, std::size_t, std::size_t) +{ + AccountID const uDstAccountID(ctx.tx[sfDestination]); + + auto const k = keylet::account(uDstAccountID); + auto const sleDst = ctx.view.read(k); + + if (!sleDst) + { + JLOG(ctx.j.trace()) + << "Delay transaction: Destination account does not exist."; + + // Another transaction could create the account and then this + // transaction would succeed. + return tecNO_DST; + } + else if ( + (sleDst->getFlags() & lsfRequireDestTag) && + !ctx.tx.isFieldPresent(sfDestinationTag)) + { + // The tag is basically account-specific information we don't + // understand, but we can require someone to fill it in. + + // We didn't make this test for a newly-formed account because there's + // no way for this field to be set. + JLOG(ctx.j.trace()) + << "Malformed transaction: DestinationTag required."; + + return tecDST_TAG_NEEDED; + } + + return tesSUCCESS; +} + +template +static TER +applyHelper( + ApplyContext& ctx, + XRPAmount const& priorBalance, + XRPAmount const& sourceBalance); + +template <> +TER +applyHelper( + ApplyContext& ctx, + XRPAmount const& priorBalance, + XRPAmount const& sourceBalance) { - auto const deliverMin = ctx_.tx[~sfDeliverMin]; + AccountID const account = ctx.tx[sfAccount]; + auto const deliverMin = ctx.tx[~sfDeliverMin]; // Ripple if source or destination is non-native or if there are paths. - std::uint32_t const uTxFlags = ctx_.tx.getFlags(); + std::uint32_t const uTxFlags = ctx.tx.getFlags(); bool const partialPaymentAllowed = uTxFlags & tfPartialPayment; bool const limitQuality = uTxFlags & tfLimitQuality; bool const defaultPathsAllowed = !(uTxFlags & tfNoRippleDirect); - auto const paths = ctx_.tx.isFieldPresent(sfPaths); - auto const sendMax = ctx_.tx[~sfSendMax]; + auto const paths = ctx.tx.isFieldPresent(sfPaths); + auto const sendMax = ctx.tx[~sfSendMax]; - AccountID const uDstAccountID(ctx_.tx.getAccountID(sfDestination)); - STAmount const saDstAmount(ctx_.tx.getFieldAmount(sfAmount)); + AccountID const uDstAccountID(ctx.tx.getAccountID(sfDestination)); + STAmount const saDstAmount(ctx.tx.getFieldAmount(sfAmount)); STAmount maxSourceAmount; if (sendMax) maxSourceAmount = *sendMax; @@ -309,44 +438,47 @@ Payment::doApply() maxSourceAmount = saDstAmount; else maxSourceAmount = STAmount( - {saDstAmount.getCurrency(), account_}, + Issue{saDstAmount.getCurrency(), account}, saDstAmount.mantissa(), saDstAmount.exponent(), saDstAmount < beast::zero); - JLOG(j_.trace()) << "maxSourceAmount=" << maxSourceAmount.getFullText() - << " saDstAmount=" << saDstAmount.getFullText(); + JLOG(ctx.journal.trace()) + << "maxSourceAmount=" << maxSourceAmount.getFullText() + << " saDstAmount=" << saDstAmount.getFullText(); // Open a ledger for editing. auto const k = keylet::account(uDstAccountID); - SLE::pointer sleDst = view().peek(k); + SLE::pointer sleDst = ctx.view().peek(k); if (!sleDst) { std::uint32_t const seqno{ - view().rules().enabled(featureDeletableAccounts) ? view().seq() - : 1}; + ctx.view().rules().enabled(featureDeletableAccounts) + ? ctx.view().seq() + : 1}; // Create the account. sleDst = std::make_shared(k); sleDst->setAccountID(sfAccount, uDstAccountID); sleDst->setFieldU32(sfSequence, seqno); - view().insert(sleDst); + ctx.view().insert(sleDst); } else { // Tell the engine that we are intending to change the destination // account. The source account gets always charged a fee so it's always // marked as modified. - view().update(sleDst); + ctx.view().update(sleDst); } // Determine whether the destination requires deposit authorization. bool const reqDepositAuth = sleDst->getFlags() & lsfDepositAuth && - view().rules().enabled(featureDepositAuth); + ctx.view().rules().enabled(featureDepositAuth); - bool const depositPreauth = view().rules().enabled(featureDepositPreauth); + bool const depositPreauth = + ctx.view().rules().enabled(featureDepositPreauth); bool const bRipple = paths || sendMax || !saDstAmount.native(); @@ -366,10 +498,10 @@ Payment::doApply() // authorization has two ways to get an IOU Payment in: // 1. If Account == Destination, or // 2. If Account is deposit preauthorized by destination. - if (uDstAccountID != account_) + if (uDstAccountID != account) { - if (!view().exists( - keylet::depositPreauth(uDstAccountID, account_))) + if (!ctx.view().exists( + keylet::depositPreauth(uDstAccountID, account))) return tecNO_PERMISSION; } } @@ -378,26 +510,26 @@ Payment::doApply() rcInput.partialPaymentAllowed = partialPaymentAllowed; rcInput.defaultPathsAllowed = defaultPathsAllowed; rcInput.limitQuality = limitQuality; - rcInput.isLedgerOpen = view().open(); + rcInput.isLedgerOpen = ctx.view().open(); path::RippleCalc::Output rc; { - PaymentSandbox pv(&view()); - JLOG(j_.debug()) << "Entering RippleCalc in payment: " - << ctx_.tx.getTransactionID(); + PaymentSandbox pv(&ctx.view()); + JLOG(ctx.journal.debug()) << "Entering RippleCalc in payment: " + << ctx.tx.getTransactionID(); rc = path::RippleCalc::rippleCalculate( pv, maxSourceAmount, saDstAmount, uDstAccountID, - account_, - ctx_.tx.getFieldPathSet(sfPaths), - ctx_.app.logs(), + account, + ctx.tx.getFieldPathSet(sfPaths), + ctx.app.logs(), &rcInput); // VFALCO NOTE We might not need to apply, depending // on the TER. But always applying *should* // be safe. - pv.apply(ctx_.rawView()); + pv.apply(ctx.rawView()); } // TODO: is this right? If the amount is the correct amount, was @@ -407,7 +539,7 @@ Payment::doApply() if (deliverMin && rc.actualAmountOut < *deliverMin) rc.setResult(tecPATH_PARTIAL); else - ctx_.deliver(rc.actualAmountOut); + ctx.deliver(rc.actualAmountOut); } auto terResult = rc.result(); @@ -425,7 +557,7 @@ Payment::doApply() // Direct XRP payment. - auto const sleSrc = view().peek(keylet::account(account_)); + auto const sleSrc = ctx.view().peek(keylet::account(account)); if (!sleSrc) return tefINTERNAL; @@ -434,21 +566,21 @@ Payment::doApply() auto const uOwnerCount = sleSrc->getFieldU32(sfOwnerCount); // This is the total reserve in drops. - auto const reserve = view().fees().accountReserve(uOwnerCount); + auto const reserve = ctx.view().fees().accountReserve(uOwnerCount); // mPriorBalance is the balance on the sending account BEFORE the // fees were charged. We want to make sure we have enough reserve // to send. Allow final spend to use reserve for fee. - auto const mmm = std::max(reserve, ctx_.tx.getFieldAmount(sfFee).xrp()); + auto const mmm = std::max(reserve, ctx.tx.getFieldAmount(sfFee).xrp()); - if (mPriorBalance < saDstAmount.xrp() + mmm) + if (priorBalance < saDstAmount.xrp() + mmm) { // Vote no. However the transaction might succeed, if applied in // a different order. - JLOG(j_.trace()) << "Delay transaction: Insufficient funds: " - << " " << to_string(mPriorBalance) << " / " - << to_string(saDstAmount.xrp() + mmm) << " (" - << to_string(reserve) << ")"; + JLOG(ctx.journal.trace()) << "Delay transaction: Insufficient funds: " + << " " << to_string(priorBalance) << " / " + << to_string(saDstAmount.xrp() + mmm) << " (" + << to_string(reserve) << ")"; return tecUNFUNDED_PAYMENT; } @@ -480,12 +612,13 @@ Payment::doApply() // We choose the base reserve as our bound because it is // a small number that seldom changes but is always sufficient // to get the account un-wedged. - if (uDstAccountID != account_) + if (uDstAccountID != account) { - if (!view().exists(keylet::depositPreauth(uDstAccountID, account_))) + if (!ctx.view().exists( + keylet::depositPreauth(uDstAccountID, account))) { // Get the base reserve. - XRPAmount const dstReserve{view().fees().accountReserve(0)}; + XRPAmount const dstReserve{ctx.view().fees().accountReserve(0)}; if (saDstAmount > dstReserve || sleDst->getFieldAmount(sfBalance) > dstReserve) @@ -495,7 +628,7 @@ Payment::doApply() } // Do the arithmetic for the transfer and make the ledger change. - sleSrc->setFieldAmount(sfBalance, mSourceBalance - saDstAmount); + sleSrc->setFieldAmount(sfBalance, sourceBalance - saDstAmount); sleDst->setFieldAmount( sfBalance, sleDst->getFieldAmount(sfBalance) + saDstAmount); @@ -506,4 +639,78 @@ Payment::doApply() return tesSUCCESS; } +template <> +TER +applyHelper(ApplyContext& ctx, XRPAmount const&, XRPAmount const&) +{ + auto const account = ctx.tx[sfAccount]; + + AccountID const uDstAccountID(ctx.tx.getAccountID(sfDestination)); + auto const saDstAmount(ctx.tx.getFieldAmount(sfAmount)); + + JLOG(ctx.journal.trace()) << " saDstAmount=" << saDstAmount.getFullText(); + + if (auto const ter = + requireAuth(ctx.view(), saDstAmount.get(), account); + ter != tesSUCCESS) + return ter; + + if (auto const ter = + requireAuth(ctx.view(), saDstAmount.get(), uDstAccountID); + ter != tesSUCCESS) + return ter; + + if (auto const ter = canTransfer( + ctx.view(), saDstAmount.get(), account, uDstAccountID); + ter != tesSUCCESS) + return ter; + + auto const& mpt = saDstAmount.get(); + auto const& issuer = mpt.getIssuer(); + // If globally/individually locked then + // - can't send between holders + // - holder can send back to issuer + // - issuer can send to holder + if (account != issuer && uDstAccountID != issuer && + (isFrozen(ctx.view(), account, mpt) || + isFrozen(ctx.view(), uDstAccountID, mpt))) + return tecMPT_LOCKED; + + PaymentSandbox pv(&ctx.view()); + auto const res = + accountSendMPT(pv, account, uDstAccountID, saDstAmount, ctx.journal); + pv.apply(ctx.rawView()); + return res; +} + +NotTEC +Payment::preflight(PreflightContext const& ctx) +{ + return std::visit( + [&](TDelIss const&) { + return preflightHelper(ctx); + }, + ctx.tx[sfAmount].asset().value()); +} + +TER +Payment::preclaim(PreclaimContext const& ctx) +{ + return std::visit( + [&](TDelIss const&) { + return preclaimHelper(ctx, MaxPathSize, MaxPathLength); + }, + ctx.tx[sfAmount].asset().value()); +} + +TER +Payment::doApply() +{ + return std::visit( + [&](TDelIss const&) { + return applyHelper(ctx_, mPriorBalance, mSourceBalance); + }, + ctx_.tx[sfAmount].asset().value()); +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index 3a7fe9cca0d..954fc6543f1 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -537,7 +537,7 @@ SetTrust::doApply() else { // Zero balance in currency. - STAmount saBalance({currency, noAccount()}); + STAmount saBalance(Issue{currency, noAccount()}); auto const k = keylet::line(account_, uDstAccountID, currency); diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index cbeabb6fc9c..e33d673e022 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -39,6 +39,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -168,6 +172,14 @@ with_txn_type(TxType txnType, F&& f) return f.template operator()(); case ttORACLE_DELETE: return f.template operator()(); + case ttMPTOKEN_ISSUANCE_CREATE: + return f.template operator()(); + case ttMPTOKEN_ISSUANCE_DESTROY: + return f.template operator()(); + case ttMPTOKEN_AUTHORIZE: + return f.template operator()(); + case ttMPTOKEN_ISSUANCE_SET: + return f.template operator()(); default: throw UnknownTxnType(txnType); } diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 09f374d2c29..44125d7f84f 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -78,9 +79,15 @@ hasExpired(ReadView const& view, std::optional const& exp); /** Controls the treatment of frozen account balances */ enum FreezeHandling { fhIGNORE_FREEZE, fhZERO_IF_FROZEN }; +/** Controls the treatment of unauthorized MPT balances */ +enum AuthHandling { ahIGNORE_AUTH, ahZERO_IF_UNAUTHORIZED }; + [[nodiscard]] bool isGlobalFrozen(ReadView const& view, AccountID const& issuer); +[[nodiscard]] bool +isGlobalFrozen(ReadView const& view, MPTIssue const& mpt); + [[nodiscard]] bool isIndividualFrozen( ReadView const& view, @@ -97,6 +104,12 @@ isIndividualFrozen( return isIndividualFrozen(view, account, issue.currency, issue.account); } +[[nodiscard]] inline bool +isIndividualFrozen( + ReadView const& view, + AccountID const& account, + MPTIssue const& mpt); + [[nodiscard]] bool isFrozen( ReadView const& view, @@ -110,6 +123,9 @@ isFrozen(ReadView const& view, AccountID const& account, Issue const& issue) return isFrozen(view, account, issue.currency, issue.account); } +[[nodiscard]] bool +isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mpt); + // Returns the amount an account can spend without going into debt. // // <-- saAmount: amount of currency held by account. May be negative. @@ -130,6 +146,15 @@ accountHolds( FreezeHandling zeroIfFrozen, beast::Journal j); +[[nodiscard]] STAmount +accountHolds( + ReadView const& view, + AccountID const& account, + MPTIssue const& issue, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j); + // Returns the amount an account can spend of the currency type saDefault, or // returns saDefault if this account is the issuer of the currency in // question. Should be used in favor of accountHolds when questioning how much @@ -209,6 +234,9 @@ forEachItemAfter( [[nodiscard]] Rate transferRate(ReadView const& view, AccountID const& issuer); +[[nodiscard]] Rate +transferRate(ReadView const& view, MPTID const& id); + /** Returns `true` if the directory is empty @param key The key of the directory */ @@ -419,6 +447,14 @@ rippleCredit( bool bCheckIssuer, beast::Journal j); +[[nodiscard]] TER +rippleMPTCredit( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STAmount saAmount, + beast::Journal j); + [[nodiscard]] TER accountSend( ApplyView& view, @@ -428,6 +464,15 @@ accountSend( beast::Journal j, WaiveTransferFee waiveFee = WaiveTransferFee::No); +[[nodiscard]] TER +accountSendMPT( + ApplyView& view, + AccountID const& from, + AccountID const& to, + const STAmount& saAmount, + beast::Journal j, + WaiveTransferFee waiveFee = WaiveTransferFee::No); + [[nodiscard]] TER issueIOU( ApplyView& view, @@ -458,6 +503,22 @@ transferXRP( */ [[nodiscard]] TER requireAuth(ReadView const& view, Issue const& issue, AccountID const& account); +[[nodiscard]] TER +requireAuth( + ReadView const& view, + MPTIssue const& mpt, + AccountID const& account); + +/** Check if the destination account is allowed + * to receive MPT. Return tecNO_AUTH if it doesn't + * and tesSUCCESS otherwise. + */ +[[nodiscard]] TER +canTransfer( + ReadView const& view, + MPTIssue const& mpt, + AccountID const& from, + AccountID const& to); /** Deleter function prototype. Returns the status of the entry deletion * (if should not be skipped) and if the entry should be skipped. The status diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 13ac07e5e74..e1d85d73422 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -177,6 +177,14 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer) return false; } +bool +isGlobalFrozen(ReadView const& view, MPTIssue const& mpt) +{ + if (auto const sle = view.read(keylet::mptIssuance(mpt.getMptID()))) + return sle->getFlags() & lsfMPTLocked; + return false; +} + bool isIndividualFrozen( ReadView const& view, @@ -197,6 +205,17 @@ isIndividualFrozen( return false; } +bool +isIndividualFrozen( + ReadView const& view, + AccountID const& account, + MPTIssue const& mpt) +{ + if (auto const sle = view.read(keylet::mptoken(mpt.getMptID(), account))) + return sle->getFlags() & lsfMPTLocked; + return false; +} + // Can the specified account spend the specified currency issued by // the specified issuer or does the freeze flag prohibit it? bool @@ -222,6 +241,14 @@ isFrozen( return false; } +bool +isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mpt) +{ + if (isGlobalFrozen(view, mpt)) + return true; + return isIndividualFrozen(view, account, mpt); +} + STAmount accountHolds( ReadView const& view, @@ -241,13 +268,13 @@ accountHolds( auto const sle = view.read(keylet::line(account, issuer, currency)); if (!sle) { - amount.clear({currency, issuer}); + amount.clear(Issue{currency, issuer}); } else if ( (zeroIfFrozen == fhZERO_IF_FROZEN) && isFrozen(view, account, currency, issuer)) { - amount.clear(Issue(currency, issuer)); + amount.clear(Issue{currency, issuer}); } else { @@ -278,6 +305,47 @@ accountHolds( view, account, issue.currency, issue.account, zeroIfFrozen, j); } +STAmount +accountHolds( + ReadView const& view, + AccountID const& account, + MPTIssue const& issue, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j) +{ + STAmount amount; + + auto const sleMpt = view.read(keylet::mptoken(issue.getMptID(), account)); + if (!sleMpt) + amount.clear(issue); + else if (zeroIfFrozen == fhZERO_IF_FROZEN && isFrozen(view, account, issue)) + amount.clear(issue); + else + { + auto const amt = sleMpt->getFieldU64(sfMPTAmount); + auto const locked = sleMpt->getFieldU64(sfLockedAmount); + if (amt > locked) + amount = STAmount{issue, amt - locked}; + + // only if auth check is needed, as it needs to do an additional read + // operation + if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED) + { + auto const sleIssuance = + view.read(keylet::mptIssuance(issue.getMptID())); + + // if auth is enabled on the issuance and mpt is not authorized, + // clear amount + if (sleIssuance && sleIssuance->isFlag(lsfMPTRequireAuth) && + !sleMpt->isFlag(lsfMPTAuthorized)) + amount.clear(issue); + } + } + + return amount; +} + STAmount accountFunds( ReadView const& view, @@ -495,6 +563,18 @@ transferRate(ReadView const& view, AccountID const& issuer) return parityRate; } +Rate +transferRate(ReadView const& view, MPTID const& id) +{ + auto const sle = view.read(keylet::mptIssuance(id)); + + // fee is 0-50,000 (0-50%), rate is 1,000,000,000-2,000,000,000 + if (sle && sle->isFieldPresent(sfTransferFee)) + return Rate{1'000'000'000u + 10'000 * sle->getFieldU16(sfTransferFee)}; + + return parityRate; +} + bool areCompatible( ReadView const& validLedger, @@ -820,9 +900,8 @@ trustCreate( bSetHigh ? sfHighLimit : sfLowLimit, saLimit); sleRippleState->setFieldAmount( bSetHigh ? sfLowLimit : sfHighLimit, - STAmount( - {saBalance.getCurrency(), - bSetDst ? uSrcAccountID : uDstAccountID})); + STAmount(Issue{ + saBalance.getCurrency(), bSetDst ? uSrcAccountID : uDstAccountID})); if (uQualityIn) sleRippleState->setFieldU32( @@ -1055,7 +1134,7 @@ rippleCredit( return tesSUCCESS; } - STAmount const saReceiverLimit({currency, uReceiverID}); + STAmount const saReceiverLimit(Issue{currency, uReceiverID}); STAmount saBalance{saAmount}; saBalance.setIssuer(noAccount()); @@ -1156,7 +1235,7 @@ accountSend( } else { - assert(saAmount >= beast::zero); + assert(saAmount >= beast::zero && !saAmount.holds()); } /* If we aren't sending anything or if the sender is the same as the @@ -1256,6 +1335,96 @@ accountSend( return terResult; } +static TER +rippleSendMPT( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STAmount const& saAmount, + STAmount& saActual, + beast::Journal j, + WaiveTransferFee waiveFee) +{ + assert(uSenderID != uReceiverID); + + // Safe to get MPT since rippleSendMPT is only called by accountSendMPT + auto const issuer = saAmount.getIssuer(); + + if (uSenderID == issuer || uReceiverID == issuer || issuer == noAccount()) + { + // if sender is issuer, check that the new OutstandingAmount will not + // exceed MaximumAmount + if (uSenderID == issuer) + { + auto const mptID = + keylet::mptIssuance(saAmount.get().getMptID()); + auto const sle = view.peek(mptID); + if (!sle) + return tecMPT_ISSUANCE_NOT_FOUND; + + if (sle->getFieldU64(sfOutstandingAmount) + saAmount.mpt().value() > + (*sle)[~sfMaximumAmount].value_or(maxMPTokenAmount)) + return tecMPT_MAX_AMOUNT_EXCEEDED; + } + + // Direct send: redeeming IOUs and/or sending own IOUs. + auto const ter = + rippleMPTCredit(view, uSenderID, uReceiverID, saAmount, j); + if (ter != tesSUCCESS) + return ter; + saActual = saAmount; + return tesSUCCESS; + } + + // Sending 3rd party MPTs: transit. + if (auto const sle = + view.read(keylet::mptIssuance(saAmount.get().getMptID()))) + { + saActual = (waiveFee == WaiveTransferFee::Yes) + ? saAmount + : multiply( + saAmount, + transferRate(view, saAmount.get().getMptID())); + + JLOG(j.debug()) << "rippleSend> " << to_string(uSenderID) << " - > " + << to_string(uReceiverID) + << " : deliver=" << saAmount.getFullText() + << " cost=" << saActual.getFullText(); + + if (auto const terResult = + rippleMPTCredit(view, issuer, uReceiverID, saAmount, j); + terResult != tesSUCCESS) + return terResult; + + return rippleMPTCredit(view, uSenderID, issuer, saActual, j); + } + + return tecINTERNAL; +} + +TER +accountSendMPT( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STAmount const& saAmount, + beast::Journal j, + WaiveTransferFee waiveFee) +{ + assert(saAmount >= beast::zero && saAmount.holds()); + + /* If we aren't sending anything or if the sender is the same as the + * receiver then we don't need to do anything. + */ + if (!saAmount || (uSenderID == uReceiverID)) + return tesSUCCESS; + + STAmount saActual{saAmount.asset()}; + + return rippleSendMPT( + view, uSenderID, uReceiverID, saAmount, saActual, j, waiveFee); +} + static bool updateTrustLine( ApplyView& view, @@ -1377,7 +1546,7 @@ issueIOU( // NIKB TODO: The limit uses the receiver's account as the issuer and // this is unnecessarily inefficient as copying which could be avoided // is now required. Consider available options. - STAmount const limit({issue.currency, account}); + STAmount const limit(Issue{issue.currency, account}); STAmount final_balance = amount; final_balance.setIssuer(noAccount()); @@ -1537,6 +1706,39 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account) return tesSUCCESS; } +TER +requireAuth(ReadView const& view, MPTIssue const& mpt, AccountID const& account) +{ + auto const mptID = keylet::mptIssuance(mpt.getMptID()); + if (auto const sle = view.read(mptID); + sle && sle->getFieldU32(sfFlags) & lsfMPTRequireAuth) + { + auto const mptokenID = keylet::mptoken(mptID.key, account); + if (auto const tokSle = view.read(mptokenID); tokSle && + //(sle->getFlags() & lsfMPTRequireAuth) && + !(tokSle->getFlags() & lsfMPTAuthorized)) + return TER{tecNO_AUTH}; + } + return tesSUCCESS; +} + +TER +canTransfer( + ReadView const& view, + MPTIssue const& mpt, + AccountID const& from, + AccountID const& to) +{ + auto const mptID = keylet::mptIssuance(mpt.getMptID()); + if (auto const sle = view.read(mptID); + sle && !(sle->getFieldU32(sfFlags) & lsfMPTCanTransfer)) + { + if (from != (*sle)[sfIssuer] && to != (*sle)[sfIssuer]) + return TER{tecNO_AUTH}; + } + return tesSUCCESS; +} + TER cleanupOnAccountDelete( ApplyView& view, @@ -1662,4 +1864,84 @@ deleteAMMTrustLine( return tesSUCCESS; } +TER +rippleMPTCredit( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STAmount saAmount, + beast::Journal j) +{ + auto const mptID = keylet::mptIssuance(saAmount.get().getMptID()); + auto const issuer = saAmount.getIssuer(); + if (!view.exists(mptID)) + return tecMPT_ISSUANCE_NOT_FOUND; + if (uSenderID == issuer) + { + if (auto sle = view.peek(mptID)) + { + sle->setFieldU64( + sfOutstandingAmount, + sle->getFieldU64(sfOutstandingAmount) + saAmount.mpt().value()); + + view.update(sle); + } + else + return tecINTERNAL; + } + else + { + auto const mptokenID = keylet::mptoken(mptID.key, uSenderID); + if (auto sle = view.peek(mptokenID)) + { + auto const amt = sle->getFieldU64(sfMPTAmount); + auto const pay = saAmount.mpt().value(); + if (amt >= pay) + { + if (amt == pay) + sle->makeFieldAbsent(sfMPTAmount); + else + sle->setFieldU64(sfMPTAmount, amt - pay); + view.update(sle); + } + else + return tecINSUFFICIENT_FUNDS; + } + else + return tecNO_AUTH; + } + + if (uReceiverID == issuer) + { + if (auto sle = view.peek(mptID)) + { + auto const outstanding = sle->getFieldU64(sfOutstandingAmount); + auto const redeem = saAmount.mpt().value(); + if (outstanding >= redeem) + { + sle->setFieldU64(sfOutstandingAmount, outstanding - redeem); + view.update(sle); + } + else + return tecINSUFFICIENT_FUNDS; + } + else + return tecINTERNAL; + } + else + { + auto const mptokenID = keylet::mptoken(mptID.key, uReceiverID); + if (auto sle = view.peek(mptokenID)) + { + sle->setFieldU64( + sfMPTAmount, + sle->getFieldU64(sfMPTAmount) + saAmount.mpt().value()); + view.update(sle); + } + else + return tecNO_AUTH; + } + return tesSUCCESS; +} + } // namespace ripple diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h b/src/xrpld/rpc/MPTokenIssuanceID.h new file mode 100644 index 00000000000..f7f45fded3b --- /dev/null +++ b/src/xrpld/rpc/MPTokenIssuanceID.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_RPC_MPTOKENISSUANCEID_H_INCLUDED +#define RIPPLE_RPC_MPTOKENISSUANCEID_H_INCLUDED + +#include +#include +#include +#include + +#include +#include + +namespace ripple { + +namespace RPC { + +bool +canHaveMPTokenIssuanceID( + std::shared_ptr const& serializedTx, + TxMeta const& transactionMeta); + +std::optional +getIDFromCreatedIssuance(TxMeta const& transactionMeta); + +void +insertMPTokenIssuanceID( + Json::Value& response, + std::shared_ptr const& transaction, + TxMeta const& transactionMeta); +/** @} */ + +} // namespace RPC +} // namespace ripple + +#endif diff --git a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp new file mode 100644 index 00000000000..8de1e8f3344 --- /dev/null +++ b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp @@ -0,0 +1,83 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include +#include +#include + +namespace ripple { + +namespace RPC { + +bool +canHaveMPTokenIssuanceID( + std::shared_ptr const& serializedTx, + TxMeta const& transactionMeta) +{ + if (!serializedTx) + return false; + + TxType const tt = serializedTx->getTxnType(); + if (tt != ttMPTOKEN_ISSUANCE_CREATE) + return false; + + // if the transaction failed nothing could have been delivered. + if (transactionMeta.getResultTER() != tesSUCCESS) + return false; + + return true; +} + +std::optional +getIDFromCreatedIssuance(TxMeta const& transactionMeta) +{ + for (STObject const& node : transactionMeta.getNodes()) + { + if (node.getFieldU16(sfLedgerEntryType) != ltMPTOKEN_ISSUANCE || + node.getFName() != sfCreatedNode) + continue; + + auto const& mptNode = + node.peekAtField(sfNewFields).downcast(); + return getMptID( + mptNode.getAccountID(sfIssuer), mptNode.getFieldU32(sfSequence)); + } + + return std::nullopt; +} + +void +insertMPTokenIssuanceID( + Json::Value& response, + std::shared_ptr const& transaction, + TxMeta const& transactionMeta) +{ + if (!canHaveMPTokenIssuanceID(transaction, transactionMeta)) + return; + + std::optional result = getIDFromCreatedIssuance(transactionMeta); + if (result.has_value()) + response[jss::mpt_issuance_id] = to_string(result.value()); +} + +} // namespace RPC +} // namespace ripple diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index fa66fecfbba..87ec9ff03f4 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -285,7 +285,13 @@ getAccountObjects( if (!typeFilter.has_value() || typeMatchesFilter(typeFilter.value(), sleNode->getType())) { - jvObjects.append(sleNode->getJson(JsonOptions::none)); + auto sleJson = sleNode->getJson(JsonOptions::none); + if (sleNode->getType() == ltMPTOKEN_ISSUANCE) + sleJson[jss::mpt_issuance_id] = to_string(getMptID( + sleNode->getAccountID(sfIssuer), + (*sleNode)[sfSequence])); + + jvObjects.append(sleJson); } if (++i == mlimit) @@ -915,7 +921,7 @@ chooseLedgerEntryType(Json::Value const& params) std::pair result{RPC::Status::OK, ltANY}; if (params.isMember(jss::type)) { - static constexpr std::array, 22> + static constexpr std::array, 24> types{ {{jss::account, ltACCOUNT_ROOT}, {jss::amendments, ltAMENDMENTS}, @@ -939,7 +945,9 @@ chooseLedgerEntryType(Json::Value const& params) {jss::ticket, ltTICKET}, {jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID}, {jss::xchain_owned_create_account_claim_id, - ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}}}; + ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, + {jss::mpt_issuance, ltMPTOKEN_ISSUANCE}, + {jss::mptoken, ltMPTOKEN}}}; auto const& p = params[jss::type]; if (!p.isString()) diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 65ee50c0891..401f808f56a 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -213,7 +213,8 @@ checkPayment( if (!dstAccountID) return RPC::invalid_field_error("tx_json.Destination"); - if ((doPath == false) && params.isMember(jss::build_path)) + if (((doPath == false) && params.isMember(jss::build_path)) || + (params.isMember(jss::build_path) && amount.holds())) return RPC::make_error( rpcINVALID_PARAMS, "Field 'build_path' not allowed in this context."); diff --git a/src/xrpld/rpc/detail/Tuning.h b/src/xrpld/rpc/detail/Tuning.h index 4f4a8be1bf7..d4557a9fcfa 100644 --- a/src/xrpld/rpc/detail/Tuning.h +++ b/src/xrpld/rpc/detail/Tuning.h @@ -57,6 +57,9 @@ static LimitRange constexpr accountNFTokens = {20, 100, 400}; /** Limits for the nft_buy_offers & nft_sell_offers commands. */ static LimitRange constexpr nftOffers = {50, 250, 500}; +/** Limits for the nft_buy_offers & nft_sell_offers commands. */ +static LimitRange constexpr mptHolders = {10, 200, 400}; + static int constexpr defaultAutoFillFeeMultiplier = 10; static int constexpr defaultAutoFillFeeDivisor = 1; static int constexpr maxPathfindsInProgress = 2; diff --git a/src/xrpld/rpc/handlers/AccountObjects.cpp b/src/xrpld/rpc/handlers/AccountObjects.cpp index c192fbf9071..63389753244 100644 --- a/src/xrpld/rpc/handlers/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/AccountObjects.cpp @@ -222,7 +222,9 @@ doAccountObjects(RPC::JsonContext& context) {jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID}, {jss::xchain_owned_create_account_claim_id, ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, - {jss::bridge, ltBRIDGE}}; + {jss::bridge, ltBRIDGE}, + {jss::mpt_issuance, ltMPTOKEN_ISSUANCE}, + {jss::mptoken, ltMPTOKEN}}; typeFilter.emplace(); typeFilter->reserve(std::size(deletionBlockers)); diff --git a/src/xrpld/rpc/handlers/AccountTx.cpp b/src/xrpld/rpc/handlers/AccountTx.cpp index a85abd86682..887694daf21 100644 --- a/src/xrpld/rpc/handlers/AccountTx.cpp +++ b/src/xrpld/rpc/handlers/AccountTx.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -349,6 +350,8 @@ populateJsonResponse( insertDeliveredAmount( jvObj[jss::meta], context, txn, *txnMeta); insertNFTSyntheticInJson(jvObj, sttx, *txnMeta); + RPC::insertMPTokenIssuanceID( + jvObj[jss::meta], sttx, *txnMeta); } else assert(false && "Missing transaction medatata"); diff --git a/src/xrpld/rpc/handlers/Handlers.h b/src/xrpld/rpc/handlers/Handlers.h index 0085f51465a..b1f65cdea57 100644 --- a/src/xrpld/rpc/handlers/Handlers.h +++ b/src/xrpld/rpc/handlers/Handlers.h @@ -51,6 +51,8 @@ doBlackList(RPC::JsonContext&); Json::Value doCanDelete(RPC::JsonContext&); Json::Value +doMPTHolders(RPC::JsonContext&); +Json::Value doChannelAuthorize(RPC::JsonContext&); Json::Value doChannelVerify(RPC::JsonContext&); diff --git a/src/xrpld/rpc/handlers/LedgerData.cpp b/src/xrpld/rpc/handlers/LedgerData.cpp index ad26b83b43b..80d321f0372 100644 --- a/src/xrpld/rpc/handlers/LedgerData.cpp +++ b/src/xrpld/rpc/handlers/LedgerData.cpp @@ -124,6 +124,10 @@ doLedgerData(RPC::JsonContext& context) Json::Value& entry = nodes.append(sle->getJson(JsonOptions::none)); entry[jss::index] = to_string(sle->key()); + + if (sle->getType() == ltMPTOKEN_ISSUANCE) + entry[jss::mpt_issuance_id] = to_string(getMptID( + sle->getAccountID(sfIssuer), (*sle)[sfSequence])); } } } diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index f461cd3100b..30b1113cd05 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -644,6 +644,73 @@ doLedgerEntry(RPC::JsonContext& context) uNodeIndex = keylet::oracle(*account, *documentID).key; } } + else if (context.params.isMember(jss::mpt_issuance)) + { + expectedType = ltMPTOKEN_ISSUANCE; + auto const unparsedMPTIssuanceID = + context.params[jss::mpt_issuance]; + if (unparsedMPTIssuanceID.isString()) + { + uint192 mptIssuanceID; + if (!mptIssuanceID.parseHex(unparsedMPTIssuanceID.asString())) + { + uNodeIndex = beast::zero; + jvResult[jss::error] = "malformedRequest"; + } + else + uNodeIndex = keylet::mptIssuance(mptIssuanceID).key; + } + else + { + jvResult[jss::error] = "malformedRequest"; + } + } + else if (context.params.isMember(jss::mptoken)) + { + expectedType = ltMPTOKEN; + if (!context.params[jss::mptoken].isObject()) + { + if (!uNodeIndex.parseHex( + context.params[jss::mptoken].asString())) + { + uNodeIndex = beast::zero; + jvResult[jss::error] = "malformedRequest"; + } + } + else if ( + !context.params[jss::mptoken].isMember(jss::mpt_issuance_id) || + !context.params[jss::mptoken].isMember(jss::account)) + { + jvResult[jss::error] = "malformedRequest"; + } + else + { + try + { + auto const mptIssuanceIdStr = + context.params[jss::mptoken][jss::mpt_issuance_id] + .asString(); + + uint192 mptIssuanceID; + if (!mptIssuanceID.parseHex(mptIssuanceIdStr)) + Throw( + "Cannot parse mpt_issuance_id"); + + auto const account = parseBase58( + context.params[jss::mptoken][jss::account].asString()); + + if (!account || account->isZero()) + jvResult[jss::error] = "malformedAddress"; + else + uNodeIndex = + keylet::mptoken(mptIssuanceID, *account).key; + } + catch (std::runtime_error const&) + { + jvResult[jss::error] = "malformedRequest"; + } + } + } else { if (context.params.isMember("params") && diff --git a/src/xrpld/rpc/handlers/Tx.cpp b/src/xrpld/rpc/handlers/Tx.cpp index ba103d186fc..98af3a809bf 100644 --- a/src/xrpld/rpc/handlers/Tx.cpp +++ b/src/xrpld/rpc/handlers/Tx.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -265,6 +266,7 @@ populateJsonResponse( insertDeliveredAmount( response[jss::meta], context, result.txn, *meta); insertNFTSyntheticInJson(response, sttx, *meta); + RPC::insertMPTokenIssuanceID(response[jss::meta], sttx, *meta); } } response[jss::validated] = result.validated;