diff --git a/src/ripple/app/misc/AMMHelpers.h b/src/ripple/app/misc/AMMHelpers.h index 24c25922800..5b3a6b3a373 100644 --- a/src/ripple/app/misc/AMMHelpers.h +++ b/src/ripple/app/misc/AMMHelpers.h @@ -134,7 +134,7 @@ withinRelativeDistance( template requires( std::is_same_v || std::is_same_v || - std::is_same_v) + std::is_same_v || std::is_same_v) bool withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist) { diff --git a/src/ripple/app/misc/AMMUtils.h b/src/ripple/app/misc/AMMUtils.h index c25503ceb9c..a6fc7dae4e9 100644 --- a/src/ripple/app/misc/AMMUtils.h +++ b/src/ripple/app/misc/AMMUtils.h @@ -62,8 +62,8 @@ ammHolds( STAmount ammLPHolds( ReadView const& view, - Currency const& cur1, - Currency const& cur2, + Asset const& cur1, + Asset const& cur2, AccountID const& ammAccount, AccountID const& lpAccount, beast::Journal const j); diff --git a/src/ripple/app/misc/impl/AMMUtils.cpp b/src/ripple/app/misc/impl/AMMUtils.cpp index 3c78fe6d6df..537a8a7463e 100644 --- a/src/ripple/app/misc/impl/AMMUtils.cpp +++ b/src/ripple/app/misc/impl/AMMUtils.cpp @@ -103,8 +103,8 @@ ammHolds( STAmount ammLPHolds( ReadView const& view, - Currency const& cur1, - Currency const& cur2, + Asset const& cur1, + Asset const& cur2, AccountID const& ammAccount, AccountID const& lpAccount, beast::Journal const j) @@ -173,8 +173,21 @@ ammAccountHolds( if (auto const sle = view.read(keylet::account(ammAccountID))) return (*sle)[sfBalance]; } - else if (auto const sle = view.read( - keylet::line(ammAccountID, issue.account(), issue.asset())); + else if (issue.isMPT()) + { + auto const cftokenID = keylet::mptoken( + keylet::mptIssuance(static_cast(issue.asset())).key, + ammAccountID); + if (auto const sle = view.read(cftokenID)) + return STAmount{ + issue, + sle->getFieldU64(sfMPTAmount) - + sle->getFieldU64(sfLockedAmount)}; + } + else if (auto const sle = view.read(keylet::line( + ammAccountID, + issue.account(), + static_cast(issue.asset()))); sle && !isFrozen(view, ammAccountID, issue.asset(), issue.account())) { diff --git a/src/ripple/app/paths/Flow.cpp b/src/ripple/app/paths/Flow.cpp index 7c76ec2e6b5..fedc71e63b0 100644 --- a/src/ripple/app/paths/Flow.cpp +++ b/src/ripple/app/paths/Flow.cpp @@ -55,6 +55,19 @@ finishFlow( return result; }; +static std::variant +getTypedAmt(Issue const& iss) +{ + static auto xrp = XRPAmount{}; + static auto cft = MPTAmount{}; + static auto iou = IOUAmount{}; + if (isXRP(iss)) + return &xrp; + if (iss.isMPT()) + return &cft; + return &iou; +} + path::RippleCalc::Output flow( PaymentSandbox& sb, @@ -128,106 +141,32 @@ flow( } } - const bool srcIsXRP = isXRP(srcIssue.asset()); - const bool dstIsXRP = isXRP(dstIssue.asset()); - - auto const asDeliver = toAmountSpec(deliver); - - // The src account may send either xrp or iou. The dst account may receive - // either xrp or iou. Since XRP and IOU amounts are represented by different - // types, use templates to tell `flow` about the amount types. - if (srcIsXRP && dstIsXRP) - { - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( - sb, - strands, - asDeliver.xrp, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); - } - - if (srcIsXRP && !dstIsXRP) - { - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( - sb, - strands, - asDeliver.iou, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); - } - - if (!srcIsXRP && dstIsXRP) - { - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( - sb, - strands, - asDeliver.xrp, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); - } - - assert(!srcIsXRP && !dstIsXRP); - if (srcIssue.asset().isMPT()) - { - assert(dstIssue.asset().isMPT()); - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( + // The src account may send either xrp,iou,cft. The dst account may receive + // either xrp,iou,cft. Since XRP and IOU amounts are represented by + // different types, use templates to tell `flow` about the amount types. + path::RippleCalc::Output result; + std::visit( + [&, &strands_ = strands]( + TIn const*&&, TOut const*&&) { + result = finishFlow( sb, - strands, - asDeliver.mpt, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); - } - return finishFlow( - sb, - srcIssue, - dstIssue, - flow( - sb, - strands, - asDeliver.iou, - partialPayment, - offerCrossing, - limitQuality, - sendMax, - j, - ammContext, - flowDebugInfo)); + srcIssue, + dstIssue, + flow( + sb, + strands_, + get(deliver), + partialPayment, + offerCrossing, + limitQuality, + sendMax, + j, + ammContext, + flowDebugInfo)); + }, + getTypedAmt(srcIssue), + getTypedAmt(dstIssue)); + return result; } } // namespace ripple diff --git a/src/ripple/app/paths/impl/AMMLiquidity.cpp b/src/ripple/app/paths/impl/AMMLiquidity.cpp index 3f22ebacec5..2618a32b7dc 100644 --- a/src/ripple/app/paths/impl/AMMLiquidity.cpp +++ b/src/ripple/app/paths/impl/AMMLiquidity.cpp @@ -99,6 +99,8 @@ maxAmount() { if constexpr (std::is_same_v) return XRPAmount(STAmount::cMaxNative); + if constexpr (std::is_same_v) + return MPTAmount(STAmount::cMaxNative); else if constexpr (std::is_same_v) return IOUAmount(STAmount::cMaxValue / 2, STAmount::cMaxOffset); else if constexpr (std::is_same_v) diff --git a/src/ripple/app/paths/impl/BookStep.cpp b/src/ripple/app/paths/impl/BookStep.cpp index 8e4ca28f3f5..d95f7dbeb21 100644 --- a/src/ripple/app/paths/impl/BookStep.cpp +++ b/src/ripple/app/paths/impl/BookStep.cpp @@ -684,31 +684,55 @@ BookStep::forEachOffer( if (flowCross && (!isXRP(offer.issueIn().asset())) && (offer.owner() != offer.issueIn().account())) { - auto const& issuerID = offer.issueIn().account(); - auto const issuer = afView.read(keylet::account(issuerID)); - if (issuer && ((*issuer)[sfFlags] & lsfRequireAuth)) + if (offer.issueIn().isMPT()) { - // Issuer requires authorization. See if offer owner has that. - auto const& ownerID = offer.owner(); - auto const authFlag = - issuerID > ownerID ? lsfHighAuth : lsfLowAuth; - - auto const line = afView.read( - keylet::line(ownerID, issuerID, offer.issueIn().asset())); - - if (!line || (((*line)[sfFlags] & authFlag) == 0)) + auto const mptokenID = keylet::mptoken( + keylet::mptIssuance( + static_cast(offer.issueIn().asset())) + .key, + offer.owner()); + if (!afView.exists(mptokenID)) { - // Offer owner not authorized to hold IOU from issuer. - // Remove this offer even if no crossing occurs. if (auto const key = offer.key()) offers.permRmOffer(*key); if (!offerAttempted) - // Change quality only if no previous offers were tried. ofrQ = std::nullopt; - // Returning true causes offers.step() to delete the offer. return true; } } + else + { + auto const& issuerID = offer.issueIn().account(); + auto const issuer = afView.read(keylet::account(issuerID)); + if (issuer && ((*issuer)[sfFlags] & lsfRequireAuth)) + { + // Issuer requires authorization. See if offer owner has + // that. + auto const& ownerID = offer.owner(); + auto const authFlag = + issuerID > ownerID ? lsfHighAuth : lsfLowAuth; + + auto const line = afView.read(keylet::line( + ownerID, + issuerID, + static_cast(offer.issueIn().asset()))); + + if (!line || (((*line)[sfFlags] & authFlag) == 0)) + { + // Offer owner not authorized to hold IOU from issuer. + // Remove this offer even if no crossing occurs. + if (auto const key = offer.key()) + offers.permRmOffer(*key); + if (!offerAttempted) + // Change quality only if no previous offers were + // tried. + ofrQ = std::nullopt; + // Returning true causes offers.step() to delete the + // offer. + return true; + } + } + } } if (!static_cast(this)->checkQualityThreshold( @@ -1267,15 +1291,23 @@ BookStep::check(StrandContext const& ctx) const { if (auto const prev = ctx.prevStep->directStepSrcAcct()) { - auto const& view = ctx.view; - auto const& cur = book_.in.account(); - - auto sle = view.read(keylet::line(*prev, cur, book_.in.asset())); - if (!sle) - return terNO_LINE; - if ((*sle)[sfFlags] & - ((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple)) - return terNO_RIPPLE; + if (book_.in.isMPT()) + { + // TODO MPT + } + else + { + auto const& view = ctx.view; + auto const& cur = book_.in.account(); + + auto sle = + view.read(keylet::line(*prev, cur, book_.in.asset())); + if (!sle) + return terNO_LINE; + if ((*sle)[sfFlags] & + ((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple)) + return terNO_RIPPLE; + } } } @@ -1371,4 +1403,35 @@ make_BookStepXI(StrandContext const& ctx, Issue const& out) return make_BookStepHelper(ctx, xrpIssue(), out); } +// MPT's +std::pair> +make_BookStepMM(StrandContext const& ctx, Issue const& in, Issue const& out) +{ + return make_BookStepHelper(ctx, in, out); +} + +std::pair> +make_BookStepMI(StrandContext const& ctx, Issue const& in, Issue const& out) +{ + return make_BookStepHelper(ctx, in, out); +} + +std::pair> +make_BookStepIM(StrandContext const& ctx, Issue const& in, Issue const& out) +{ + return make_BookStepHelper(ctx, in, out); +} + +std::pair> +make_BookStepMX(StrandContext const& ctx, Issue const& in) +{ + return make_BookStepHelper(ctx, in, xrpIssue()); +} + +std::pair> +make_BookStepXM(StrandContext const& ctx, Issue const& out) +{ + return make_BookStepHelper(ctx, xrpIssue(), out); +} + } // namespace ripple diff --git a/src/ripple/app/paths/impl/PaySteps.cpp b/src/ripple/app/paths/impl/PaySteps.cpp index 4d9e4308ed3..f8206be02c8 100644 --- a/src/ripple/app/paths/impl/PaySteps.cpp +++ b/src/ripple/app/paths/impl/PaySteps.cpp @@ -133,41 +133,26 @@ toStep( if (isXRP(outAsset)) { if (curIssue.isMPT()) - { - JLOG(j.info()) << "Found mpt/xrp offer payment step"; - return {temBAD_PATH, std::unique_ptr{}}; - } + return make_BookStepMX(ctx, curIssue); return make_BookStepIX(ctx, curIssue); } if (isXRP(curIssue.asset())) { if (e2->hasMPT()) - { - JLOG(j.info()) << "Found xrp/mpt offer payment step"; - return {temBAD_PATH, std::unique_ptr{}}; - } + return make_BookStepXM(ctx, {outAsset}); return make_BookStepXI(ctx, {outAsset, outIssuer}); } - // TODO MPT, add MPT for CI or IC, once supported in BookStep + // CI or IC if (curIssue.isMPT() && !e2->hasMPT()) - { - JLOG(j.info()) << "Found mpt/iou offer payment step"; - return {temBAD_PATH, std::unique_ptr{}}; - } + return make_BookStepMI(ctx, curIssue, {outAsset, outIssuer}); if (!curIssue.isMPT() && e2->hasMPT()) - { - JLOG(j.info()) << "Found iou/mpt offer payment step"; - return {temBAD_PATH, std::unique_ptr{}}; - } + return make_BookStepIM(ctx, curIssue, {outAsset}); // TODO - // TODO MPT, add MPT for CC or II, once supported in BookStep + // CC or II if (curIssue.isMPT()) - { - JLOG(j.info()) << "Found mpt/mpt offer payment step"; - return {temBAD_PATH, std::unique_ptr{}}; - } + return make_BookStepMM(ctx, curIssue, {outAsset}); // TODO return make_BookStepII(ctx, curIssue, {outAsset, outIssuer}); } @@ -225,7 +210,7 @@ toStrand( if (hasAccount && (pe.getAccountID() == noAccount())) return {temBAD_PATH, Strand{}}; - if (hasMPT && (!hasIssuer || hasCurrency || hasAccount)) + if (hasMPT && (hasIssuer || hasCurrency || hasAccount)) return {temBAD_PATH, Strand{}}; } @@ -366,7 +351,7 @@ toStrand( Asset asset; asset = cur->getAsset(); if (isXRP(asset)) - curIssue.setIssuer(xrpAccount()); + curIssue = xrpIssue(); if (cur->hasCurrency()) curIssue = std::make_pair(asset, curIssue.account()); else diff --git a/src/ripple/app/paths/impl/Steps.h b/src/ripple/app/paths/impl/Steps.h index a9c3bebeacf..4feceb53dc9 100644 --- a/src/ripple/app/paths/impl/Steps.h +++ b/src/ripple/app/paths/impl/Steps.h @@ -618,6 +618,15 @@ make_BookStepXI(StrandContext const& ctx, Issue const& out); std::pair> make_XRPEndpointStep(StrandContext const& ctx, AccountID const& acc); +std::pair> +make_BookStepMM(StrandContext const& ctx, Issue const& in, Issue const& out); + +std::pair> +make_BookStepMX(StrandContext const& ctx, Issue const& in); + +std::pair> +make_BookStepXM(StrandContext const& ctx, Issue const& out); + template bool isDirectXrpToXrp(Strand const& strand); diff --git a/src/ripple/app/paths/impl/StrandFlow.h b/src/ripple/app/paths/impl/StrandFlow.h index a4f46e38540..705731504b2 100644 --- a/src/ripple/app/paths/impl/StrandFlow.h +++ b/src/ripple/app/paths/impl/StrandFlow.h @@ -405,6 +405,8 @@ limitOut( return XRPAmount{*out}; else if constexpr (std::is_same_v) return IOUAmount{*out}; + else if constexpr (std::is_same_v) + return MPTAmount{*out}; else return STAmount{ remainingOut.issue(), out->mantissa(), out->exponent()}; diff --git a/src/ripple/app/paths/impl/XRPEndpointStep.cpp b/src/ripple/app/paths/impl/XRPEndpointStep.cpp index 4878463801c..73d978c2bf2 100644 --- a/src/ripple/app/paths/impl/XRPEndpointStep.cpp +++ b/src/ripple/app/paths/impl/XRPEndpointStep.cpp @@ -202,7 +202,9 @@ class XRPEndpointOfferCrossingStep static std::int32_t computeReserveReduction(StrandContext const& ctx, AccountID const& acc) { - if (ctx.isFirst && !ctx.view.read(keylet::line(acc, ctx.strandDeliver))) + if (ctx.isFirst && + (ctx.strandDeliver.isMPT() || + !ctx.view.read(keylet::line(acc, ctx.strandDeliver)))) return -1; return 0; } diff --git a/src/ripple/app/tx/impl/AMMCreate.cpp b/src/ripple/app/tx/impl/AMMCreate.cpp index fe293cffa62..20e843445a1 100644 --- a/src/ripple/app/tx/impl/AMMCreate.cpp +++ b/src/ripple/app/tx/impl/AMMCreate.cpp @@ -125,7 +125,7 @@ AMMCreate::preclaim(PreclaimContext const& ctx) } auto noDefaultRipple = [](ReadView const& view, Issue const& issue) { - if (isXRP(issue)) + if (isXRP(issue) || issue.isMPT()) return false; if (auto const issuerAccount = @@ -206,6 +206,7 @@ applyCreate( ApplyContext& ctx_, Sandbox& sb, AccountID const& account_, + XRPAmount const& priorBalance, beast::Journal j_) { auto const amount = ctx_.tx[sfAmount]; @@ -304,6 +305,20 @@ applyCreate( } auto sendAndTrustSet = [&](STAmount const& amount) -> TER { + if (amount.isMPT()) + { + auto const& mpt = static_cast(amount.issue().asset()); + auto const mptIssuanceID = keylet::mptIssuance(mpt); + if (auto const sle = sb.read(mptIssuanceID); !sle) + return tecINTERNAL; + else + { + if (auto const ter = + mptAuthorize(sb, *ammAccount, mpt, 0, priorBalance, j_); + ter != tesSUCCESS) + return ter; + } + } if (auto const res = accountSend( sb, account_, @@ -313,7 +328,7 @@ applyCreate( WaiveTransferFee::Yes)) return res; // Set AMM flag on AMM trustline - if (!isXRP(amount)) + if (!isXRP(amount) && !amount.isMPT()) { if (SLE::pointer sleRippleState = sb.peek(keylet::line(*ammAccount, amount.issue())); @@ -369,7 +384,7 @@ AMMCreate::doApply() // as we go on processing transactions. Sandbox sb(&ctx_.view()); - auto const result = applyCreate(ctx_, sb, account_, j_); + auto const result = applyCreate(ctx_, sb, account_, mPriorBalance, j_); if (result.second) sb.apply(ctx_.rawView()); diff --git a/src/ripple/app/tx/impl/CreateOffer.cpp b/src/ripple/app/tx/impl/CreateOffer.cpp index e3fae15f0cd..f2f1309f4f3 100644 --- a/src/ripple/app/tx/impl/CreateOffer.cpp +++ b/src/ripple/app/tx/impl/CreateOffer.cpp @@ -186,12 +186,10 @@ CreateOffer::preclaim(PreclaimContext const& ctx) // Make sure that we are authorized to hold what the taker will pay us. if (!saTakerPays.native()) { - auto result = checkAcceptAsset( - ctx.view, - ctx.flags, - id, - ctx.j, - Issue(uPaysCurrency, uPaysIssuerID)); + auto const iss = saTakerPays.isMPT() + ? Issue{uPaysCurrency} + : Issue{uPaysCurrency, uPaysIssuerID}; + auto result = checkAcceptAsset(ctx.view, ctx.flags, id, ctx.j, iss); if (result != tesSUCCESS) return result; } @@ -215,7 +213,7 @@ CreateOffer::checkAcceptAsset( if (!issuerAccount) { JLOG(j.debug()) - << "delay: can't receive IOUs from non-existent issuer: " + << "delay: can't receive IOUs/MPTs from non-existent issuer: " << to_string(issue.account()); return (flags & tapRETRY) ? TER{terNO_ACCOUNT} : TER{tecNO_ISSUER}; @@ -228,6 +226,13 @@ CreateOffer::checkAcceptAsset( // An account can always accept its own issuance. return tesSUCCESS; + if (issue.isMPT()) + { + if (requireAuth(view, issue, id) != tesSUCCESS) + return (flags & tapRETRY) ? TER{terNO_AUTH} : TER{tecNO_AUTH}; + return tesSUCCESS; + } + if ((*issuerAccount)[sfFlags] & lsfRequireAuth) { auto const trustLine = @@ -1183,13 +1188,23 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) bool const bookExisted = static_cast(sb.peek(dir)); auto const bookNode = sb.dirAppend(dir, offer_index, [&](SLE::ref sle) { - sle->setFieldH160( - sfTakerPaysCurrency, - static_cast(saTakerPays.issue().asset())); + if (saTakerPays.isMPT()) + sle->setFieldH192( + sfTakerPaysMPTID, + getMptID(static_cast(saTakerPays.issue().asset()))); + else + sle->setFieldH160( + sfTakerPaysCurrency, + static_cast(saTakerPays.issue().asset())); sle->setFieldH160(sfTakerPaysIssuer, saTakerPays.issue().account()); - sle->setFieldH160( - sfTakerGetsCurrency, - static_cast(saTakerGets.issue().asset())); + if (saTakerGets.isMPT()) + sle->setFieldH192( + sfTakerGetsMPTID, + getMptID(static_cast(saTakerGets.issue().asset()))); + else + sle->setFieldH160( + sfTakerGetsCurrency, + static_cast(saTakerGets.issue().asset())); sle->setFieldH160(sfTakerGetsIssuer, saTakerGets.issue().account()); sle->setFieldU64(sfExchangeRate, uRate); }); diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index da01f6e2c7b..9b22dd7ec9b 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -954,7 +954,7 @@ ValidMPTIssuance::finalize( { JLOG(j.fatal()) << "Invariant failed: a MPT issuance was deleted"; } - else if (mptokensCreated_ != 0) + else if (mptokensCreated_ != 0 && tx.getTxnType() != ttAMM_CREATE) { JLOG(j.fatal()) << "Invariant failed: a MPToken was created"; } @@ -964,7 +964,8 @@ ValidMPTIssuance::finalize( } return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && - mptokensCreated_ == 0 && mptokensDeleted_ == 0; + (tx.getTxnType() == ttAMM_CREATE || mptokensCreated_ == 0) && + mptokensDeleted_ == 0; } } // namespace ripple diff --git a/src/ripple/app/tx/impl/Offer.h b/src/ripple/app/tx/impl/Offer.h index 90c265651bf..c5c1b910659 100644 --- a/src/ripple/app/tx/impl/Offer.h +++ b/src/ripple/app/tx/impl/Offer.h @@ -270,6 +270,47 @@ TOffer::setFieldAmounts() m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out, issOut_)); } +// MPT +template <> +inline void +TOffer::setFieldAmounts() +{ + m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in, issIn_)); + m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out, issOut_)); +} + +template <> +inline void +TOffer::setFieldAmounts() +{ + m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in, issIn_)); + m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out, issOut_)); +} + +template <> +inline void +TOffer::setFieldAmounts() +{ + m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in, issIn_)); + m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out, issOut_)); +} + +template <> +inline void +TOffer::setFieldAmounts() +{ + m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in, issIn_)); + m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out)); +} + +template <> +inline void +TOffer::setFieldAmounts() +{ + m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in)); + m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out, issOut_)); +} + template Issue const& TOffer::issueIn() const diff --git a/src/ripple/app/tx/impl/OfferStream.cpp b/src/ripple/app/tx/impl/OfferStream.cpp index 2bf804165e3..47916ebecea 100644 --- a/src/ripple/app/tx/impl/OfferStream.cpp +++ b/src/ripple/app/tx/impl/OfferStream.cpp @@ -134,26 +134,24 @@ accountFundsHelper( view, id, issue.asset(), issue.account(), freezeHandling, j)); } +static MPTAmount +accountFundsHelper( + ReadView const& view, + AccountID const& id, + MPTAmount const& amtDefault, + Issue const& issue, + FreezeHandling freezeHandling, + beast::Journal j) +{ + return toAmount(accountHolds( + view, id, issue.asset(), issue.account(), freezeHandling, j)); +} + template template -bool + requires ValidTaker bool TOfferStreamBase::shouldRmSmallIncreasedQOffer() const { - static_assert( - std::is_same_v || - std::is_same_v, - "STAmount is not supported"); - - static_assert( - std::is_same_v || - std::is_same_v, - "STAmount is not supported"); - - static_assert( - !std::is_same_v || - !std::is_same_v, - "Cannot have XRP/XRP offers"); - if (!view_.rules().enabled(fixRmSmallIncreasedQOffers)) return false; @@ -176,7 +174,8 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const toAmount(offer_.amount().in), toAmount(offer_.amount().out)}; - if constexpr (!inIsXRP && !outIsXRP) + if constexpr ( + !inIsXRP && !outIsXRP && std::is_same_v) { if (ofrAmts.in >= ofrAmts.out) return false; @@ -311,37 +310,47 @@ TOfferStreamBase::step() continue; } - bool const rmSmallIncreasedQOffer = [&] { - bool const inIsXRP = isXRP(offer_.issueIn()); - bool const outIsXRP = isXRP(offer_.issueOut()); - if (inIsXRP && !outIsXRP) + using Var = std::variant; + auto toTypedAmt = [&](T const& amt) -> Var { + static auto xrp = XRPAmount{}; + static auto cft = MPTAmount{}; + static auto iou = IOUAmount{}; + if constexpr (std::is_same_v) { - // Without the `if constexpr`, the - // `shouldRmSmallIncreasedQOffer` template will be instantiated - // even if it is never used. This can cause compiler errors in - // some cases, hence the `if constexpr` guard. - // Note that TIn can be XRPAmount or STAmount, and TOut can be - // IOUAmount or STAmount. - if constexpr (!(std::is_same_v || - std::is_same_v)) - return shouldRmSmallIncreasedQOffer(); + if (isXRP(amt)) + return &xrp; + if (amt.isMPT()) + return &cft; + return &iou; } - if (!inIsXRP && outIsXRP) - { - // See comment above for `if constexpr` rationale - if constexpr (!(std::is_same_v || - std::is_same_v)) - return shouldRmSmallIncreasedQOffer(); - } - if (!inIsXRP && !outIsXRP) + if constexpr (!std::is_same_v) + return amt; + }; + + bool const rmSmallIncreasedQOffer = [&] { + bool ret = false; + if constexpr ( + !std::is_same_v && + !std::is_same_v) + return shouldRmSmallIncreasedQOffer(); + else if constexpr ( + std::is_same_v && std::is_same_v) { - // See comment above for `if constexpr` rationale - if constexpr (!(std::is_same_v || - std::is_same_v)) - return shouldRmSmallIncreasedQOffer(); + std::visit( + [&]( + TInAmt const*&&, TOutAmt const*&&) { + if constexpr ( + !std::is_same_v || + !std::is_same_v) + ret = + shouldRmSmallIncreasedQOffer(); + }, + toTypedAmt(offer_.amount().in), + toTypedAmt(offer_.amount().out)); + return ret; } - assert(0); // xrp/xrp offer!?! should never happen - return false; + assert(0); + return ret; }(); if (rmSmallIncreasedQOffer) @@ -395,9 +404,19 @@ template class FlowOfferStream; template class FlowOfferStream; template class FlowOfferStream; template class FlowOfferStream; +template class FlowOfferStream; +template class FlowOfferStream; +template class FlowOfferStream; +template class FlowOfferStream; +template class FlowOfferStream; template class TOfferStreamBase; template class TOfferStreamBase; template class TOfferStreamBase; template class TOfferStreamBase; +template class TOfferStreamBase; +template class TOfferStreamBase; +template class TOfferStreamBase; +template class TOfferStreamBase; +template class TOfferStreamBase; } // namespace ripple diff --git a/src/ripple/app/tx/impl/OfferStream.h b/src/ripple/app/tx/impl/OfferStream.h index 9472bc4e74d..4ce34c5508d 100644 --- a/src/ripple/app/tx/impl/OfferStream.h +++ b/src/ripple/app/tx/impl/OfferStream.h @@ -32,6 +32,19 @@ namespace ripple { +// clang-format off +template +concept ValidTaker = + ((std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) && + (!std::is_same_v || + !std::is_same_v)); +// clang-format on + template class TOfferStreamBase { @@ -86,7 +99,7 @@ class TOfferStreamBase permRmOffer(uint256 const& offerIndex) = 0; template - bool + requires ValidTaker bool shouldRmSmallIncreasedQOffer() const; public: diff --git a/src/ripple/app/tx/impl/Payment.cpp b/src/ripple/app/tx/impl/Payment.cpp index fb5665fb7c3..d217d60898c 100644 --- a/src/ripple/app/tx/impl/Payment.cpp +++ b/src/ripple/app/tx/impl/Payment.cpp @@ -92,7 +92,8 @@ Payment::preflight(PreflightContext const& ctx) auto const& uDstAsset = saDstAmount.getAsset(); bool const bXRPDirect = uSrcAsset.isXRP() && uDstAsset.isXRP(); - bool const bMPTDirect = uSrcAsset.isMPT() && uDstAsset.isMPT(); + bool const bMPTDirect = + uSrcAsset.isMPT() && uDstAsset.isMPT() && uSrcAsset == uDstAsset; bool const bDirect = bXRPDirect || bMPTDirect; if (!isLegalNet(saDstAmount) || !isLegalNet(maxSourceAmount)) diff --git a/src/ripple/ledger/PaymentSandbox.h b/src/ripple/ledger/PaymentSandbox.h index 60e42af1147..a54c14c162d 100644 --- a/src/ripple/ledger/PaymentSandbox.h +++ b/src/ripple/ledger/PaymentSandbox.h @@ -53,7 +53,7 @@ class DeferredCredits adjustments( AccountID const& main, AccountID const& other, - Currency const& currency) const; + Asset const& asset) const; void credit( @@ -76,7 +76,7 @@ class DeferredCredits private: // lowAccount, highAccount - using Key = std::tuple; + using Key = std::tuple; struct Value { explicit Value() = default; @@ -87,7 +87,7 @@ class DeferredCredits }; static Key - makeKey(AccountID const& a1, AccountID const& a2, Currency const& c); + makeKey(AccountID const& a1, AccountID const& a2, Asset const& a); std::map credits_; std::map ownerCounts_; diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index 70446bc258e..9ff24d1780e 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -494,6 +494,16 @@ rippleMPTCredit( Rate transferRateMPT(ReadView const& view, MPT const& id); +// TODO MPT +[[nodiscard]] TER +mptAuthorize( + ApplyView& view, + AccountID const& account, + MPT const& mptIssuanceID, + std::uint32_t flags, + XRPAmount const& priorBalance, + beast::Journal j); + } // namespace ripple #endif diff --git a/src/ripple/ledger/impl/PaymentSandbox.cpp b/src/ripple/ledger/impl/PaymentSandbox.cpp index c4b8881542f..dbb318e47bd 100644 --- a/src/ripple/ledger/impl/PaymentSandbox.cpp +++ b/src/ripple/ledger/impl/PaymentSandbox.cpp @@ -34,12 +34,12 @@ auto DeferredCredits::makeKey( AccountID const& a1, AccountID const& a2, - Currency const& c) -> Key + Asset const& a) -> Key { if (a1 < a2) - return std::make_tuple(a1, a2, c); + return std::make_tuple(a1, a2, a); else - return std::make_tuple(a2, a1, c); + return std::make_tuple(a2, a1, a); } void @@ -113,11 +113,11 @@ auto DeferredCredits::adjustments( AccountID const& main, AccountID const& other, - Currency const& currency) const -> std::optional + Asset const& asset) const -> std::optional { std::optional result; - Key const k = makeKey(main, other, currency); + Key const k = makeKey(main, other, asset); auto i = credits_.find(k); if (i == credits_.end()) return result; @@ -206,7 +206,8 @@ PaymentSandbox::balanceHook( // to compute usable balance just slightly above what the ledger // calculates (but always less than the actual balance). auto adjustedAmt = std::min({amount, lastBal - delta, minBal}); - adjustedAmt.setIssuer(amount.getIssuer()); + if (!adjustedAmt.isMPT()) + adjustedAmt.setIssuer(amount.getIssuer()); if (isXRP(issuer) && adjustedAmt < beast::zero) // A calculated negative XRP balance is not an error case. Consider a diff --git a/src/ripple/ledger/impl/View.cpp b/src/ripple/ledger/impl/View.cpp index efb37837bae..f64db8b71b0 100644 --- a/src/ripple/ledger/impl/View.cpp +++ b/src/ripple/ledger/impl/View.cpp @@ -269,8 +269,6 @@ accountHolds( auto const locked = sle->getFieldU64(sfLockedAmount); if (amt > locked) amount = STAmount{Issue{asset}, amt - locked}; - // TODO MPT balanceHook - return amount; } else { @@ -287,6 +285,9 @@ accountHolds( << " account=" << to_string(account) << " amount=" << amount.getFullText(); + // TODO MPT does this apply to MPT? + if (amount.isMPT()) + return amount; return view.balanceHook(account, issuer, amount); } @@ -1810,4 +1811,47 @@ rippleMPTCredit( return tesSUCCESS; } +TER +mptAuthorize( + ApplyView& view, + AccountID const& account, + MPT const& mptIssuanceID, + std::uint32_t flags, + XRPAmount const& priorBalance, + beast::Journal j) +{ + auto const sleAcct = view.peek(keylet::account(account)); + if (!sleAcct) + return tecINTERNAL; + + std::uint32_t const uOwnerCount = sleAcct->getFieldU32(sfOwnerCount); + XRPAmount const reserveCreate( + (uOwnerCount < 2) ? XRPAmount(beast::zero) + : view.fees().accountReserve(uOwnerCount + 1)); + + if (priorBalance < reserveCreate) + return tecINSUFFICIENT_RESERVE; + + auto const 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] = + getMptID(mptIssuanceID.second, mptIssuanceID.first); + (*mptoken)[sfFlags] = 0; + (*mptoken)[sfOwnerNode] = *ownerNode; + view.insert(mptoken); + + // Update owner count. + adjustOwnerCount(view, sleAcct, 1, j); + + return tesSUCCESS; +} + } // namespace ripple diff --git a/src/ripple/protocol/AMMCore.h b/src/ripple/protocol/AMMCore.h index 816bf86214b..600f6e24538 100644 --- a/src/ripple/protocol/AMMCore.h +++ b/src/ripple/protocol/AMMCore.h @@ -59,15 +59,12 @@ ammAccountID( /** Calculate Liquidity Provider Token (LPT) Currency. */ Currency -ammLPTCurrency(Currency const& cur1, Currency const& cur2); +ammLPTCurrency(Asset const& a1, Asset const& a2); /** Calculate LPT Issue from AMM asset pair. */ Issue -ammLPTIssue( - Currency const& cur1, - Currency const& cur2, - AccountID const& ammAccountID); +ammLPTIssue(Asset const& a1, Asset const& a2, AccountID const& ammAccountID); /** Validate the amount. * If validZero is false and amount is beast::zero then invalid amount. diff --git a/src/ripple/protocol/Asset.h b/src/ripple/protocol/Asset.h index 5da2215c0df..56777ae9610 100644 --- a/src/ripple/protocol/Asset.h +++ b/src/ripple/protocol/Asset.h @@ -29,16 +29,13 @@ namespace ripple { class Asset { - inline static MPT none = noMPT(); using asset_type = std::variant; private: asset_type asset_; public: - Asset() : asset_(Currency{beast::zero}) - { - } + Asset() = default; Asset(Currency const& c) : asset_(c) { } @@ -57,6 +54,18 @@ class Asset asset_ = u; return *this; } + Asset& + operator=(uint192 const& u) + { + std::uint32_t sequence; + std::memcpy(&sequence, u.data(), sizeof(sequence)); + sequence = boost::endian::big_to_native(sequence); + AccountID account; + std::memcpy( + account.begin(), u.begin() + sizeof(sequence), sizeof(account)); + asset_ = std::make_pair(sequence, account); + return *this; + } asset_type constexpr const& asset() const @@ -80,13 +89,6 @@ class Asset void addBitString(Serializer& s) const; - bool - empty() const - { - return std::holds_alternative(asset_) && - std::get(asset_) == none; - } - template friend void hash_append(Hasher& h, Asset const& a) @@ -160,7 +162,7 @@ class Asset friend bool isXRP(Asset const& a) { - return !a.empty() && a.isXRP(); + return a.isXRP(); } friend std::ostream& operator<<(std::ostream& stream, Asset const& a) @@ -170,6 +172,23 @@ class Asset } }; +inline constexpr std::weak_ordering +operator<=>(Asset const& lhs, Asset const& rhs) +{ + //assert(lhs.isCurrency() == rhs.isCurrency()); + if (lhs.isCurrency() != rhs.isCurrency()) + Throw("Invalid Asset comparison"); + if (lhs.isCurrency()) + return std::get(lhs.asset_) <=> + std::get(rhs.asset_); + if (auto const c{ + std::get(lhs.asset_).second <=> + std::get(rhs.asset_).second}; + c != 0) + return c; + return std::get(lhs.asset_).first <=> std::get(rhs.asset_).first; +} + } // namespace ripple #endif // RIPPLE_PROTOCOL_ASSET_H_INCLUDED diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index be17a09b4f8..c1c132b5318 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -338,6 +338,9 @@ getTicketIndex(AccountID const& account, std::uint32_t uSequence); uint256 getTicketIndex(AccountID const& account, SeqProxy ticketSeq); +uint192 +getMptID(MPT const& mpt); + uint192 getMptID(AccountID const& account, std::uint32_t sequence); diff --git a/src/ripple/protocol/Issue.h b/src/ripple/protocol/Issue.h index e20704208af..206eaa6ad6d 100644 --- a/src/ripple/protocol/Issue.h +++ b/src/ripple/protocol/Issue.h @@ -83,13 +83,7 @@ class Issue Issue& operator=(uint192 const& mptid) { - std::uint32_t sequence; - std::memcpy(&sequence, mptid.data(), sizeof(sequence)); - sequence = boost::endian::big_to_native(sequence); - AccountID account; - std::memcpy( - account.begin(), mptid.begin() + sizeof(sequence), sizeof(account)); - asset_ = std::make_pair(sequence, account); + asset_ = mptid; account_ = std::nullopt; return *this; } diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 6a9eb31723d..9310f0faf4d 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -476,6 +476,8 @@ extern SF_UINT160 const sfTakerGetsIssuer; // 192-bit (common) extern SF_UINT192 const sfMPTokenIssuanceID; +extern SF_UINT192 const sfTakerPaysMPTID; +extern SF_UINT192 const sfTakerGetsMPTID; // 256-bit (common) extern SF_UINT256 const sfLedgerHash; diff --git a/src/ripple/protocol/STObject.h b/src/ripple/protocol/STObject.h index f62ea125b97..0123070c0be 100644 --- a/src/ripple/protocol/STObject.h +++ b/src/ripple/protocol/STObject.h @@ -354,6 +354,8 @@ class STObject : public STBase, public CountedObject void setFieldH128(SField const& field, uint128 const&); void + setFieldH192(SField const& field, uint192 const&); + void setFieldH256(SField const& field, uint256 const&); void setFieldVL(SField const& field, Blob const&); diff --git a/src/ripple/protocol/STPathSet.h b/src/ripple/protocol/STPathSet.h index 1fec998fb0f..272e750df8b 100644 --- a/src/ripple/protocol/STPathSet.h +++ b/src/ripple/protocol/STPathSet.h @@ -295,13 +295,13 @@ inline STPathElement::STPathElement( if (!is_offer_) mType |= typeAccount; - if ((asset.empty() || !asset.isMPT()) && (forceCurrency || !isXRP(asset))) + if (!asset.isMPT() && (forceCurrency || !isXRP(asset))) mType |= typeCurrency; if (!isXRP(issuer)) mType |= typeIssuer; - if (!asset.empty() && asset.isMPT()) + if (asset.isMPT()) mType |= typeMPT; hash_value_ = get_hash(*this); @@ -318,6 +318,10 @@ inline STPathElement::STPathElement( , mIssuerID(issuer) , is_offer_(isXRP(mAccountID)) { + if (!asset.isMPT()) + mType = mType & (~Type::typeMPT); + else if (asset.isXRP()) + mType = mType & (~Type::typeCurrency); hash_value_ = get_hash(*this); } diff --git a/src/ripple/protocol/impl/AMMCore.cpp b/src/ripple/protocol/impl/AMMCore.cpp index 55ed9864bc6..f21add7f352 100644 --- a/src/ripple/protocol/impl/AMMCore.cpp +++ b/src/ripple/protocol/impl/AMMCore.cpp @@ -39,12 +39,16 @@ ammAccountID( } Currency -ammLPTCurrency(Currency const& cur1, Currency const& cur2) +ammLPTCurrency(Asset const& a1, Asset const& a2) { // AMM LPToken is 0x03 plus 19 bytes of the hash std::int32_t constexpr AMMCurrencyCode = 0x03; - auto const [minC, maxC] = std::minmax(cur1, cur2); - auto const hash = sha512Half(minC, maxC); + auto const [minA, maxA] = std::minmax(a1, a2); + uint256 hash; + std::visit( + [&](auto&& arg1, auto&& arg2) { hash = sha512Half(arg1, arg2); }, + minA.asset(), + maxA.asset()); Currency currency; *currency.begin() = AMMCurrencyCode; std::copy( @@ -53,12 +57,9 @@ ammLPTCurrency(Currency const& cur1, Currency const& cur2) } Issue -ammLPTIssue( - Currency const& cur1, - Currency const& cur2, - AccountID const& ammAccountID) +ammLPTIssue(Asset const& a1, Asset const& a2, AccountID const& ammAccountID) { - return Issue(ammLPTCurrency(cur1, cur2), ammAccountID); + return Issue(ammLPTCurrency(a1, a2), ammAccountID); } NotTEC diff --git a/src/ripple/protocol/impl/Asset.cpp b/src/ripple/protocol/impl/Asset.cpp index ed484de4437..808f55bb2db 100644 --- a/src/ripple/protocol/impl/Asset.cpp +++ b/src/ripple/protocol/impl/Asset.cpp @@ -33,7 +33,7 @@ Asset::addBitString(ripple::Serializer& s) const } } -Asset::operator Currency const &() const +Asset::operator Currency const&() const { assert(std::holds_alternative(asset_)); if (!std::holds_alternative(asset_)) @@ -41,31 +41,14 @@ Asset::operator Currency const &() const return std::get(asset_); } -Asset::operator MPT const &() const +Asset::operator MPT const&() const { - assert(std::holds_alternative(asset_)); + // assert(std::holds_alternative(asset_)); if (!std::holds_alternative(asset_)) Throw("Invalid MPT cast"); return std::get(asset_); } -constexpr std::weak_ordering -operator<=>(Asset const& lhs, Asset const& rhs) -{ - assert(lhs.isCurrency() == rhs.isCurrency()); - if (lhs.isCurrency() != rhs.isCurrency()) - Throw("Invalid Asset comparison"); - if (lhs.isCurrency()) - return std::get(lhs.asset_) <=> - std::get(rhs.asset_); - if (auto const c{ - std::get(lhs.asset_).second <=> - std::get(rhs.asset_).second}; - c != 0) - return c; - return std::get(lhs.asset_).first <=> std::get(rhs.asset_).first; -} - std::string to_string(Asset const& a) { diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 1394d5bfa3d..dbac25452b8 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -137,6 +137,16 @@ getTicketIndex(AccountID const& account, SeqProxy ticketSeq) return getTicketIndex(account, ticketSeq.value()); } +uint192 +getMptID(MPT const& mpt) +{ + uint192 u; + auto const sequence = boost::endian::native_to_big(mpt.first); + memcpy(u.data(), &sequence, sizeof(sequence)); + memcpy(u.data() + sizeof(sequence), mpt.second.data(), sizeof(mpt.second)); + return u; +} + uint192 getMptID(AccountID const& account, std::uint32_t sequence) { diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index d7f3387b47e..6274f92fc72 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -65,8 +65,10 @@ LedgerFormats::LedgerFormats() { {sfOwner, soeOPTIONAL}, // for owner directories {sfTakerPaysCurrency, soeOPTIONAL}, // order book directories + {sfTakerPaysMPTID, soeOPTIONAL}, // order book directories {sfTakerPaysIssuer, soeOPTIONAL}, // order book directories {sfTakerGetsCurrency, soeOPTIONAL}, // order book directories + {sfTakerGetsMPTID, soeOPTIONAL}, // order book directories {sfTakerGetsIssuer, soeOPTIONAL}, // order book directories {sfExchangeRate, soeOPTIONAL}, // order book directories {sfIndexes, soeREQUIRED}, diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 3b3f309d382..f27ec226aab 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -205,6 +205,9 @@ CONSTRUCT_TYPED_SFIELD(sfTakerGetsIssuer, "TakerGetsIssuer", UINT160, // 192-bit (common) CONSTRUCT_TYPED_SFIELD(sfMPTokenIssuanceID, "MPTokenIssuanceID", UINT192, 1); +CONSTRUCT_TYPED_SFIELD(sfTakerPaysMPTID, "TakerPaysMPTID", UINT192, 2); +CONSTRUCT_TYPED_SFIELD(sfTakerGetsMPTID, "TakerGetsMPTID", UINT192, 3); + // 256-bit (common) CONSTRUCT_TYPED_SFIELD(sfLedgerHash, "LedgerHash", UINT256, 1); diff --git a/src/ripple/protocol/impl/STIssue.cpp b/src/ripple/protocol/impl/STIssue.cpp index 508bcef7729..36d8bdc7ee5 100644 --- a/src/ripple/protocol/impl/STIssue.cpp +++ b/src/ripple/protocol/impl/STIssue.cpp @@ -51,7 +51,34 @@ STIssue::STIssue(SerialIter& sit, SField const& name) : STBase{name} if (isXRP(currency) != isXRP(account)) Throw( "invalid issue: currency and account native mismatch"); - issue_ = std::make_pair(currency, account); + + auto getSequence = [](auto const& u) -> std::uint32_t { + std::uint32_t s; + std::memcpy(&s, u.data(), sizeof(s)); + return boost::endian::big_to_native(s); + }; + // check if MPT + static size_t constexpr seqSize = sizeof(MPT::first_type); + static size_t constexpr truncAcctSize = sizeof(MPT::second_type) - seqSize; + if (isXRP(currency)) + issue_ = std::make_pair(currency, account); + else if (auto const sequence = getSequence(currency); + sequence == getSequence(account) && + memcmp( + currency.data() + seqSize, + account.data() + 2 * seqSize, + truncAcctSize - seqSize) == 0) + { + AccountID account1; + memcpy(account1.data(), currency.data(), truncAcctSize); + memcpy( + account1.data() + truncAcctSize, + account.data() + truncAcctSize, + seqSize); + issue_ = std::make_pair(sequence, account1); + } + else + issue_ = std::make_pair(currency, account); } STIssue::STIssue(SField const& name, Issue const& issue) @@ -82,13 +109,34 @@ Json::Value STIssue::getJson(JsonOptions) const void STIssue::add(Serializer& s) const { - // TODO add BACKWARDS compatible serialization once MPT is supported - assert(!issue_.isMPT()); + // serialize mpt as currency/2 x sequence + // where currency = currency (32 bits) + account (first 128 bits) + // and account = currency (32 bits) + account (last 128 bits). + // when decoding, check first 32 bits of each 160 bits for MPT and verify + // 96 bits of the account if (issue_.isMPT()) - Throw("MPT is not supported in STIssue"); - s.addBitString(static_cast(issue_.asset())); - if (!isXRP(issue_.asset())) - s.addBitString(issue_.account()); + { + uint160 c{beast::zero}; + uint160 a{beast::zero}; + auto const mpt = static_cast(issue_.asset()); + auto const& account = mpt.second; + auto const sequence = boost::endian::native_to_big(mpt.first); + static size_t constexpr seqSize = sizeof(MPT::first_type); + static size_t constexpr truncAcctSize = + sizeof(MPT::second_type) - seqSize; + memcpy(c.data(), &sequence, seqSize); + memcpy(c.data() + seqSize, account.data(), truncAcctSize); + memcpy(a.data(), &sequence, seqSize); + memcpy(a.data(), account.data() + seqSize, truncAcctSize); + s.addBitString(c); + s.addBitString(a); + } + else + { + s.addBitString(static_cast(issue_.asset())); + if (!isXRP(issue_.asset())) + s.addBitString(issue_.account()); + } } bool diff --git a/src/ripple/protocol/impl/STObject.cpp b/src/ripple/protocol/impl/STObject.cpp index 05c18187f53..8fadc53050c 100644 --- a/src/ripple/protocol/impl/STObject.cpp +++ b/src/ripple/protocol/impl/STObject.cpp @@ -679,6 +679,12 @@ STObject::setFieldH128(SField const& field, uint128 const& v) setFieldUsingSetValue(field, v); } +void +STObject::setFieldH192(SField const& field, uint192 const& v) +{ + setFieldUsingSetValue(field, v); +} + void STObject::setFieldH256(SField const& field, uint256 const& v) { diff --git a/src/ripple/protocol/impl/STParsedJSON.cpp b/src/ripple/protocol/impl/STParsedJSON.cpp index 6a5850f4fe4..17375b6f0db 100644 --- a/src/ripple/protocol/impl/STParsedJSON.cpp +++ b/src/ripple/protocol/impl/STParsedJSON.cpp @@ -625,7 +625,7 @@ parseLeaf( json_name + "." + ss.str()); // each element in this path has some combination of - // account, currency, or issuer + // account, asset, or issuer Json::Value pathEl = value[i][j]; @@ -635,12 +635,16 @@ parseLeaf( return ret; } + bool const isMPT = + pathEl.isMember(jss::mpt_issuance_id); + std::string const assetName = + isMPT ? "mpt_issuance_id" : "currency"; Json::Value const& account = pathEl["account"]; - Json::Value const& currency = pathEl["currency"]; + Json::Value const& asset = pathEl[assetName]; Json::Value const& issuer = pathEl["issuer"]; bool hasCurrency = false; AccountID uAccount, uIssuer; - Currency uCurrency; + Asset uAsset; if (account) { @@ -668,27 +672,44 @@ parseLeaf( } } - if (currency) + if (asset) { - // human currency - if (!currency.isString()) + // human asset + if (!asset.isString()) { error = - string_expected(element_name, "currency"); + string_expected(element_name, assetName); return ret; } hasCurrency = true; - if (!uCurrency.parseHex(currency.asString())) + if (isMPT) { - if (!to_currency( - uCurrency, currency.asString())) + // uint32+accountID + uint192 u; + if (!u.parseHex(asset.asString())) { error = - invalid_data(element_name, "currency"); + invalid_data(element_name, assetName); return ret; } + uAsset = u; + } + else + { + Currency currency; + if (!currency.parseHex(asset.asString())) + { + if (!to_currency( + currency, asset.asString())) + { + error = invalid_data( + element_name, assetName); + return ret; + } + } + uAsset = currency; } } @@ -715,8 +736,7 @@ parseLeaf( } } - p.emplace_back( - uAccount, uCurrency, uIssuer, hasCurrency); + p.emplace_back(uAccount, uAsset, uIssuer, hasCurrency); } tail.push_back(p); diff --git a/src/ripple/protocol/impl/STTx.cpp b/src/ripple/protocol/impl/STTx.cpp index 2bc436db0ef..51b8f79c129 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -530,19 +530,26 @@ isMemoOkay(STObject const& st, std::string& reason) } // Ensure all account fields are 160-bits and that MPT amount is only passed -// to Payment tx (until MPT is supported in more tx) +// to Payment and OfferCreate tx (until MPT is supported in more tx) static bool isAccountAndMPTFieldOkay(STObject const& st) { auto const txType = st[~sfTransactionType]; - bool const isPaymentTx = txType && safe_cast(*txType) == ttPAYMENT; + bool const isMPTAmountAllowed = [&]() { + if (!txType) + return true; + auto t = safe_cast(*txType); + return t == ttPAYMENT || t == ttOFFER_CREATE || t == ttAMM_CREATE || + t == ttAMM_DEPOSIT || t == ttAMM_WITHDRAW || t == ttAMM_BID || + t == ttAMM_VOTE || t == ttAMM_DELETE; + }(); 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) + if (amt && amt->isMPT() && !isMPTAmountAllowed) return false; } diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index d1c74221489..b096f5c1233 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -826,7 +826,7 @@ class MPToken_test : public beast::unit_test::suite mptAlice.create(); - env(offer(alice, mptAlice.mpt(100), XRP(100)), ter(temINVALID)); + env(check::create(env.master, alice, mptAlice.mpt(100)), ter(temINVALID)); env.close(); BEAST_EXPECT(expectOffers(env, alice, 0)); @@ -859,6 +859,421 @@ class MPToken_test : public beast::unit_test::suite meta[jss::mpt_issuance_id] == to_string(mptAlice.issuanceID())); } + void + testOfferCrossing(FeatureBitset features) + { + testcase("Offer Crossing"); + using namespace test::jtx; + Account const gw = Account("gw"); + Account const alice = Account("alice"); + Account const carol = Account("carol"); + auto const USD = gw["USD"]; + + // XRP/MPT + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {&alice, &carol}}); + + mpt.create({.ownerCount = 1, .holderCount = 0}); + + mpt.authorize({.account = &alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = &carol}); + mpt.pay(gw, carol, 200); + + env(offer(alice, XRP(100), mpt.mpt(101))); + env.close(); + BEAST_EXPECT(expectOffers( + env, alice, 1, {{Amounts{XRP(100), mpt.mpt(101)}}})); + + env(offer(carol, mpt.mpt(101), XRP(100))); + env.close(); + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(expectOffers(env, carol, 0)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 301)); + } + + // IOU/MPT + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {&alice, &carol}}); + + mpt.create({.ownerCount = 1, .holderCount = 0}); + + env(trust(alice, USD(2'000))); + env(pay(gw, alice, USD(1'000))); + env.close(); + + env(trust(carol, USD(2'000))); + env(pay(gw, carol, USD(1'000))); + env.close(); + + mpt.authorize({.account = &alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = &carol}); + mpt.pay(gw, carol, 200); + + env(offer(alice, USD(100), mpt.mpt(101))); + env.close(); + BEAST_EXPECT(expectOffers( + env, alice, 1, {{Amounts{USD(100), mpt.mpt(101)}}})); + + env(offer(carol, mpt.mpt(101), USD(100))); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == USD(1'100)); + BEAST_EXPECT(env.balance(carol, USD) == USD(900)); + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(expectOffers(env, carol, 0)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 301)); + } + + // MPT/MPT + { + Env env{*this, features}; + + MPTTester mpt1(env, gw, {.holders = {&alice, &carol}}); + mpt1.create({.ownerCount = 1, .holderCount = 0}); + + MPTTester mpt2( + env, gw, {.holders = {&alice, &carol}, .fund = false}); + mpt2.create({.ownerCount = 2, .holderCount = 0}); + + mpt1.authorize({.account = &alice}); + mpt1.authorize({.account = &carol}); + mpt1.pay(gw, alice, 200); + mpt1.pay(gw, carol, 200); + + mpt2.authorize({.account = &alice}); + mpt2.authorize({.account = &carol}); + mpt2.pay(gw, alice, 200); + mpt2.pay(gw, carol, 200); + + env(offer(alice, mpt2.mpt(100), mpt1.mpt(101))); + env.close(); + BEAST_EXPECT(expectOffers( + env, alice, 1, {{Amounts{mpt2.mpt(100), mpt1.mpt(101)}}})); + + env(offer(carol, mpt1.mpt(101), mpt2.mpt(100))); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(expectOffers(env, carol, 0)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(carol, 301)); + BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 300)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 100)); + } + } + + void + testCrossAssetPayment(FeatureBitset features) + { + testcase("Cross Asset Payment"); + using namespace test::jtx; + Account const gw = Account("gw"); + Account const alice = Account("alice"); + Account const carol = Account("carol"); + Account const bob = Account("bob"); + auto const USD = gw["USD"]; + + // MPT/XRP + { + Env env{*this, features}; + MPTTester mpt(env, gw, {.holders = {&alice, &carol, &bob}}); + + mpt.create({.ownerCount = 1, .holderCount = 0}); + + mpt.authorize({.account = &alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = &carol}); + mpt.pay(gw, carol, 200); + + mpt.authorize({.account = &bob}); + + env(offer(alice, XRP(100), mpt.mpt(101))); + env.close(); + BEAST_EXPECT(expectOffers( + env, alice, 1, {{Amounts{XRP(100), mpt.mpt(101)}}})); + + env(pay(carol, bob, mpt.mpt(101)), + test::jtx::path(~mpt.MPT()), + sendmax(XRP(100)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 101)); + } + + // MPT/IOU + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {&alice, &carol, &bob}}); + + mpt.create({.ownerCount = 1, .holderCount = 0}); + + env(trust(alice, USD(2'000))); + env(pay(gw, alice, USD(1'000))); + env(trust(bob, USD(2'000))); + env(pay(gw, bob, USD(1'000))); + env(trust(carol, USD(2'000))); + env(pay(gw, carol, USD(1'000))); + env.close(); + + mpt.authorize({.account = &alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = &carol}); + mpt.pay(gw, carol, 200); + + mpt.authorize({.account = &bob}); + + env(offer(alice, USD(100), mpt.mpt(101))); + env.close(); + BEAST_EXPECT(expectOffers( + env, alice, 1, {{Amounts{USD(100), mpt.mpt(101)}}})); + + env(pay(carol, bob, mpt.mpt(101)), + test::jtx::path(~mpt.MPT()), + sendmax(USD(100)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(env.balance(carol, USD) == USD(900)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 101)); + } + + // IOU/MPT + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {&alice, &carol, &bob}}); + + mpt.create({.ownerCount = 1, .holderCount = 0}); + + env(trust(alice, USD(2'000)), txflags(tfClearNoRipple)); + env(pay(gw, alice, USD(1'000))); + env(trust(bob, USD(2'000)), txflags(tfClearNoRipple)); + env.close(); + + mpt.authorize({.account = &alice}); + env(pay(gw, alice, mpt.mpt(200))); + + mpt.authorize({.account = &carol}); + env(pay(gw, carol, mpt.mpt(200))); + + env(offer(alice, mpt.mpt(101), USD(100))); + env.close(); + BEAST_EXPECT(expectOffers( + env, alice, 1, {{Amounts{mpt.mpt(101), USD(100)}}})); + + env(pay(carol, bob, USD(100)), + test::jtx::path(~USD), + sendmax(mpt.mpt(101)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(env.balance(alice, USD) == USD(900)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 301)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 99)); + BEAST_EXPECT(env.balance(bob, USD) == USD(100)); + } + + // MPT/MPT + { + Env env{*this, features}; + + MPTTester mpt1(env, gw, {.holders = {&alice, &carol, &bob}}); + mpt1.create({.ownerCount = 1, .holderCount = 0}); + + MPTTester mpt2( + env, gw, {.holders = {&alice, &carol, &bob}, .fund = false}); + mpt2.create({.ownerCount = 2, .holderCount = 0}); + + mpt1.authorize({.account = &alice}); + mpt1.pay(gw, alice, 200); + mpt2.authorize({.account = &alice}); + + mpt2.authorize({.account = &carol}); + mpt2.pay(gw, carol, 200); + + mpt1.authorize({.account = &bob}); + + env(offer(alice, mpt2.mpt(100), mpt1.mpt(101))); + env.close(); + BEAST_EXPECT(expectOffers( + env, alice, 1, {{Amounts{mpt2.mpt(100), mpt1.mpt(101)}}})); + + env(pay(carol, bob, mpt1.mpt(101)), + test::jtx::path(~mpt1.MPT()), + sendmax(mpt2.mpt(100)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(200)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 100)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 101)); + } + + // XRP/MPT AMM + { + Env env{*this, features}; + + fund(env, gw, {alice, carol, bob}, XRP(11'000), {USD(20'000)}); + + MPTTester mpt(env, gw, {.fund = false}); + + mpt.create({.ownerCount = 1, .holderCount = 0}); + + mpt.authorize({.account = &alice}); + mpt.authorize({.account = &bob}); + mpt.pay(gw, alice, 10'100); + + AMM amm(env, alice, XRP(10'000), mpt.mpt(10'100)); + + env(pay(carol, bob, mpt.mpt(100)), + test::jtx::path(~mpt.MPT()), + sendmax(XRP(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(XRP(10'100), mpt.mpt(10'000), amm.tokens())); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 100)); + } + + // IOU/MPT AMM + { + Env env{*this, features}; + + fund(env, gw, {alice, carol, bob}, XRP(11'000), {USD(20'000)}); + + MPTTester mpt(env, gw, {.fund = false}); + + mpt.create({.ownerCount = 1, .holderCount = 0}); + + mpt.authorize({.account = &alice}); + mpt.authorize({.account = &bob}); + mpt.pay(gw, alice, 10'100); + + AMM amm(env, alice, USD(10'000), mpt.mpt(10'100)); + + env(pay(carol, bob, mpt.mpt(100)), + test::jtx::path(~mpt.MPT()), + sendmax(USD(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(USD(10'100), mpt.mpt(10'000), amm.tokens())); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 100)); + } + + // MPT/MPT AMM cross-asset payment + { + Env env{*this, features}; + env.fund(XRP(20'000), gw, alice, carol, bob); + env.close(); + + MPTTester mpt1(env, gw, {.fund = false}); + mpt1.create(); + mpt1.authorize({.account = &alice}); + mpt1.authorize({.account = &bob}); + mpt1.pay(gw, alice, 10'100); + + MPTTester mpt2(env, gw, {.fund = false}); + mpt2.create(); + mpt2.authorize({.account = &alice}); + mpt2.authorize({.account = &bob}); + mpt2.authorize({.account = &carol}); + mpt2.pay(gw, alice, 10'100); + mpt2.pay(gw, carol, 100); + + AMM amm(env, alice, mpt2.mpt(10'000), mpt1.mpt(10'100)); + + env(pay(carol, bob, mpt1.mpt(100)), + test::jtx::path(~mpt1.MPT()), + sendmax(mpt2.mpt(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + mpt2.mpt(10'100), mpt1.mpt(10'000), amm.tokens())); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 100)); + } + + // Multi-steps with AMM + // IOU/MPT1 MPT1/MPT2 MPT2/IOU IOU/IOU AMM:IOU/MPT MPT/IOU + { + Env env{*this, features}; + auto const USD = gw["USD"]; + auto const EUR = gw["EUR"]; + auto const CRN = gw["CRN"]; + auto const YAN = gw["YAN"]; + + fund( + env, + gw, + {alice, carol, bob}, + XRP(1'000), + {USD(1'000), EUR(1'000), CRN(2'000), YAN(1'000)}); + + auto createMPT = [&]() -> MPTTester { + MPTTester mpt(env, gw, {.fund = false}); + mpt.create(); + mpt.authorize({.account = &alice}); + mpt.pay(gw, alice, 2'000); + return mpt; + }; + + auto mpt1 = createMPT(); + auto mpt2 = createMPT(); + auto mpt3 = createMPT(); + + env(offer(alice, EUR(100), mpt1.mpt(101))); + env(offer(alice, mpt1.mpt(101), mpt2.mpt(102))); + env(offer(alice, mpt2.mpt(102), USD(103))); + env(offer(alice, USD(103), CRN(104))); + env.close(); + AMM amm(env, alice, CRN(1'000), mpt3.mpt(1'104)); + env(offer(alice, mpt3.mpt(104), YAN(100))); + + env(pay(carol, bob, YAN(100)), + test::jtx::path( + ~mpt1.MPT(), ~mpt2.MPT(), ~USD, ~CRN, ~mpt3.MPT(), ~YAN), + sendmax(EUR(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT(env.balance(bob, YAN) == YAN(1'100)); + BEAST_EXPECT(amm.expectBalances( + CRN(1'104), mpt3.mpt(1'000), amm.tokens())); + BEAST_EXPECT(expectOffers(env, alice, 0)); + } + } + public: void run() override @@ -884,6 +1299,9 @@ class MPToken_test : public beast::unit_test::suite // Test Direct Payment testPayment(all); + // Integration with DEX + testOfferCrossing(all); + testCrossAssetPayment(all); // Test MPT Amount is invalid in non-Payment Tx testMPTInvalidInTx(all); diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index 7314bf21720..b436170f373 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -155,10 +155,10 @@ operator<<(std::ostream& os, PrettyAmount const& amount); struct BookSpec { AccountID account; - ripple::Currency currency; + ripple::Asset asset; - BookSpec(AccountID const& account_, ripple::Currency const& currency_) - : account(account_), currency(currency_) + BookSpec(AccountID const& account_, ripple::Asset const& asset_) + : account(account_), asset(asset_) { } }; @@ -407,11 +407,9 @@ class MPT } friend BookSpec - operator~(MPT const& mpt) + operator~(test::jtx::MPT const& mpt) { - assert(false); - Throw("MPT is not supported"); - return BookSpec{beast::zero, noCurrency()}; + return BookSpec{mpt.mptID.second, mpt.mptID}; } }; diff --git a/src/test/jtx/impl/paths.cpp b/src/test/jtx/impl/paths.cpp index 82af5b6ade2..19a9093d560 100644 --- a/src/test/jtx/impl/paths.cpp +++ b/src/test/jtx/impl/paths.cpp @@ -88,8 +88,13 @@ void path::append_one(BookSpec const& book) { auto& jv = create(); - jv["currency"] = to_string(book.currency); - jv["issuer"] = toBase58(book.account); + if (book.asset.isMPT()) + jv["mpt_issuance_id"] = to_string(book.asset); + else + { + jv["currency"] = to_string(book.asset); + jv["issuer"] = toBase58(book.account); + } } void diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 12b384c2902..5c0905f338f 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -103,7 +103,6 @@ struct MPTCreate 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; }; @@ -192,6 +191,13 @@ class MPTTester PrettyAmount mpt(std::uint64_t amount) const; + test::jtx::MPT + MPT() const + { + assert(mpt_); + return test::jtx::MPT(issuer_.name(), *mpt_); + } + uint256 const& issuanceKey() const {