From b91ed89fcd24d11426de72f9722a7b7dd6dcaaf4 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Sat, 17 Feb 2024 10:08:36 +0000 Subject: [PATCH] MPT code with no direct pay and hacked STAmount Co-Authored-By: Gregory Tsipenyuk --- Builds/CMake/RippledCore.cmake | 7 + src/ripple/app/ledger/impl/LedgerToJson.cpp | 13 + src/ripple/app/misc/NetworkOPs.cpp | 3 + src/ripple/app/tx/impl/InvariantCheck.cpp | 168 ++++ src/ripple/app/tx/impl/InvariantCheck.h | 27 +- src/ripple/app/tx/impl/MPTokenAuthorize.cpp | 231 +++++ src/ripple/app/tx/impl/MPTokenAuthorize.h | 48 + .../app/tx/impl/MPTokenIssuanceCreate.cpp | 121 +++ .../app/tx/impl/MPTokenIssuanceCreate.h | 45 + .../app/tx/impl/MPTokenIssuanceDestroy.cpp | 82 ++ .../app/tx/impl/MPTokenIssuanceDestroy.h | 48 + src/ripple/app/tx/impl/MPTokenIssuanceSet.cpp | 118 +++ src/ripple/app/tx/impl/MPTokenIssuanceSet.h | 48 + src/ripple/app/tx/impl/applySteps.cpp | 12 + src/ripple/basics/MPTAmount.h | 246 +++++ src/ripple/basics/base_uint.h | 1 + src/ripple/protocol/Feature.h | 3 +- src/ripple/protocol/Indexes.h | 38 + src/ripple/protocol/LedgerFormats.h | 24 + src/ripple/protocol/Protocol.h | 6 + src/ripple/protocol/SField.h | 11 + src/ripple/protocol/STAmount.h | 43 +- src/ripple/protocol/STBitString.h | 8 + src/ripple/protocol/STObject.h | 2 + src/ripple/protocol/Serializer.h | 6 + src/ripple/protocol/TER.h | 7 +- src/ripple/protocol/TxFlags.h | 31 + src/ripple/protocol/TxFormats.h | 11 + src/ripple/protocol/UintTypes.h | 13 + src/ripple/protocol/impl/Feature.cpp | 1 + src/ripple/protocol/impl/Indexes.cpp | 64 ++ src/ripple/protocol/impl/LedgerFormats.cpp | 30 + src/ripple/protocol/impl/SField.cpp | 10 + src/ripple/protocol/impl/STAmount.cpp | 292 ++++-- src/ripple/protocol/impl/STObject.cpp | 6 + src/ripple/protocol/impl/STParsedJSON.cpp | 24 + src/ripple/protocol/impl/STTx.cpp | 14 +- src/ripple/protocol/impl/STVar.cpp | 6 + src/ripple/protocol/impl/TER.cpp | 3 + src/ripple/protocol/impl/TxFormats.cpp | 33 + src/ripple/protocol/impl/UintTypes.cpp | 7 + src/ripple/protocol/jss.h | 204 ++-- src/ripple/rpc/MPTokenIssuanceID.h | 53 + src/ripple/rpc/handlers/AccountObjects.cpp | 4 +- src/ripple/rpc/handlers/AccountTx.cpp | 3 + src/ripple/rpc/handlers/Handlers.h | 2 + src/ripple/rpc/handlers/LedgerData.cpp | 4 + src/ripple/rpc/handlers/LedgerEntry.cpp | 67 ++ src/ripple/rpc/handlers/Tx.cpp | 2 + src/ripple/rpc/impl/Handler.cpp | 1 + src/ripple/rpc/impl/MPTokenIssuanceID.cpp | 83 ++ src/ripple/rpc/impl/RPCHelpers.cpp | 14 +- src/ripple/rpc/impl/Tuning.h | 3 + src/test/app/MPToken_test.cpp | 911 ++++++++++++++++++ src/test/jtx.h | 1 + src/test/jtx/Env.h | 6 + src/test/jtx/amount.h | 70 ++ src/test/jtx/impl/mpt.cpp | 387 ++++++++ src/test/jtx/mpt.h | 258 +++++ 59 files changed, 3814 insertions(+), 170 deletions(-) create mode 100644 src/ripple/app/tx/impl/MPTokenAuthorize.cpp create mode 100644 src/ripple/app/tx/impl/MPTokenAuthorize.h create mode 100644 src/ripple/app/tx/impl/MPTokenIssuanceCreate.cpp create mode 100644 src/ripple/app/tx/impl/MPTokenIssuanceCreate.h create mode 100644 src/ripple/app/tx/impl/MPTokenIssuanceDestroy.cpp create mode 100644 src/ripple/app/tx/impl/MPTokenIssuanceDestroy.h create mode 100644 src/ripple/app/tx/impl/MPTokenIssuanceSet.cpp create mode 100644 src/ripple/app/tx/impl/MPTokenIssuanceSet.h create mode 100644 src/ripple/basics/MPTAmount.h create mode 100644 src/ripple/rpc/MPTokenIssuanceID.h create mode 100644 src/ripple/rpc/impl/MPTokenIssuanceID.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 diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 9c0c7016379..199818b9e84 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -544,6 +544,10 @@ target_sources (rippled PRIVATE src/ripple/app/tx/impl/AMMWithdraw.cpp src/ripple/app/tx/impl/ApplyContext.cpp src/ripple/app/tx/impl/BookTip.cpp + src/ripple/app/tx/impl/MPTokenIssuanceCreate.cpp + src/ripple/app/tx/impl/MPTokenIssuanceDestroy.cpp + src/ripple/app/tx/impl/MPTokenAuthorize.cpp + src/ripple/app/tx/impl/MPTokenIssuanceSet.cpp src/ripple/app/tx/impl/CancelCheck.cpp src/ripple/app/tx/impl/CancelOffer.cpp src/ripple/app/tx/impl/CashCheck.cpp @@ -765,6 +769,7 @@ target_sources (rippled PRIVATE src/ripple/rpc/handlers/ValidatorListSites.cpp src/ripple/rpc/handlers/Validators.cpp src/ripple/rpc/handlers/WalletPropose.cpp + src/ripple/rpc/impl/MPTokenIssuanceID.cpp src/ripple/rpc/impl/DeliveredAmount.cpp src/ripple/rpc/impl/Handler.cpp src/ripple/rpc/impl/LegacyPathFind.cpp @@ -814,6 +819,7 @@ if (tests) src/test/app/AMM_test.cpp src/test/app/AMMCalc_test.cpp src/test/app/AMMExtended_test.cpp + src/test/app/MPToken_test.cpp src/test/app/Check_test.cpp src/test/app/Clawback_test.cpp src/test/app/CrossingLimits_test.cpp @@ -962,6 +968,7 @@ if (tests) src/test/jtx/impl/Account.cpp src/test/jtx/impl/AMM.cpp src/test/jtx/impl/AMMTest.cpp + src/test/jtx/impl/mpt.cpp src/test/jtx/impl/Env.cpp src/test/jtx/impl/JSONRPCClient.cpp src/test/jtx/impl/TestHelpers.cpp diff --git a/src/ripple/app/ledger/impl/LedgerToJson.cpp b/src/ripple/app/ledger/impl/LedgerToJson.cpp index 9cef4e65b56..065d97c9c6d 100644 --- a/src/ripple/app/ledger/impl/LedgerToJson.cpp +++ b/src/ripple/app/ledger/impl/LedgerToJson.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include namespace ripple { @@ -157,6 +158,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()) @@ -190,6 +197,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/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 239fd6306e0..a4cc173da23 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -67,6 +67,7 @@ #include #include #include +#include #include #include #include @@ -3120,6 +3121,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/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index c717777f88f..51b4d142a58 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -392,6 +392,8 @@ LedgerEntryTypesMatch::visitEntry( case ltXCHAIN_OWNED_CLAIM_ID: case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: case ltDID: + case ltMPTOKEN_ISSUANCE: + case ltMPTOKEN: break; default: invalidTypeAdded_ = true; @@ -799,4 +801,170 @@ ValidClawback::finalize( 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/ripple/app/tx/impl/InvariantCheck.h b/src/ripple/app/tx/impl/InvariantCheck.h index eb606c2ed3b..d30cc413b2d 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.h +++ b/src/ripple/app/tx/impl/InvariantCheck.h @@ -418,6 +418,30 @@ class ValidClawback 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 + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); +}; + // additional invariant checks can be declared above and then added to this // tuple using InvariantChecks = std::tuple< @@ -432,7 +456,8 @@ using InvariantChecks = std::tuple< ValidNewAccountRoot, ValidNFTokenPage, NFTokenCountTracking, - ValidClawback>; + ValidClawback, + ValidMPTIssuance>; /** * @brief get a tuple of all invariant checks diff --git a/src/ripple/app/tx/impl/MPTokenAuthorize.cpp b/src/ripple/app/tx/impl/MPTokenAuthorize.cpp new file mode 100644 index 00000000000..86821266328 --- /dev/null +++ b/src/ripple/app/tx/impl/MPTokenAuthorize.cpp @@ -0,0 +1,231 @@ +//------------------------------------------------------------------------------ +/* + 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 temMALFORMED; + + // 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 temMALFORMED; + + // 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::doApply() +{ + auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; + auto const sleAcct = view().peek(keylet::account(account_)); + if (!sleAcct) + return tecINTERNAL; + + auto const holderID = ctx_.tx[~sfMPTokenHolder]; + + // If the account that submitted the tx is a holder + // Note: `account_` is holder's account + // `holderID` is NOT used + if (!holderID) + { + // When a holder wants to unauthorize/delete a MPT, the ledger must + // - delete mptokenKey from owner directory + // - delete the MPToken + if (ctx_.tx.getFlags() & tfMPTUnauthorize) + { + auto const mptokenKey = keylet::mptoken(mptIssuanceID, account_); + auto const sleMpt = view().peek(mptokenKey); + if (!sleMpt) + return tecINTERNAL; + + if (!view().dirRemove( + keylet::ownerDir(account_), + (*sleMpt)[sfOwnerNode], + sleMpt->key(), + false)) + return tecINTERNAL; + + adjustOwnerCount(view(), sleAcct, -1, j_); + + 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 (mPriorBalance < reserveCreate) + return tecINSUFFICIENT_RESERVE; + + auto const mptokenKey = keylet::mptoken(mptIssuanceID, account_); + + auto const ownerNode = view().dirInsert( + keylet::ownerDir(account_), mptokenKey, describeOwnerDir(account_)); + + if (!ownerNode) + return tecDIR_FULL; + + auto mptoken = std::make_shared(mptokenKey); + (*mptoken)[sfAccount] = account_; + (*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID; + (*mptoken)[sfFlags] = 0; + (*mptoken)[sfOwnerNode] = *ownerNode; + view().insert(mptoken); + + // Update owner count. + adjustOwnerCount(view(), sleAcct, 1, j_); + + return tesSUCCESS; + } + + auto const sleMptIssuance = view().read(keylet::mptIssuance(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 (account_ != (*sleMptIssuance)[sfIssuer]) + return tecINTERNAL; + + auto const sleMpt = view().peek(keylet::mptoken(mptIssuanceID, *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 (ctx_.tx.getFlags() & 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; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/MPTokenAuthorize.h b/src/ripple/app/tx/impl/MPTokenAuthorize.h new file mode 100644 index 00000000000..39d0fee8ac6 --- /dev/null +++ b/src/ripple/app/tx/impl/MPTokenAuthorize.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_MPTOKENAUTHORIZE_H_INCLUDED +#define RIPPLE_TX_MPTOKENAUTHORIZE_H_INCLUDED + +#include + +namespace ripple { + +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); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/ripple/app/tx/impl/MPTokenIssuanceCreate.cpp b/src/ripple/app/tx/impl/MPTokenIssuanceCreate.cpp new file mode 100644 index 00000000000..9a2e27addf7 --- /dev/null +++ b/src/ripple/app/tx/impl/MPTokenIssuanceCreate.cpp @@ -0,0 +1,121 @@ +//------------------------------------------------------------------------------ +/* + 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::doApply() +{ + auto const acct = view().peek(keylet::account(account_)); + if (!acct) + return tecINTERNAL; + + if (mPriorBalance < view().fees().accountReserve((*acct)[sfOwnerCount] + 1)) + return tecINSUFFICIENT_RESERVE; + + auto const mptIssuanceKeylet = + keylet::mptIssuance(account_, ctx_.tx.getSeqProxy().value()); + + // create the MPTokenIssuance + { + auto const ownerNode = view().dirInsert( + keylet::ownerDir(account_), + mptIssuanceKeylet, + describeOwnerDir(account_)); + + if (!ownerNode) + return tecDIR_FULL; + + auto mptIssuance = std::make_shared(mptIssuanceKeylet); + (*mptIssuance)[sfFlags] = ctx_.tx.getFlags() & ~tfUniversal; + (*mptIssuance)[sfIssuer] = account_; + (*mptIssuance)[sfOutstandingAmount] = 0; + (*mptIssuance)[sfOwnerNode] = *ownerNode; + (*mptIssuance)[sfSequence] = ctx_.tx.getSeqProxy().value(); + + if (auto const max = ctx_.tx[~sfMaximumAmount]) + (*mptIssuance)[sfMaximumAmount] = *max; + + if (auto const scale = ctx_.tx[~sfAssetScale]) + (*mptIssuance)[sfAssetScale] = *scale; + + if (auto const fee = ctx_.tx[~sfTransferFee]) + (*mptIssuance)[sfTransferFee] = *fee; + + if (auto const metadata = ctx_.tx[~sfMPTokenMetadata]) + (*mptIssuance)[sfMPTokenMetadata] = *metadata; + + view().insert(mptIssuance); + } + + // Update owner count. + adjustOwnerCount(view(), acct, 1, j_); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/MPTokenIssuanceCreate.h b/src/ripple/app/tx/impl/MPTokenIssuanceCreate.h new file mode 100644 index 00000000000..c6c2b4b3505 --- /dev/null +++ b/src/ripple/app/tx/impl/MPTokenIssuanceCreate.h @@ -0,0 +1,45 @@ +//------------------------------------------------------------------------------ +/* + 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 { + +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; +}; + +} // namespace ripple + +#endif diff --git a/src/ripple/app/tx/impl/MPTokenIssuanceDestroy.cpp b/src/ripple/app/tx/impl/MPTokenIssuanceDestroy.cpp new file mode 100644 index 00000000000..60e0e77a933 --- /dev/null +++ b/src/ripple/app/tx/impl/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/ripple/app/tx/impl/MPTokenIssuanceDestroy.h b/src/ripple/app/tx/impl/MPTokenIssuanceDestroy.h new file mode 100644 index 00000000000..50523cf9aab --- /dev/null +++ b/src/ripple/app/tx/impl/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/ripple/app/tx/impl/MPTokenIssuanceSet.cpp b/src/ripple/app/tx/impl/MPTokenIssuanceSet.cpp new file mode 100644 index 00000000000..8f46379f87c --- /dev/null +++ b/src/ripple/app/tx/impl/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/ripple/app/tx/impl/MPTokenIssuanceSet.h b/src/ripple/app/tx/impl/MPTokenIssuanceSet.h new file mode 100644 index 00000000000..b9e2658d8d0 --- /dev/null +++ b/src/ripple/app/tx/impl/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/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index 10e2b0c4524..6cf13c51273 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -37,6 +37,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -159,6 +163,14 @@ with_txn_type(TxType txnType, F&& f) return f.template operator()(); case ttDID_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/ripple/basics/MPTAmount.h b/src/ripple/basics/MPTAmount.h new file mode 100644 index 00000000000..2678c56040e --- /dev/null +++ b/src/ripple/basics/MPTAmount.h @@ -0,0 +1,246 @@ +//------------------------------------------------------------------------------ +/* + 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 mpt_type = std::int64_t; + +protected: + mpt_type mpt_; + +public: + MPTAmount() = default; + constexpr MPTAmount(MPTAmount const& other) = default; + constexpr MPTAmount& + operator=(MPTAmount const& other) = default; + + constexpr MPTAmount(beast::Zero) : mpt_(0) + { + } + + constexpr explicit MPTAmount(mpt_type value) : mpt_(value) + { + } + + constexpr MPTAmount& operator=(beast::Zero) + { + mpt_ = 0; + return *this; + } + + MPTAmount& + operator=(mpt_type value) + { + mpt_ = value; + return *this; + } + + constexpr MPTAmount + operator*(mpt_type const& rhs) const + { + return MPTAmount{mpt_ * rhs}; + } + + friend constexpr MPTAmount + operator*(mpt_type lhs, MPTAmount const& rhs) + { + // multiplication is commutative + return rhs * lhs; + } + + MPTAmount& + operator+=(MPTAmount const& other) + { + mpt_ += other.mpt(); + return *this; + } + + MPTAmount& + operator-=(MPTAmount const& other) + { + mpt_ -= other.mpt(); + return *this; + } + + MPTAmount& + operator+=(mpt_type const& rhs) + { + mpt_ += rhs; + return *this; + } + + MPTAmount& + operator-=(mpt_type const& rhs) + { + mpt_ -= rhs; + return *this; + } + + MPTAmount& + operator*=(mpt_type const& rhs) + { + mpt_ *= rhs; + return *this; + } + + MPTAmount + operator-() const + { + return MPTAmount{-mpt_}; + } + + bool + operator==(MPTAmount const& other) const + { + return mpt_ == other.mpt_; + } + + bool + operator==(mpt_type other) const + { + return mpt_ == other; + } + + bool + operator<(MPTAmount const& other) const + { + return mpt_ < other.mpt_; + } + + /** Returns true if the amount is not zero */ + explicit constexpr operator bool() const noexcept + { + return mpt_ != 0; + } + + /** Return the sign of the amount */ + constexpr int + signum() const noexcept + { + return (mpt_ < 0) ? -1 : (mpt_ ? 1 : 0); + } + + Json::Value + 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 (mpt_ < min) + return min; + if (mpt_ > max) + return max; + return static_cast(mpt_); + } + + /** Returns the underlying value. Code SHOULD NOT call this + function unless the type has been abstracted away, + e.g. in a templated function. + */ + constexpr mpt_type + mpt() const + { + return mpt_; + } + + friend std::istream& + operator>>(std::istream& s, MPTAmount& val) + { + s >> val.mpt_; + return s; + } + + static MPTAmount + minPositiveAmount() + { + return MPTAmount{1}; + } +}; + +// Output MPTAmount as just the value. +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const MPTAmount& q) +{ + return os << q.mpt(); +} + +inline std::string +to_string(MPTAmount const& amount) +{ + return std::to_string(amount.mpt()); +} + +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.mpt()); + auto const neg = amt.mpt() < 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("XRP mulRatio overflow"); + return MPTAmount(r.convert_to()); +} + +} // namespace ripple + +#endif // RIPPLE_BASICS_INTEGRALAMOUNT_H_INCLUDED diff --git a/src/ripple/basics/base_uint.h b/src/ripple/basics/base_uint.h index 8b15b082647..a18ce999afe 100644 --- a/src/ripple/basics/base_uint.h +++ b/src/ripple/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 diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 8e6483b1dbd..56baeef492e 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,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 = 67; +static constexpr std::size_t numFeatures = 68; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -354,6 +354,7 @@ extern uint256 const featureDID; extern uint256 const fixFillOrKill; extern uint256 const fixNFTokenReserve; extern uint256 const fixInnerObjTemplate; +extern uint256 const featureMPTokensV1; } // namespace ripple diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 9a330b6b4f0..db3caf504ff 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -283,6 +283,41 @@ xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq); Keylet did(AccountID const& account) noexcept; +Keylet +mptIssuance(AccountID const& issuer, std::uint32_t seq) noexcept; + +Keylet +mptIssuance(uint192 const& mpt) noexcept; + +Keylet +mptIssuance(ripple::MPT const& mpt) noexcept; + +inline Keylet +mptIssuance(uint256 const& issuance) +{ + return {ltMPTOKEN_ISSUANCE, issuance}; +} + +Keylet +mptoken(MPT const& issuanceID, AccountID const& holder) noexcept; + +Keylet +mptoken(uint192 const& issuanceID, AccountID const& holder) noexcept; + +inline Keylet +mptoken(uint256 const& mptokenKey) +{ + return {ltMPTOKEN, mptokenKey}; +} + +Keylet +mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept; + +Keylet +mpt_dir(uint192 const& id) noexcept; + +Keylet +mpt_dir(uint256 const& id) noexcept; } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: @@ -303,6 +338,9 @@ getTicketIndex(AccountID const& account, std::uint32_t uSequence); uint256 getTicketIndex(AccountID const& account, SeqProxy ticketSeq); +uint192 +getMptID(AccountID const& account, std::uint32_t sequence); + } // namespace ripple #endif diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index db64942790a..60c6b1731d9 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -192,6 +192,18 @@ enum LedgerEntryType : std::uint16_t */ ltDID = 0x0049, + /** 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. @@ -303,6 +315,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/src/ripple/protocol/Protocol.h b/src/ripple/protocol/Protocol.h index 49642efc4cf..80fd05c1223 100644 --- a/src/ripple/protocol/Protocol.h +++ b/src/ripple/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 = 0x7FFFFFFFFFFFFFFFull; + /** A ledger index. */ using LedgerIndex = std::uint32_t; diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 5d7acb12383..6a9eb31723d 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -369,6 +369,7 @@ extern SF_UINT8 const sfWasLockingChainSend; 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; @@ -459,6 +460,10 @@ extern SF_UINT64 const sfReferenceCount; extern SF_UINT64 const sfXChainClaimID; extern SF_UINT64 const sfXChainAccountCreateCount; extern SF_UINT64 const sfXChainAccountClaimCount; +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; @@ -469,6 +474,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; @@ -554,6 +562,7 @@ extern SF_VL const sfMemoData; extern SF_VL const sfMemoFormat; extern SF_VL const sfDIDDocument; extern SF_VL const sfData; +extern SF_VL const sfMPTokenMetadata; // variable length (uncommon) extern SF_VL const sfFulfillment; @@ -577,6 +586,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; @@ -636,6 +646,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/src/ripple/protocol/STAmount.h b/src/ripple/protocol/STAmount.h index 1de1568ae03..4b60b945291 100644 --- a/src/ripple/protocol/STAmount.h +++ b/src/ripple/protocol/STAmount.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -70,8 +71,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; @@ -148,6 +151,7 @@ class STAmount final : public STBase, public CountedObject // Legacy support for new-style amounts STAmount(IOUAmount const& amount, Issue const& issue); STAmount(XRPAmount const& amount); + STAmount(MPTAmount const& amount, Issue const& issue); operator Number() const; //-------------------------------------------------------------------------- @@ -162,6 +166,15 @@ class STAmount final : public STBase, public CountedObject bool native() const noexcept; + bool + isMPT() const noexcept; + + bool + isIOU() const noexcept; + + std::string + getTypeName() const noexcept; + bool negative() const noexcept; @@ -265,6 +278,8 @@ class STAmount final : public STBase, public CountedObject xrp() const; IOUAmount iou() const; + MPTAmount + mpt() const; private: static std::unique_ptr @@ -334,6 +349,20 @@ STAmount::native() const noexcept return mIsNative; } +inline bool +STAmount::isMPT() const noexcept +{ + // MPT TODO + return false; //! mIsNative && mIssue.isMPT(); +} + +inline bool +STAmount::isIOU() const noexcept +{ + // MPT TODO + return !mIsNative; //&& !mIssue.isMPT(); +} + inline bool STAmount::negative() const noexcept { @@ -385,6 +414,9 @@ inline STAmount::operator Number() const { if (mIsNative) return xrp(); + // TODO MPT + // if (mIssue.isMPT()) + // return mpt(); return iou(); } @@ -552,6 +584,13 @@ isXRP(STAmount const& amount) return isXRP(amount.issue().currency); } +inline bool +isMPT(STAmount const& amount) +{ + // TODO MPT + return false; // isMPT(amount.issue()); +} + // Since `canonicalize` does not have access to a ledger, this is needed to put // the low-level routine stAmountCanonicalize on an amendment switch. Only // transactions need to use this switchover. Outside of a transaction it's safe diff --git a/src/ripple/protocol/STBitString.h b/src/ripple/protocol/STBitString.h index decdfa64861..8eb689ee090 100644 --- a/src/ripple/protocol/STBitString.h +++ b/src/ripple/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/src/ripple/protocol/STObject.h b/src/ripple/protocol/STObject.h index 5476cd01198..f25c5fdf25b 100644 --- a/src/ripple/protocol/STObject.h +++ b/src/ripple/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/src/ripple/protocol/Serializer.h b/src/ripple/protocol/Serializer.h index e3ef00eaf65..1375abf23f1 100644 --- a/src/ripple/protocol/Serializer.h +++ b/src/ripple/protocol/Serializer.h @@ -373,6 +373,12 @@ class SerialIter return getBitString<160>(); } + uint192 + get192() + { + return getBitString<192>(); + } + uint256 get256() { diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 61028d60e9d..8e8d8097fc7 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -124,6 +124,7 @@ enum TEMcodes : TERUnderlyingType { temSEQ_AND_TICKET, temBAD_NFTOKEN_TRANSFER_FEE, + temBAD_MPTOKEN_TRANSFER_FEE, temBAD_AMM_TOKENS, @@ -330,7 +331,11 @@ enum TECcodes : TERUnderlyingType { tecXCHAIN_SELF_COMMIT = 184, tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR = 185, tecXCHAIN_CREATE_ACCOUNT_DISABLED = 186, - tecEMPTY_DID = 187 + tecEMPTY_DID = 187, + tecMPTOKEN_EXISTS = 188, + tecMPT_MAX_AMOUNT_EXCEEDED = 189, + tecMPT_LOCKED = 190 + }; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/TxFlags.h b/src/ripple/protocol/TxFlags.h index ba2b97562db..10f43c070e1 100644 --- a/src/ripple/protocol/TxFlags.h +++ b/src/ripple/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/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index b12547b0a67..dd78b51c33e 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -190,6 +190,17 @@ enum TxType : std::uint16_t /** This transaction type deletes a DID */ ttDID_DELETE = 50, + /** This transaction creates a new MPTokenIssuance object. */ + ttMPTOKEN_ISSUANCE_CREATE = 51, + + /** This transaction destroys an existing MPTokenIssuance object. */ + ttMPTOKEN_ISSUANCE_DESTROY = 52, + + /** This transaction destroys an existing MPTokenIssuance object. */ + ttMPTOKEN_AUTHORIZE = 53, + + /** This transaction sets an existing MPTokenIssuance or MPToken object. */ + ttMPTOKEN_ISSUANCE_SET = 54, /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/src/ripple/protocol/UintTypes.h b/src/ripple/protocol/UintTypes.h index 6fb685551f8..14cb3ec90d5 100644 --- a/src/ripple/protocol/UintTypes.h +++ b/src/ripple/protocol/UintTypes.h @@ -46,6 +46,12 @@ class NodeIDTag explicit NodeIDTag() = default; }; +class MPTTag +{ +public: + explicit MPTTag() = default; +}; + } // namespace detail /** Directory is an index into the directory of offer books. @@ -58,6 +64,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 MPT = std::pair; + /** XRP currency. */ Currency const& xrpCurrency(); @@ -66,6 +75,10 @@ xrpCurrency(); Currency const& noCurrency(); +/** A placeholder for empty MPTID. */ +MPT const& +noMPT(); + /** We deliberately disallow the currency that looks like "XRP" because too many people were using it instead of the correct XRP currency. */ Currency const& diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index ab36983edd7..9973307c2f3 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -461,6 +461,7 @@ REGISTER_FEATURE(DID, Supported::yes, VoteBehavior::De REGISTER_FIX(fixFillOrKill, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixNFTokenReserve, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX(fixInnerObjTemplate, Supported::yes, 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/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 74f6b6492de..020f9c9bf27 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -72,6 +72,9 @@ enum class LedgerNameSpace : std::uint16_t { XCHAIN_CLAIM_ID = 'Q', XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K', DID = 'I', + MPTOKEN_ISSUANCE = '~', + MPTOKEN = 't', + MPT_DIR = 'k', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -134,6 +137,16 @@ getTicketIndex(AccountID const& account, SeqProxy ticketSeq) return getTicketIndex(account, ticketSeq.value()); } +uint192 +getMptID(AccountID const& account, std::uint32_t sequence) +{ + uint192 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 { @@ -444,6 +457,57 @@ did(AccountID const& account) noexcept return {ltDID, indexHash(LedgerNameSpace::DID, account)}; } +Keylet +mptIssuance(AccountID const& issuer, std::uint32_t seq) noexcept +{ + return mptIssuance(getMptID(issuer, seq)); +} + +Keylet +mptIssuance(ripple::MPT const& mpt) noexcept +{ + return mptIssuance(mpt.second, mpt.first); +} + +Keylet +mptIssuance(uint192 const& mpt) noexcept +{ + return { + ltMPTOKEN_ISSUANCE, indexHash(LedgerNameSpace::MPTOKEN_ISSUANCE, mpt)}; +} + +Keylet +mptoken(uint192 const& issuanceID, AccountID const& holder) noexcept +{ + return mptoken(mptIssuance(issuanceID).key, holder); +} + +Keylet +mptoken(MPT const& mptID, AccountID const& holder) noexcept +{ + return mptoken( + mptIssuance(getMptID(mptID.second, mptID.first)).key, holder); +} + +Keylet +mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept +{ + return { + ltMPTOKEN, indexHash(LedgerNameSpace::MPTOKEN, issuanceKey, holder)}; +} + +Keylet +mpt_dir(uint192 const& id) noexcept +{ + return { + ltDIR_NODE, indexHash(LedgerNameSpace::MPT_DIR, mptIssuance(id).key)}; +} + +Keylet +mpt_dir(uint256 const& id) noexcept +{ + return {ltDIR_NODE, indexHash(LedgerNameSpace::MPT_DIR, id)}; +} } // namespace keylet } // namespace ripple diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 729ddc1c7bc..d7f3387b47e 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -339,6 +339,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/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 027c8ffb9c5..3b3f309d382 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -97,6 +97,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); @@ -188,6 +189,10 @@ CONSTRUCT_TYPED_SFIELD(sfReferenceCount, "ReferenceCount", U CONSTRUCT_TYPED_SFIELD(sfXChainClaimID, "XChainClaimID", UINT64, 20); CONSTRUCT_TYPED_SFIELD(sfXChainAccountCreateCount, "XChainAccountCreateCount", UINT64, 21); CONSTRUCT_TYPED_SFIELD(sfXChainAccountClaimCount, "XChainAccountClaimCount", UINT64, 22); +CONSTRUCT_TYPED_SFIELD(sfMaximumAmount, "MaximumAmount", UINT64, 23); +CONSTRUCT_TYPED_SFIELD(sfOutstandingAmount, "OutstandingAmount", UINT64, 24); +CONSTRUCT_TYPED_SFIELD(sfLockedAmount, "LockedAmount", UINT64, 25); +CONSTRUCT_TYPED_SFIELD(sfMPTAmount, "MPTAmount", UINT64, 26); // 128-bit CONSTRUCT_TYPED_SFIELD(sfEmailHash, "EmailHash", UINT128, 1); @@ -198,6 +203,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); @@ -300,6 +308,7 @@ CONSTRUCT_TYPED_SFIELD(sfHookParameterName, "HookParameterName", VL, CONSTRUCT_TYPED_SFIELD(sfHookParameterValue, "HookParameterValue", VL, 25); CONSTRUCT_TYPED_SFIELD(sfDIDDocument, "DIDDocument", VL, 26); CONSTRUCT_TYPED_SFIELD(sfData, "Data", VL, 27); +CONSTRUCT_TYPED_SFIELD(sfMPTokenMetadata, "MPTokenMetadata", VL, 28); // account CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1); @@ -312,6 +321,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/ripple/protocol/impl/STAmount.cpp b/src/ripple/protocol/impl/STAmount.cpp index 201bcb6b681..6d2337777e6 100644 --- a/src/ripple/protocol/impl/STAmount.cpp +++ b/src/ripple/protocol/impl/STAmount.cpp @@ -78,26 +78,57 @@ getSNValue(STAmount const& amount) return ret; } +static std::int64_t +getMPTValue(STAmount const& amount) +{ + if (!amount.isMPT()) + Throw("amount is not native!"); + + 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() && + // TODO MPT + return (v1.native() == v2.native() || v1.isMPT() == v2.isMPT()) && v1.issue().currency == v2.issue().currency; } STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name) { + // TODO MPT make sure backward compatible + std::uint64_t value = sit.get64(); + // TODO must fix serialization for IOU, it incorrectly sets cMPToken + bool isMPT = (value & cMPToken) && !(value & cIssuedCurrency); - // native - if ((value & cNotNative) == 0) + // native or MPT + if ((value & cIssuedCurrency) == 0 || isMPT) { + // TODO MPT +#if 0 + if (isMPT) + { + mIssue = std::make_pair( + sit.get32(), static_cast(sit.get160())); + } + else +#endif + mIssue = xrpIssue(); // positive - if ((value & cPosNative) != 0) + if ((value & cPositive) != 0) { - mValue = value & ~cPosNative; + mValue = value & cValueMask; mOffset = 0; - mIsNative = true; + mIsNative = !isMPT; mIsNegative = false; return; } @@ -106,9 +137,9 @@ 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; + mIsNative = !isMPT; mIsNegative = true; return; } @@ -167,7 +198,7 @@ STAmount::STAmount( bool negative, unchecked) : STBase(name) - , mIssue(issue) + , mIssue(native ? xrpIssue() : issue) , mValue(mantissa) , mOffset(exponent) , mIsNative(native) @@ -182,7 +213,7 @@ STAmount::STAmount( bool native, bool negative, unchecked) - : mIssue(issue) + : mIssue(native ? xrpIssue() : issue) , mValue(mantissa) , mOffset(exponent) , mIsNative(native) @@ -198,7 +229,7 @@ STAmount::STAmount( bool native, bool negative) : STBase(name) - , mIssue(issue) + , mIssue(native ? xrpIssue() : issue) , mValue(mantissa) , mOffset(exponent) , mIsNative(native) @@ -208,13 +239,14 @@ STAmount::STAmount( } STAmount::STAmount(SField const& name, std::int64_t mantissa) - : STBase(name), mOffset(0), mIsNative(true) + : STBase(name), mIssue(xrpIssue()), mOffset(0), mIsNative(true) { set(mantissa); } STAmount::STAmount(SField const& name, std::uint64_t mantissa, bool negative) : STBase(name) + , mIssue(xrpIssue()) , mValue(mantissa) , mOffset(0) , mIsNative(true) @@ -253,7 +285,8 @@ STAmount::STAmount(SField const& name, STAmount const& from) //------------------------------------------------------------------------------ STAmount::STAmount(std::uint64_t mantissa, bool negative) - : mValue(mantissa) + : mIssue(xrpIssue()) + , mValue(mantissa) , mOffset(0) , mIsNative(true) , mIsNegative(mantissa != 0 && negative) @@ -308,7 +341,10 @@ STAmount::STAmount(IOUAmount const& amount, Issue const& issue) } STAmount::STAmount(XRPAmount const& amount) - : mOffset(0), mIsNative(true), mIsNegative(amount < beast::zero) + : mIssue(xrpIssue()) + , mOffset(0) + , mIsNative(true) + , mIsNegative(amount < beast::zero) { if (mIsNegative) mValue = unsafe_cast(-amount.drops()); @@ -318,6 +354,20 @@ STAmount::STAmount(XRPAmount const& amount) canonicalize(); } +STAmount::STAmount(MPTAmount const& amount, Issue const& issue) + : mIssue(issue) + , mOffset(0) + , mIsNative(false) + , mIsNegative(amount < beast::zero) +{ + if (mIsNegative) + mValue = unsafe_cast(-amount.mpt()); + else + mValue = unsafe_cast(amount.mpt()); + + canonicalize(); +} + std::unique_ptr STAmount::construct(SerialIter& sit, SField const& name) { @@ -359,7 +409,7 @@ STAmount::xrp() const IOUAmount STAmount::iou() const { - if (mIsNative) + if (mIsNative || isMPT()) Throw("Cannot return native STAmount as IOUAmount"); auto mantissa = static_cast(mValue); @@ -371,6 +421,20 @@ STAmount::iou() const return {mantissa, exponent}; } +MPTAmount +STAmount::mpt() const +{ + if (!isMPT()) + Throw("Cannot return STAmount as MPTAmount"); + + auto value = static_cast(mValue); + + if (mIsNegative) + value = -value; + + return MPTAmount{value}; +} + STAmount& STAmount::operator=(IOUAmount const& iou) { @@ -424,8 +488,11 @@ operator+(STAmount const& v1, STAmount const& v2) v2.negative()}; } + // TODO if (v1.native()) return {v1.getFName(), getSNValue(v1) + getSNValue(v2)}; + if (v1.isMPT()) + return {v1.mIssue, v1.mpt().mpt() + v2.mpt().mpt()}; if (getSTNumberSwitchover()) { @@ -534,8 +601,14 @@ STAmount::setJson(Json::Value& elem) const // 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); + // TODO MPT + // if (mIssue.isMPT()) + // elem[jss::mpt_issuance_id] = to_string(mIssue.asset()); + // else + { + elem[jss::currency] = to_string(mIssue.currency); + elem[jss::issuer] = to_string(mIssue.account); + } } else { @@ -581,7 +654,8 @@ STAmount::getText() const bool const scientific( (mOffset != 0) && ((mOffset < -25) || (mOffset > -5))); - if (mIsNative || scientific) + // TODO MPT + if (mIsNative || /*mIssue.isMPT() ||*/ scientific) { ret.append(raw_value); @@ -660,30 +734,45 @@ Json::Value STAmount::getJson(JsonOptions) const void STAmount::add(Serializer& s) const { + // TODO MPT make sure backward compatible if (mIsNative) { assert(mOffset == 0); if (!mIsNegative) - s.add64(mValue | cPosNative); + s.add64(mValue | cPositive); else s.add64(mValue); } else { - if (*this == beast::zero) - s.add64(cNotNative); - else if (mIsNegative) // 512 = not native - s.add64( - mValue | - (static_cast(mOffset + 512 + 97) << (64 - 10))); - else // 256 = positive - s.add64( - mValue | - (static_cast(mOffset + 512 + 256 + 97) - << (64 - 10))); - - s.addBitString(mIssue.currency); + // TODO MPT +#if 0 + if (mIssue.isMPT()) + { + if (mIsNegative) + s.add64(mValue | cMPToken); + else + s.add64(mValue | cMPToken | cPositive); + s.add32(std::get(mIssue.asset().asset()).first); + } + else +#endif + { + if (*this == beast::zero) + s.add64(cIssuedCurrency); + else if (mIsNegative) // 512 = not native + s.add64( + mValue | + (static_cast(mOffset + 512 + 97) + << (64 - 10))); + else // 256 = positive + s.add64( + mValue | + (static_cast(mOffset + 512 + 256 + 97) + << (64 - 10))); + s.addBitString(mIssue.currency); + } s.addBitString(mIssue.account); } } @@ -722,10 +811,11 @@ STAmount::isDefault() const void STAmount::canonicalize() { - if (isXRP(*this)) + // TODO MPT + if (isXRP(*this) /*|| mIssue.isMPT()*/) { // native currency amounts should always have an offset of zero - mIsNative = true; + mIsNative = isXRP(*this); // log(2^64,10) ~ 19.2 if (mValue == 0 || mOffset <= -20) @@ -748,9 +838,21 @@ STAmount::canonicalize() { Number num( mIsNegative ? -mValue : mValue, mOffset, Number::unchecked{}); - XRPAmount xrp{num}; - mIsNegative = xrp.drops() < 0; - mValue = mIsNegative ? -xrp.drops() : xrp.drops(); + if (mIsNative) + { + XRPAmount xrp{num}; + mIsNegative = xrp.drops() < 0; + mValue = mIsNegative ? -xrp.drops() : xrp.drops(); + } + else + { + // TODO MPT +#if 0 + MPTAmount c{num}; + mIsNegative = c.mpt() < 0; + mValue = mIsNegative ? -c.mpt() : c.mpt(); +#endif + } mOffset = 0; } else @@ -932,8 +1034,9 @@ amountFromJson(SField const& name, Json::Value const& v) Issue issue; Json::Value value; - Json::Value currency; + Json::Value asset; Json::Value issuer; + bool isMPT = false; if (v.isNull()) { @@ -943,13 +1046,21 @@ amountFromJson(SField const& name, Json::Value const& v) else if (v.isObject()) { value = v[jss::value]; - currency = v[jss::currency]; - issuer = v[jss::issuer]; + if (v.isMember(jss::mpt_issuance_id)) + { + isMPT = true; + asset = v[jss::mpt_issuance_id]; + } + else + { + asset = 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); + asset = v.get(Json::UInt(1), Json::nullValue); issuer = v.get(Json::UInt(2), Json::nullValue); } else if (v.isString()) @@ -964,7 +1075,7 @@ amountFromJson(SField const& name, Json::Value const& v) value = elements[0]; if (elements.size() > 1) - currency = elements[1]; + asset = elements[1]; if (elements.size() > 2) issuer = elements[2]; @@ -974,8 +1085,8 @@ amountFromJson(SField const& name, Json::Value const& v) value = v; } - bool const native = !currency.isString() || currency.asString().empty() || - (currency.asString() == systemCurrencyCode()); + bool const native = !asset.isString() || asset.asString().empty() || + (asset.asString() == systemCurrencyCode()); if (native) { @@ -985,14 +1096,26 @@ amountFromJson(SField const& name, Json::Value const& v) } 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)) + // TODO MPT +#if 0 + if (isMPT) + { + // sequence (32 bits) + account (160 bits) + uint192 u; + if (!u.parseHex(asset.asString())) + Throw("invalid MPTokenIssuanceID"); + issue = u; + } + else +#endif + { + if (!to_currency(issue.currency, asset.asString())) + Throw("invalid currency"); + if (!issuer.isString() || + !to_issuer(issue.account, issuer.asString())) + Throw("invalid issuer"); + } + if (isXRP(issue)) Throw("invalid issuer"); } @@ -1174,7 +1297,7 @@ divide(STAmount const& num, STAmount const& den, Issue const& issue) int numOffset = num.exponent(); int denOffset = den.exponent(); - if (num.native()) + if (num.native() || num.isMPT()) { while (numVal < STAmount::cMinValue) { @@ -1184,7 +1307,7 @@ divide(STAmount const& num, STAmount const& den, Issue const& issue) } } - if (den.native()) + if (den.native() || den.isMPT()) { while (denVal < STAmount::cMinValue) { @@ -1226,6 +1349,24 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) return STAmount(v1.getFName(), minV * maxV); } + // TODO MPT + if (v1.isMPT() && v2.isMPT() /*&& issue.isMPT()*/) + { + 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(issue, minV * maxV); + } if (getSTNumberSwitchover()) return {IOUAmount{Number{v1} * Number{v2}}, issue}; @@ -1235,7 +1376,8 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) int offset1 = v1.exponent(); int offset2 = v2.exponent(); - if (v1.native()) + // TODO MPT + if (v1.native() || v1.isMPT()) { while (value1 < STAmount::cMinValue) { @@ -1244,7 +1386,7 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) } } - if (v2.native()) + if (v2.native() || v2.isMPT()) { while (value2 < STAmount::cMinValue) { @@ -1423,6 +1565,7 @@ mulRoundImpl( bool const xrp = isXRP(issue); + // TODO MPT if (v1.native() && v2.native() && xrp) { std::uint64_t minV = @@ -1438,11 +1581,30 @@ mulRoundImpl( return STAmount(v1.getFName(), minV * maxV); } + // TODO MPT + if (v1.isMPT() && v2.isMPT() /*&& issue.isMPT()*/) + { + 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(issue, minV * maxV); + } std::uint64_t value1 = v1.mantissa(), value2 = v2.mantissa(); int offset1 = v1.exponent(), offset2 = v2.exponent(); - if (v1.native()) + // TODO MPT + if (v1.native() || v1.isMPT()) { while (value1 < STAmount::cMinValue) { @@ -1451,7 +1613,8 @@ mulRoundImpl( } } - if (v2.native()) + // TODO MPT + if (v2.native() || v2.isMPT()) { while (value2 < STAmount::cMinValue) { @@ -1545,7 +1708,8 @@ divRoundImpl( std::uint64_t numVal = num.mantissa(), denVal = den.mantissa(); int numOffset = num.exponent(), denOffset = den.exponent(); - if (num.native()) + // TODO MPT + if (num.native() || num.isMPT()) { while (numVal < STAmount::cMinValue) { @@ -1554,7 +1718,8 @@ divRoundImpl( } } - if (den.native()) + // TODO MPT + if (den.native() || den.isMPT()) { while (denVal < STAmount::cMinValue) { @@ -1578,8 +1743,10 @@ divRoundImpl( int offset = numOffset - denOffset - 17; + // TODO MPT if (resultNegative != roundUp) - canonicalizeRound(isXRP(issue), amount, offset, roundUp); + canonicalizeRound( + isXRP(issue) /*|| issue.isMPT()*/, amount, offset, roundUp); STAmount result = [&]() { // If appropriate, tell Number the rounding mode we are using. @@ -1593,7 +1760,8 @@ divRoundImpl( if (roundUp && !resultNegative && !result) { - if (isXRP(issue)) + // TODO MPT + if (isXRP(issue) /*|| issue.isMPT()*/) { // return the smallest value above zero amount = 1; diff --git a/src/ripple/protocol/impl/STObject.cpp b/src/ripple/protocol/impl/STObject.cpp index 7c546a2568e..f35506d48ff 100644 --- a/src/ripple/protocol/impl/STObject.cpp +++ b/src/ripple/protocol/impl/STObject.cpp @@ -594,6 +594,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/ripple/protocol/impl/STParsedJSON.cpp b/src/ripple/protocol/impl/STParsedJSON.cpp index fb960e6f11e..6a5850f4fe4 100644 --- a/src/ripple/protocol/impl/STParsedJSON.cpp +++ b/src/ripple/protocol/impl/STParsedJSON.cpp @@ -454,6 +454,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/ripple/protocol/impl/STTx.cpp b/src/ripple/protocol/impl/STTx.cpp index 51fb11ad761..2bc436db0ef 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -529,15 +529,21 @@ isMemoOkay(STObject const& st, std::string& reason) return true; } -// Ensure all account fields are 160-bits +// Ensure all account fields are 160-bits and that MPT amount is only passed +// to Payment tx (until MPT is supported in more tx) static bool -isAccountFieldOkay(STObject const& st) +isAccountAndMPTFieldOkay(STObject const& st) { + auto const txType = st[~sfTransactionType]; + bool const isPaymentTx = txType && safe_cast(*txType) == ttPAYMENT; for (int i = 0; i < st.getCount(); ++i) { auto t = dynamic_cast(st.peekAtPIndex(i)); if (t && t->isDefault()) return false; + auto amt = dynamic_cast(st.peekAtPIndex(i)); + if (amt && amt->isMPT() && !isPaymentTx) + return false; } return true; @@ -549,9 +555,9 @@ passesLocalChecks(STObject const& st, std::string& reason) if (!isMemoOkay(st, reason)) return false; - if (!isAccountFieldOkay(st)) + if (!isAccountAndMPTFieldOkay(st)) { - reason = "An account field is invalid."; + reason = "An account or MPT field is invalid."; return false; } diff --git a/src/ripple/protocol/impl/STVar.cpp b/src/ripple/protocol/impl/STVar.cpp index 2ec55ccaf03..0318b2f5127 100644 --- a/src/ripple/protocol/impl/STVar.cpp +++ b/src/ripple/protocol/impl/STVar.cpp @@ -140,6 +140,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; @@ -201,6 +204,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/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index 5f608e806ab..9eaf86f5f76 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -111,6 +111,9 @@ transResults() MAKE_ERROR(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR, "Bad public key account pair in an xchain transaction."), MAKE_ERROR(tecXCHAIN_CREATE_ACCOUNT_DISABLED, "This bridge does not support account creation."), MAKE_ERROR(tecEMPTY_DID, "The DID object did not have a URI or DIDDocument field."), + 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(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 7be8ca741e2..5e8f5a2ce0d 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -483,6 +483,39 @@ TxFormats::TxFormats() commonFields); add(jss::DIDDelete, ttDID_DELETE, {}, 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/ripple/protocol/impl/UintTypes.cpp b/src/ripple/protocol/impl/UintTypes.cpp index 821e81238b0..0938401eb32 100644 --- a/src/ripple/protocol/impl/UintTypes.cpp +++ b/src/ripple/protocol/impl/UintTypes.cpp @@ -125,6 +125,13 @@ noCurrency() return currency; } +MPT const& +noMPT() +{ + static MPT const mpt{0, noAccount()}; + return mpt; +} + Currency const& badCurrency() { diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index fd1c94c67d2..49fa8029700 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -41,101 +41,108 @@ 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(AuthAccount); // in: AMM Auction Slot -JSS(AuthAccounts); // in: AMM Auction Slot -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(incomplete_shards); // out: OverlayImpl, PeerImp -JSS(Invalid); // -JSS(LastLedgerSequence); // in: TransactionSign; 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(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(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(RippleState); // ledger type. -JSS(SLE_hit_rate); // out: GetCounts. -JSS(SetFee); // transaction type. -JSS(UNLModify); // transaction type. -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(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(AuthAccount); // in: AMM Auction Slot +JSS(AuthAccounts); // in: AMM Auction Slot +JSS(Bridge); // ledger type. +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(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(incomplete_shards); // out: OverlayImpl, PeerImp +JSS(Invalid); // +JSS(LastLedgerSequence); // in: TransactionSign; 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(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(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(RippleState); // ledger type. +JSS(SLE_hit_rate); // out: GetCounts. +JSS(SetFee); // transaction type. +JSS(UNLModify); // transaction type. +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(VoteSlots); // out: AMM Vote JSS(XChainAddAccountCreateAttestation); // transaction type. JSS(XChainAddClaimAttestation); // transaction type. JSS(XChainAccountCreateCommit); // transaction type. @@ -220,6 +227,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 @@ -351,6 +363,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. @@ -438,6 +451,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/ripple/rpc/MPTokenIssuanceID.h b/src/ripple/rpc/MPTokenIssuanceID.h new file mode 100644 index 00000000000..7a06048229c --- /dev/null +++ b/src/ripple/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/ripple/rpc/handlers/AccountObjects.cpp b/src/ripple/rpc/handlers/AccountObjects.cpp index bbf5b6e126a..29674b11ce6 100644 --- a/src/ripple/rpc/handlers/AccountObjects.cpp +++ b/src/ripple/rpc/handlers/AccountObjects.cpp @@ -201,7 +201,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/ripple/rpc/handlers/AccountTx.cpp b/src/ripple/rpc/handlers/AccountTx.cpp index 40395aae32f..2ee748babdb 100644 --- a/src/ripple/rpc/handlers/AccountTx.cpp +++ b/src/ripple/rpc/handlers/AccountTx.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include @@ -361,6 +362,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/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h index ba93be54513..7ffcb00d504 100644 --- a/src/ripple/rpc/handlers/Handlers.h +++ b/src/ripple/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/ripple/rpc/handlers/LedgerData.cpp b/src/ripple/rpc/handlers/LedgerData.cpp index f5433945772..5fa055e2792 100644 --- a/src/ripple/rpc/handlers/LedgerData.cpp +++ b/src/ripple/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/ripple/rpc/handlers/LedgerEntry.cpp b/src/ripple/rpc/handlers/LedgerEntry.cpp index baff721cc1f..c7f629a329f 100644 --- a/src/ripple/rpc/handlers/LedgerEntry.cpp +++ b/src/ripple/rpc/handlers/LedgerEntry.cpp @@ -598,6 +598,73 @@ doLedgerEntry(RPC::JsonContext& context) else uNodeIndex = keylet::did(*account).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/ripple/rpc/handlers/Tx.cpp b/src/ripple/rpc/handlers/Tx.cpp index 0237fef22ac..167749a99dc 100644 --- a/src/ripple/rpc/handlers/Tx.cpp +++ b/src/ripple/rpc/handlers/Tx.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -385,6 +386,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; diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index d05c3279800..3c9cb463def 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -95,6 +95,7 @@ Handler const handlerArray[]{ {"book_changes", byRef(&doBookChanges), Role::USER, NO_CONDITION}, {"book_offers", byRef(&doBookOffers), Role::USER, NO_CONDITION}, {"can_delete", byRef(&doCanDelete), Role::ADMIN, NO_CONDITION}, + //{"mpt_holders", byRef(&doMPTHolders), Role::ADMIN, NO_CONDITION}, {"channel_authorize", byRef(&doChannelAuthorize), Role::USER, NO_CONDITION}, {"channel_verify", byRef(&doChannelVerify), Role::USER, NO_CONDITION}, {"connect", byRef(&doConnect), Role::ADMIN, NO_CONDITION}, diff --git a/src/ripple/rpc/impl/MPTokenIssuanceID.cpp b/src/ripple/rpc/impl/MPTokenIssuanceID.cpp new file mode 100644 index 00000000000..545dddd282f --- /dev/null +++ b/src/ripple/rpc/impl/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/ripple/rpc/impl/RPCHelpers.cpp b/src/ripple/rpc/impl/RPCHelpers.cpp index 5e7300adea8..054d782d0a1 100644 --- a/src/ripple/rpc/impl/RPCHelpers.cpp +++ b/src/ripple/rpc/impl/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) @@ -934,7 +940,7 @@ chooseLedgerEntryType(Json::Value const& params) std::pair result{RPC::Status::OK, ltANY}; if (params.isMember(jss::type)) { - static constexpr std::array, 20> + static constexpr std::array, 22> types{ {{jss::account, ltACCOUNT_ROOT}, {jss::amendments, ltAMENDMENTS}, @@ -956,7 +962,9 @@ chooseLedgerEntryType(Json::Value const& params) {jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID}, {jss::xchain_owned_create_account_claim_id, ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, - {jss::did, ltDID}}}; + {jss::did, ltDID}, + {jss::mpt_issuance, ltMPTOKEN_ISSUANCE}, + {jss::mptoken, ltMPTOKEN}}}; auto const& p = params[jss::type]; if (!p.isString()) diff --git a/src/ripple/rpc/impl/Tuning.h b/src/ripple/rpc/impl/Tuning.h index 4f4a8be1bf7..d4557a9fcfa 100644 --- a/src/ripple/rpc/impl/Tuning.h +++ b/src/ripple/rpc/impl/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/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp new file mode 100644 index 00000000000..bcd62d94fb0 --- /dev/null +++ b/src/test/app/MPToken_test.cpp @@ -0,0 +1,911 @@ +//------------------------------------------------------------------------------ +/* + 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 + +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 while not enabling transfer + mptAlice.create( + {.maxAmt = 100, + .assetScale = 0, + .transferFee = maxTransferFee + 1, + .metadata = "test", + .flags = tfMPTCanTransfer, + .err = temBAD_MPTOKEN_TRANSFER_FEE}); + + // 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 = 0xFFFFFFFFFFFFFFF0ull, + .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 = 0x7FFFFFFFFFFFFFFF, + .assetScale = 1, + .transferFee = 10, + .metadata = "123", + .ownerCount = 1, + .flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | + tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback}); + } + } + + 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 + // TODO MPT +#if 0 + mptAlice.pay(alice, bob, 100); + + mptAlice.destroy({.err = tecHAS_OBLIGATIONS}); +#endif + } + } + } + + 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 = temMALFORMED}); + + mptAlice.authorize( + {.account = &bob, .holder = &bob, .err = temMALFORMED}); + + // alice tries to hold onto her own token + mptAlice.authorize({.account = &alice, .err = temMALFORMED}); + + // alice tries to authorize herself + mptAlice.authorize({.holder = &alice, .err = temMALFORMED}); + + // 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 + { + // TODO MPT +#if 0 + // 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); +#endif + } + + // 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 = temMALFORMED}); + + // 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}); + + // 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); + } + + // 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}); + + 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}); + + // 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}); + + 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}); + + 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}); + + 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); + } + + // TODO: This test is currently failing! Modify the STAmount to change + // the range + // 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); + // } + + // 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); + } + } + + void + testMPTInvalidInTx(FeatureBitset features) + { + testcase("MPT Amount Invalid in Transaction"); + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"); // issuer + + MPTTester mptAlice(env, alice); + + mptAlice.create(); + + env(offer(alice, mptAlice.mpt(100), XRP(100)), ter(temINVALID)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + } + + 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())); + } + +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); + + // TODO MPT +#if 0 + // Test Direct Payment + testPayment(all); + + // Test MPT Amount is invalid in non-Payment Tx + testMPTInvalidInTx(all); +#endif + + // Test parsed MPTokenIssuanceID in API response metadata + // TODO: This test exercises the parsing logic of mptID in `tx`, + // but, + // mptID is also parsed in different places like `account_tx`, + // `subscribe`, `ledger`. We should create test for these + // occurances (lower prioirity). + testTxJsonMetaFields(all); + } +}; + +BEAST_DEFINE_TESTSUITE_PRIO(MPToken, tx, ripple, 2); + +} // namespace ripple diff --git a/src/test/jtx.h b/src/test/jtx.h index 03bbf154e63..6583cb51722 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 74e9e057de8..ac0cd9ad46d 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -432,6 +432,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 f8a8f67c75e..9ab6db6c7a3 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -351,6 +351,76 @@ 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::MPT mptID; + + MPT(std::string const& n, ripple::MPT const& mptID_) + : name(n), mptID(mptID_) + { + } + + Issue + issue() const + { + // TODO MPT + return noIssue(); //{mptID}; + } + + /** Implicit conversion to Issue. + + This allows passing an MPT + value where an Issue is expected. + */ + operator Issue() const + { + return issue(); + } + + 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(issue(), 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 */ + None operator()(none_t) const + { + return {issue()}; + } + + 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..62a7abfbf48 --- /dev/null +++ b/src/test/jtx/impl/mpt.cpp @@ -0,0 +1,387 @@ +//------------------------------------------------------------------------------ +/* + 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 { + +static std::array +uint64ToByteArray(std::uint64_t value) +{ + value = boost::endian::native_to_big(value); + std::array result; + std::memcpy(result.data(), &value, sizeof(value)); + return result; +} + +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"); + mpt_ = std::make_pair(env_.seq(issuer_), issuer_.id()); + id_ = getMptID(issuer_.id(), mpt_->first); + 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); + + // convert maxAmt to hex string, since json doesn't accept 64-bit int + if (arg.maxAmt) + jv[sfMaximumAmount.jsonName] = strHex(uint64ToByteArray(*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(); + mpt_.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) + { + 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::uint64_t expectedAmount) const +{ + return forObject( + [&](SLEP const& sle) { return expectedAmount == (*sle)[sfMPTAmount]; }, + &holder_); +} + +[[nodiscard]] bool +MPTTester::checkMPTokenOutstandingAmount(std::uint64_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::uint64_t amount, + std::optional err) +{ + assert(mpt_); + 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 + { + // TODO MPT +#if 0 + STAmount const saAmount = {Issue{*mpt_}, amount}; + STAmount const saActual = + multiply(saAmount, transferRateMPT(*env_.current(), *mpt_)); + // Sender pays the transfer fee if any + env_.require(mptpay(*this, src, srcAmt - saActual.mpt().mpt())); + 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().mpt())); +#endif + } +} + +PrettyAmount +MPTTester::mpt(std::uint64_t amount) const +{ + assert(mpt_); + return ripple::test::jtx::MPT(issuer_.name(), *mpt_)(amount); +} + +std::uint64_t +MPTTester::getAmount(Account const& account) const +{ + 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; + assert(forObject( + [&](SLEP const& sle) { + flags = sle->getFlags(); + return true; + }, + holder)); + return flags; +} + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h new file mode 100644 index 00000000000..12b384c2902 --- /dev/null +++ b/src/test/jtx/mpt.h @@ -0,0 +1,258 @@ +//------------------------------------------------------------------------------ +/* + 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::uint64_t const amount_; + +public: + mptpay(MPTTester& tester, Account const& account, std::uint64_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_; + std::optional mpt_; + 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::uint64_t expectedAmount) + const; + + [[nodiscard]] bool + checkMPTokenOutstandingAmount(std::uint64_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::uint64_t amount, + std::optional err = std::nullopt); + + PrettyAmount + mpt(std::uint64_t amount) const; + + uint256 const& + issuanceKey() const + { + assert(issuanceKey_); + return *issuanceKey_; + } + + uint192 const& + issuanceID() const + { + assert(id_); + return *id_; + } + + std::uint64_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