diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index 9f54e537699..1c8b9f0fee6 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -2,7 +2,9 @@ #include #include +#include #include +#include #include #include #include @@ -25,6 +27,290 @@ namespace xrpl { +/** Validate that @p account may lock @p amount of a token for later delivery + * to @p dest. + * + * The lock-side counterpart of escrowUnlockPreclaimHelper: every issuer + * control (locking opt-in, authorization, freeze/lock, transferability, + * spendable balance) that gates locking token value lives here, so any + * transactor that locks funds applies the same rules. The signature is + * view-based rather than PreclaimContext-based so it can also run from + * doApply. + */ +template +TER +escrowLockPreclaimHelper( + ReadView const& view, + AccountID const& account, + AccountID const& dest, + STAmount const& amount, + beast::Journal j); + +template <> +inline TER +escrowLockPreclaimHelper( + ReadView const& view, + AccountID const& account, + AccountID const& dest, + STAmount const& amount, + beast::Journal j) +{ + auto const& issue = amount.get(); + AccountID const& issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecNO_PERMISSION + if (issuer == account) + return tecNO_PERMISSION; + + // If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION + auto const sleIssuer = view.read(keylet::account(issuer)); + if (!sleIssuer) + return tecNO_ISSUER; + if (!sleIssuer->isFlag(lsfAllowTrustLineLocking)) + return tecNO_PERMISSION; + + // If the account does not have a trustline to the issuer, return tecNO_LINE + auto const sleRippleState = view.read(keylet::trustLine(account, issuer, issue.currency)); + if (!sleRippleState) + return tecNO_LINE; + + STAmount const balance = (*sleRippleState)[sfBalance]; + + // If balance is positive, issuer must have higher address than account + if (balance > beast::kZero && issuer < account) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If balance is negative, issuer must have lower address than account + if (balance < beast::kZero && issuer > account) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If the issuer has requireAuth set, check if the account is authorized + if (auto const ter = requireAuth(view, issue, account); !isTesSuccess(ter)) + return ter; + + // If the issuer has requireAuth set, check if the destination is authorized + if (auto const ter = requireAuth(view, issue, dest); !isTesSuccess(ter)) + return ter; + + // If the issuer has frozen the account, return tecFROZEN + if (isFrozen(view, account, issue)) + return tecFROZEN; + + // If the issuer has frozen the destination, return tecFROZEN + if (isFrozen(view, dest, issue)) + return tecFROZEN; + + STAmount const spendableAmount = + accountHolds(view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j); + + // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS + if (spendableAmount <= beast::kZero) + return tecINSUFFICIENT_FUNDS; + + // If the spendable amount is less than the amount, return + // tecINSUFFICIENT_FUNDS + if (spendableAmount < amount) + return tecINSUFFICIENT_FUNDS; + + // If the amount is not addable to the balance, return tecPRECISION_LOSS + if (!canAdd(spendableAmount, amount)) + return tecPRECISION_LOSS; + + return tesSUCCESS; +} + +template <> +inline TER +escrowLockPreclaimHelper( + ReadView const& view, + AccountID const& account, + AccountID const& dest, + STAmount const& amount, + beast::Journal j) +{ + AccountID const issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecNO_PERMISSION + if (issuer == account) + return tecNO_PERMISSION; + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = keylet::mptokenIssuance(amount.get().getMptID()); + auto const sleIssuance = view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION + if (!sleIssuance->isFlag(lsfMPTCanEscrow)) + return tecNO_PERMISSION; + + // If the issuer is not the same as the issuer of the mpt, return + // tecNO_PERMISSION + if (sleIssuance->getAccountID(sfIssuer) != issuer) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If the account does not have the mpt, return tecOBJECT_NOT_FOUND + if (!view.exists(keylet::mptoken(issuanceKey.key, account))) + return tecOBJECT_NOT_FOUND; + + // If the issuer has requireAuth set, check if the account is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth); + !isTesSuccess(ter)) + return ter; + + // If the issuer has requireAuth set, check if the destination is + // authorized + if (auto const ter = requireAuth(view, mptIssue, dest, AuthType::WeakAuth); !isTesSuccess(ter)) + return ter; + + // If the issuer has frozen the account, return tecLOCKED + if (isFrozen(view, account, mptIssue)) + return tecLOCKED; + + // If the issuer has frozen the destination, return tecLOCKED + if (isFrozen(view, dest, mptIssue)) + return tecLOCKED; + + // If the mpt cannot be transferred, return tecNO_AUTH + if (auto const ter = canTransfer(view, mptIssue, account, dest); !isTesSuccess(ter)) + return ter; + + STAmount const spendableAmount = accountHolds( + view, + account, + amount.get(), + FreezeHandling::IgnoreFreeze, + AuthHandling::IgnoreAuth, + j); + + // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS + if (spendableAmount <= beast::kZero) + return tecINSUFFICIENT_FUNDS; + + // If the spendable amount is less than the amount, return + // tecINSUFFICIENT_FUNDS + if (spendableAmount < amount) + return tecINSUFFICIENT_FUNDS; + + return tesSUCCESS; +} + +template +TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal); + +template <> +inline TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create an escrow + if (issuer == sender) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const ter = + directSendNoFee(view, sender, issuer, amount, !amount.holds(), journal); + if (!isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + return tesSUCCESS; +} + +template <> +inline TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create an escrow + if (issuer == sender) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const ter = lockEscrowMPT(view, sender, amount, journal); + if (!isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + return tesSUCCESS; +} + +template +TER +escrowUnlockPreclaimHelper( + ReadView const& view, + AccountID const& account, + STAmount const& amount, + bool checkFreeze = true); + +template <> +inline TER +escrowUnlockPreclaimHelper( + ReadView const& view, + AccountID const& account, + STAmount const& amount, + bool checkFreeze) +{ + AccountID const& issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tesSUCCESS + if (issuer == account) + return tesSUCCESS; + + // If the issuer has requireAuth set, check if the destination is authorized + if (auto const ter = requireAuth(view, amount.get(), account); !isTesSuccess(ter)) + return ter; + + // If the issuer has deep frozen the destination, return tecFROZEN + if (checkFreeze && + isDeepFrozen(view, account, amount.get().currency, amount.getIssuer())) + return tecFROZEN; + + return tesSUCCESS; +} + +template <> +inline TER +escrowUnlockPreclaimHelper( + ReadView const& view, + AccountID const& account, + STAmount const& amount, + bool checkFreeze) +{ + AccountID const& issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tesSUCCESS + if (issuer == account) + return tesSUCCESS; + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = keylet::mptokenIssuance(amount.get().getMptID()); + auto const sleIssuance = view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the issuer has requireAuth set, check if the account is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth); + !isTesSuccess(ter)) + return ter; + + // If the issuer has frozen the account, return tecLOCKED + if (checkFreeze && isFrozen(view, account, mptIssue)) + return tecLOCKED; + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + template TER escrowUnlockApplyHelper( @@ -58,6 +344,7 @@ escrowUnlockApplyHelper( bool const recvLow = issuer > receiver; bool const senderIssuer = issuer == sender; bool const receiverIssuer = issuer == receiver; + bool const lineExisted = ctx.view.exists(trustLineKey); if (senderIssuer) return tecINTERNAL; // LCOV_EXCL_LINE @@ -65,7 +352,7 @@ escrowUnlockApplyHelper( if (receiverIssuer) return tesSUCCESS; - if (!ctx.view.exists(trustLineKey) && createAsset) + if (!lineExisted && createAsset) { // Can the account cover the trust line's reserve? auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest); diff --git a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h index 6e8cd17f7f0..ab3a2ccb3d1 100644 --- a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h +++ b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -17,12 +18,19 @@ namespace xrpl { * @param slep The SLE for the PayChannel object to close. * @param view The apply view in which ledger state modifications are made. * @param key The ledger key identifying the PayChannel entry. + * @param txAccount The account submitting the transaction that closes the + * channel. * @param j Journal used for fatal-level diagnostic messages. * @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal * fails; tefINTERNAL if the source account SLE cannot be found. */ TER -closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal j); +closeChannel( + SLE::ref slep, + ApplyView& view, + uint256 const& key, + AccountID const& txAccount, + beast::Journal j); /** Add two uint32_t values with saturation at UINT32_MAX. * diff --git a/include/xrpl/protocol/PayChan.h b/include/xrpl/protocol/PayChan.h index d8f4e0f5273..612f8701572 100644 --- a/include/xrpl/protocol/PayChan.h +++ b/include/xrpl/protocol/PayChan.h @@ -1,10 +1,20 @@ #pragma once #include +#include +#include #include +#include +#include +#include +#include +#include #include +#include #include +#include + namespace xrpl { inline void @@ -15,4 +25,70 @@ serializePayChanAuthorization(Serializer& msg, uint256 const& key, XRPAmount con msg.add64(amt.drops()); } +inline void +serializePayChanAuthorization( + Serializer& msg, + uint256 const& key, + IOUAmount const& amt, + Currency const& cur, + AccountID const& iss) +{ + msg.add32(HashPrefix::PaymentChannelClaim); + msg.addBitString(key); + if (amt == beast::kZero) + { + msg.add64(STAmount::kIssuedCurrency); + } + else if (amt.signum() == -1) + { // 512 = not native; the sign is encoded by omitting the 256 flag, so + // the mantissa must be serialized as its absolute value + msg.add64( + static_cast(-amt.mantissa()) | + (static_cast(amt.exponent() + 512 + 97) << (64 - 10))); + } + else + { // 256 = positive + msg.add64( + amt.mantissa() | + (static_cast(amt.exponent() + 512 + 256 + 97) << (64 - 10))); + } + msg.addBitString(cur); + msg.addBitString(iss); +} + +inline void +serializePayChanAuthorization( + Serializer& msg, + uint256 const& key, + MPTAmount const& amt, + MPTID const& mptID, + AccountID const& iss) +{ + msg.add32(HashPrefix::PaymentChannelClaim); + msg.addBitString(key); + msg.add64(amt.value()); + msg.addBitString(mptID); + msg.addBitString(iss); +} + +inline void +serializePayChanAuthorization(Serializer& msg, uint256 const& key, STAmount const& amt) +{ + if (amt.native()) + { + serializePayChanAuthorization(msg, key, amt.xrp()); + } + else if (amt.holds()) + { + serializePayChanAuthorization( + msg, key, amt.iou(), amt.get().currency, amt.get().account); + } + else if (amt.holds()) + { + auto const mpt = amt.get(); + auto const mptID = mpt.getMptID(); + serializePayChanAuthorization(msg, key, amt.mpt(), mptID, amt.getIssuer()); + } +} + } // namespace xrpl diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index bfe03a63037..28ac83faeac 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FEATURE(TokenPaychan, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 90810e06d2d..758656beb77 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -369,6 +369,8 @@ LEDGER_ENTRY(ltPAYCHAN, 0x0078, PayChannel, payment_channel, ({ {sfPreviousTxnID, SoeRequired}, {sfPreviousTxnLgrSeq, SoeRequired}, {sfDestinationNode, SoeOptional}, + {sfTransferRate, SoeOptional}, + {sfIssuerNode, SoeOptional}, })) /** The ledger object which tracks the AMM. diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index e805596c008..1a900537d5b 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -191,7 +191,7 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, NoPriv, ({ {sfDestination, SoeRequired}, - {sfAmount, SoeRequired}, + {sfAmount, SoeRequired, SoeMptSupported}, {sfSettleDelay, SoeRequired}, {sfPublicKey, SoeRequired}, {sfCancelAfter, SoeOptional}, @@ -208,7 +208,7 @@ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, NoPriv, ({ {sfChannel, SoeRequired}, - {sfAmount, SoeRequired}, + {sfAmount, SoeRequired, SoeMptSupported}, {sfExpiration, SoeOptional}, })) @@ -222,8 +222,8 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, NoPriv, ({ {sfChannel, SoeRequired}, - {sfAmount, SoeOptional}, - {sfBalance, SoeOptional}, + {sfAmount, SoeOptional, SoeMptSupported}, + {sfBalance, SoeOptional, SoeMptSupported}, {sfSignature, SoeOptional}, {sfPublicKey, SoeOptional}, {sfCredentialIDs, SoeOptional}, diff --git a/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h b/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h index 61a4e2d044d..ee0f7315882 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h +++ b/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h @@ -287,6 +287,54 @@ class PayChannel : public LedgerEntryBase { return this->sle_->isFieldPresent(sfDestinationNode); } + + /** + * @brief Get sfTransferRate (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getTransferRate() const + { + if (hasTransferRate()) + return this->sle_->at(sfTransferRate); + return std::nullopt; + } + + /** + * @brief Check if sfTransferRate is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasTransferRate() const + { + return this->sle_->isFieldPresent(sfTransferRate); + } + + /** + * @brief Get sfIssuerNode (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getIssuerNode() const + { + if (hasIssuerNode()) + return this->sle_->at(sfIssuerNode); + return std::nullopt; + } + + /** + * @brief Check if sfIssuerNode is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasIssuerNode() const + { + return this->sle_->isFieldPresent(sfIssuerNode); + } }; /** @@ -506,6 +554,28 @@ class PayChannelBuilder : public LedgerEntryBuilderBase return *this; } + /** + * @brief Set sfTransferRate (SoeOptional) + * @return Reference to this builder for method chaining. + */ + PayChannelBuilder& + setTransferRate(std::decay_t const& value) + { + object_[sfTransferRate] = value; + return *this; + } + + /** + * @brief Set sfIssuerNode (SoeOptional) + * @return Reference to this builder for method chaining. + */ + PayChannelBuilder& + setIssuerNode(std::decay_t const& value) + { + object_[sfIssuerNode] = value; + return *this; + } + /** * @brief Build and return the completed PayChannel wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h b/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h index 1db30573661..5dc97ecf0c4 100644 --- a/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h +++ b/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h @@ -60,6 +60,7 @@ class PaymentChannelClaim : public TransactionBase /** * @brief Get sfAmount (SoeOptional) + * @note This field supports MPT (Multi-Purpose Token) amounts. * @return The field value, or std::nullopt if not present. */ [[nodiscard]] @@ -86,6 +87,7 @@ class PaymentChannelClaim : public TransactionBase /** * @brief Get sfBalance (SoeOptional) + * @note This field supports MPT (Multi-Purpose Token) amounts. * @return The field value, or std::nullopt if not present. */ [[nodiscard]] @@ -244,6 +246,7 @@ class PaymentChannelClaimBuilder : public TransactionBuilderBase #include #include +#include #include #include #include #include +#include #include +#include #include #include +#include #include +#include #include #include #include #include #include +#include #include +#include namespace xrpl { TER -closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal j) +closeChannel( + SLE::ref slep, + ApplyView& view, + uint256 const& key, + AccountID const& txAccount, + beast::Journal j) { AccountID const src = (*slep)[sfAccount]; // Remove PayChan from owner directory @@ -38,9 +50,9 @@ closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal } // Remove PayChan from recipient's owner directory, if present. + AccountID const dst = (*slep)[sfDestination]; if (auto const page = (*slep)[~sfDestinationNode]) { - auto const dst = (*slep)[sfDestination]; if (!view.dirRemove(keylet::ownerDir(dst), *page, key, true)) { // LCOV_EXCL_START @@ -57,8 +69,64 @@ closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal XRPL_ASSERT( (*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount"); - (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]; - decreaseOwnerCountForObject(view, sle, slep, 1, j); + + auto const reqDelta = (*slep)[sfAmount] - (*slep)[sfBalance]; + auto const issuer = reqDelta.getIssuer(); + + // Only update the balance if there is a positive delta. + if (reqDelta > beast::kZero) + { + if (isXRP(reqDelta)) + { + (*sle)[sfBalance] = (*sle)[sfBalance] + reqDelta; + } + else + { + if (!view.rules().enabled(featureTokenPaychan)) + return temDISABLED; + + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockPreclaimHelper(view, src, reqDelta, false); + }, + reqDelta.asset().value()); + !isTesSuccess(ret)) + return ret; + + bool const createAsset = src == txAccount; + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockApplyHelper( + view, + kParityRate, + sle, + (*sle)[sfBalance], + reqDelta, + issuer, + src, + src, + createAsset, + j); + }, + reqDelta.asset().value()); + !isTesSuccess(ret)) + return ret; + } + } + + // Remove PayChan from issuer's owner directory, if present. + if (auto const optPage = (*slep)[~sfIssuerNode]; optPage) + { + if (!view.dirRemove(keylet::ownerDir(issuer), *optPage, key, true)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Could not remove paychan from issuer owner directory"; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + } + + adjustOwnerCount(view, sle, -1, j); view.update(sle); // Remove PayChan from ledger diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index 220a7a8e410..646853493e9 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -142,7 +142,8 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref a drops_ -= (*before)[sfBalance].xrp().drops(); break; case ltPAYCHAN: - drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops(); + if (isXRP((*before)[sfAmount])) + drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops(); break; case ltESCROW: if (isXRP((*before)[sfAmount])) @@ -170,7 +171,7 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref a drops_ += (*after)[sfBalance].xrp().drops(); break; case ltPAYCHAN: - if (!isDelete) + if (!isDelete && isXRP((*after)[sfAmount])) drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops(); break; case ltESCROW: diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp index d8f7fcc27db..b015968b24b 100644 --- a/src/libxrpl/tx/invariants/MPTInvariant.cpp +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -375,6 +375,13 @@ ValidMPTIssuance::finalize( return true; } + if (tx.getTxnType() == ttPAYCHAN_CLAIM) + { + if (mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensDeleted_ == 0 && + mptokensCreated_ <= 1) + return true; + } + if (hasPrivilege(tx, MayDeleteMpt) && ((txnType == ttAMM_DELETE && mptokensDeleted_ <= 2) || mptokensDeleted_ == 1) && mptokensCreated_ == 0 && mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0) diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp index feed43d410f..f30aee7749b 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp @@ -5,15 +5,10 @@ #include #include #include -#include -#include -#include #include #include #include #include -#include -#include #include #include #include @@ -33,60 +28,6 @@ EscrowCancel::preflight(PreflightContext const& ctx) return tesSUCCESS; } -template -static TER -escrowCancelPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - STAmount const& amount); - -template <> -TER -escrowCancelPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - STAmount const& amount) -{ - AccountID const& issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tecINTERNAL - if (issuer == account) - return tecINTERNAL; // LCOV_EXCL_LINE - - // If the issuer has requireAuth set, check if the account is authorized - if (auto const ter = requireAuth(ctx.view, amount.get(), account); !isTesSuccess(ter)) - return ter; - - return tesSUCCESS; -} - -template <> -TER -escrowCancelPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - STAmount const& amount) -{ - AccountID const issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tecINTERNAL - if (issuer == account) - return tecINTERNAL; // LCOV_EXCL_LINE - - // If the mpt does not exist, return tecOBJECT_NOT_FOUND - auto const issuanceKey = keylet::mptokenIssuance(amount.get().getMptID()); - auto const sleIssuance = ctx.view.read(issuanceKey); - if (!sleIssuance) - return tecOBJECT_NOT_FOUND; - - // If the issuer has requireAuth set, check if the account is - // authorized - auto const& mptIssue = amount.get(); - if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth); - !isTesSuccess(ter)) - return ter; - - return tesSUCCESS; -} - TER EscrowCancel::preclaim(PreclaimContext const& ctx) { @@ -104,7 +45,7 @@ EscrowCancel::preclaim(PreclaimContext const& ctx) { if (auto const ret = std::visit( [&](T const&) { - return escrowCancelPreclaimHelper(ctx, account, amount); + return escrowUnlockPreclaimHelper(ctx.view, account, amount, false); }, amount.asset().value()); !isTesSuccess(ret)) diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index 50f2e8b8596..ac4db19edc8 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -179,162 +181,6 @@ EscrowCreate::preflight(PreflightContext const& ctx) return tesSUCCESS; } -template -static TER -escrowCreatePreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - AccountID const& dest, - STAmount const& amount); - -template <> -TER -escrowCreatePreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - AccountID const& dest, - STAmount const& amount) -{ - auto const& issue = amount.get(); - AccountID const& issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tecNO_PERMISSION - if (issuer == account) - return tecNO_PERMISSION; - - // If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION - auto const sleIssuer = ctx.view.read(keylet::account(issuer)); - if (!sleIssuer) - return tecNO_ISSUER; - if (!sleIssuer->isFlag(lsfAllowTrustLineLocking)) - return tecNO_PERMISSION; - - // If the account does not have a trustline to the issuer, return tecNO_LINE - auto const sleRippleState = ctx.view.read(keylet::trustLine(account, issuer, issue.currency)); - if (!sleRippleState) - return tecNO_LINE; - - STAmount const balance = (*sleRippleState)[sfBalance]; - - // If balance is positive, issuer must have higher address than account - if (balance > beast::kZero && issuer < account) - return tecNO_PERMISSION; // LCOV_EXCL_LINE - - // If balance is negative, issuer must have lower address than account - if (balance < beast::kZero && issuer > account) - return tecNO_PERMISSION; // LCOV_EXCL_LINE - - // If the issuer has requireAuth set, check if the account is authorized - if (auto const ter = requireAuth(ctx.view, issue, account); !isTesSuccess(ter)) - return ter; - - // If the issuer has requireAuth set, check if the destination is authorized - if (auto const ter = requireAuth(ctx.view, issue, dest); !isTesSuccess(ter)) - return ter; - - // If the issuer has frozen the account, return tecFROZEN - if (isFrozen(ctx.view, account, issue)) - return tecFROZEN; - - // If the issuer has frozen the destination, return tecFROZEN - if (isFrozen(ctx.view, dest, issue)) - return tecFROZEN; - - STAmount const spendableAmount = accountHolds( - ctx.view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, ctx.j); - - // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS - if (spendableAmount <= beast::kZero) - return tecINSUFFICIENT_FUNDS; - - // If the spendable amount is less than the amount, return - // tecINSUFFICIENT_FUNDS - if (spendableAmount < amount) - return tecINSUFFICIENT_FUNDS; - - // If the amount is not addable to the balance, return tecPRECISION_LOSS - if (!canAdd(spendableAmount, amount)) - return tecPRECISION_LOSS; - - return tesSUCCESS; -} - -template <> -TER -escrowCreatePreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - AccountID const& dest, - STAmount const& amount) -{ - AccountID const issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tecNO_PERMISSION - if (issuer == account) - return tecNO_PERMISSION; - - // If the mpt does not exist, return tecOBJECT_NOT_FOUND - auto const issuanceKey = keylet::mptokenIssuance(amount.get().getMptID()); - auto const sleIssuance = ctx.view.read(issuanceKey); - if (!sleIssuance) - return tecOBJECT_NOT_FOUND; - - // If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION - if (!sleIssuance->isFlag(lsfMPTCanEscrow)) - return tecNO_PERMISSION; - - // If the issuer is not the same as the issuer of the mpt, return - // tecNO_PERMISSION - if (sleIssuance->getAccountID(sfIssuer) != issuer) - return tecNO_PERMISSION; // LCOV_EXCL_LINE - - // If the account does not have the mpt, return tecOBJECT_NOT_FOUND - if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account))) - return tecOBJECT_NOT_FOUND; - - // If the issuer has requireAuth set, check if the account is - // authorized - auto const& mptIssue = amount.get(); - if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth); - !isTesSuccess(ter)) - return ter; - - // If the issuer has requireAuth set, check if the destination is - // authorized - if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth); - !isTesSuccess(ter)) - return ter; - - // If the issuer has frozen the account, return tecLOCKED - if (isFrozen(ctx.view, account, mptIssue)) - return tecLOCKED; - - // If the issuer has frozen the destination, return tecLOCKED - if (isFrozen(ctx.view, dest, mptIssue)) - return tecLOCKED; - - // If the mpt cannot be transferred, return tecNO_AUTH - if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); !isTesSuccess(ter)) - return ter; - - STAmount const spendableAmount = accountHolds( - ctx.view, - account, - amount.get(), - FreezeHandling::IgnoreFreeze, - AuthHandling::IgnoreAuth, - ctx.j); - - // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS - if (spendableAmount <= beast::kZero) - return tecINSUFFICIENT_FUNDS; - - // If the spendable amount is less than the amount, return - // tecINSUFFICIENT_FUNDS - if (spendableAmount < amount) - return tecINSUFFICIENT_FUNDS; - - return tesSUCCESS; -} - TER EscrowCreate::preclaim(PreclaimContext const& ctx) { @@ -360,7 +206,7 @@ EscrowCreate::preclaim(PreclaimContext const& ctx) if (auto const ret = std::visit( [&](T const&) { - return escrowCreatePreclaimHelper(ctx, account, dest, amount); + return escrowLockPreclaimHelper(ctx.view, account, dest, amount, ctx.j); }, amount.asset().value()); !isTesSuccess(ret)) @@ -369,54 +215,6 @@ EscrowCreate::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } -template -static TER -escrowLockApplyHelper( - ApplyView& view, - AccountID const& issuer, - AccountID const& sender, - STAmount const& amount, - beast::Journal journal); - -template <> -TER -escrowLockApplyHelper( - ApplyView& view, - AccountID const& issuer, - AccountID const& sender, - STAmount const& amount, - beast::Journal journal) -{ - // Defensive: Issuer cannot create an escrow - if (issuer == sender) - return tecINTERNAL; // LCOV_EXCL_LINE - - auto const ter = - directSendNoFee(view, sender, issuer, amount, !amount.holds(), journal); - if (!isTesSuccess(ter)) - return ter; // LCOV_EXCL_LINE - return tesSUCCESS; -} - -template <> -TER -escrowLockApplyHelper( - ApplyView& view, - AccountID const& issuer, - AccountID const& sender, - STAmount const& amount, - beast::Journal journal) -{ - // Defensive: Issuer cannot create an escrow - if (issuer == sender) - return tecINTERNAL; // LCOV_EXCL_LINE - - auto const ter = lockEscrowMPT(view, sender, amount, journal); - if (!isTesSuccess(ter)) - return ter; // LCOV_EXCL_LINE - return tesSUCCESS; -} - TER EscrowCreate::doApply() { diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp index 8bc98c7aa8e..4769aac6296 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -12,15 +12,10 @@ #include #include #include -#include -#include -#include #include #include #include #include -#include -#include #include #include #include @@ -129,68 +124,6 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx) return Transactor::calculateBaseFee(view, tx) + extraFee; } -template -static TER -escrowFinishPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& dest, - STAmount const& amount); - -template <> -TER -escrowFinishPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& dest, - STAmount const& amount) -{ - AccountID const& issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tesSUCCESS - if (issuer == dest) - return tesSUCCESS; - - // If the issuer has requireAuth set, check if the destination is authorized - if (auto const ter = requireAuth(ctx.view, amount.get(), dest); !isTesSuccess(ter)) - return ter; - - // If the issuer has deep frozen the destination, return tecFROZEN - if (isDeepFrozen(ctx.view, dest, amount.get().currency, amount.getIssuer())) - return tecFROZEN; - - return tesSUCCESS; -} - -template <> -TER -escrowFinishPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& dest, - STAmount const& amount) -{ - AccountID const& issuer = amount.getIssuer(); - // If the issuer is the same as the dest, return tesSUCCESS - if (issuer == dest) - return tesSUCCESS; - - // If the mpt does not exist, return tecOBJECT_NOT_FOUND - auto const issuanceKey = keylet::mptokenIssuance(amount.get().getMptID()); - auto const sleIssuance = ctx.view.read(issuanceKey); - if (!sleIssuance) - return tecOBJECT_NOT_FOUND; - - // If the issuer has requireAuth set, check if the destination is - // authorized - auto const& mptIssue = amount.get(); - if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth); - !isTesSuccess(ter)) - return ter; - - // If the issuer has frozen the destination, return tecLOCKED - if (isFrozen(ctx.view, dest, mptIssue)) - return tecLOCKED; - - return tesSUCCESS; -} - TER EscrowFinish::preclaim(PreclaimContext const& ctx) { @@ -215,7 +148,7 @@ EscrowFinish::preclaim(PreclaimContext const& ctx) { if (auto const ret = std::visit( [&](T const&) { - return escrowFinishPreclaimHelper(ctx, dest, amount); + return escrowUnlockPreclaimHelper(ctx.view, dest, amount); }, amount.asset().value()); !isTesSuccess(ret)) diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp index b8118bc49f9..d6bca22e470 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp @@ -4,14 +4,17 @@ #include #include #include +#include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -24,6 +27,7 @@ #include #include +#include namespace xrpl { @@ -46,11 +50,19 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx) return temMALFORMED; auto const bal = ctx.tx[~sfBalance]; - if (bal && (!isXRP(*bal) || *bal <= beast::kZero)) + if (bal && *bal <= beast::kZero) return temBAD_AMOUNT; auto const amt = ctx.tx[~sfAmount]; - if (amt && (!isXRP(*amt) || *amt <= beast::kZero)) + if (amt && *amt <= beast::kZero) + return temBAD_AMOUNT; + + if (((bal && !isXRP(*bal)) || (amt && !isXRP(*amt))) && !ctx.rules.enabled(featureTokenPaychan)) + return temBAD_AMOUNT; + + // Both bal and amt must reference the same asset before comparing, + // otherwise STAmount comparison throws. + if (bal && amt && bal->asset() != amt->asset()) return temBAD_AMOUNT; if (bal && amt && *bal > *amt) @@ -70,8 +82,8 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx) // The signature isn't needed if txAccount == src, but if it's // present, check it - auto const reqBalance = bal->xrp(); - auto const authAmt = amt ? amt->xrp() : reqBalance; + auto const reqBalance = *bal; + auto const authAmt = amt ? *amt : reqBalance; if (reqBalance > authAmt) return temBAD_AMOUNT; @@ -96,12 +108,33 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx) TER PaymentChannelClaim::preclaim(PreclaimContext const& ctx) { - if (!ctx.view.rules().enabled(featureCredentials)) - return Transactor::preclaim(ctx); + if (ctx.view.rules().enabled(featureCredentials)) + { + if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j); + !isTesSuccess(err)) + return err; + } - if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j); - !isTesSuccess(err)) - return err; + if (ctx.view.rules().enabled(featureTokenPaychan)) + { + Keylet const k(ltPAYCHAN, ctx.tx[sfChannel]); + auto const slep = ctx.view.read(k); + if (!slep) + return tecNO_TARGET; + + AccountID const dest = (*slep)[sfDestination]; + STAmount const amount = (*slep)[sfAmount]; + if (!isXRP(amount) && ctx.tx.isFieldPresent(sfBalance)) + { + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockPreclaimHelper(ctx.view, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + } return tesSUCCESS; } @@ -116,25 +149,32 @@ PaymentChannelClaim::doApply() AccountID const src = (*slep)[sfAccount]; AccountID const dst = (*slep)[sfDestination]; - AccountID const txAccount = ctx_.tx[sfAccount]; auto const curExpiration = (*slep)[~sfExpiration]; if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) || isChannelExpired(ctx_.view(), curExpiration)) { - return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); + return closeChannel( + slep, ctx_.view(), k.key, accountID_, ctx_.registry.get().getJournal("View")); } - if (txAccount != src && txAccount != dst) + if (accountID_ != src && accountID_ != dst) return tecNO_PERMISSION; if (ctx_.tx[~sfBalance]) { - auto const chanBalance = slep->getFieldAmount(sfBalance).xrp(); - auto const chanFunds = slep->getFieldAmount(sfAmount).xrp(); - auto const reqBalance = ctx_.tx[sfBalance].xrp(); + auto const chanBalance = slep->getFieldAmount(sfBalance); + auto const chanFunds = slep->getFieldAmount(sfAmount); + auto const reqBalance = ctx_.tx[sfBalance]; - if (txAccount == dst && !ctx_.tx[~sfSignature]) + // The requested balance must match the channel's asset; otherwise + // STAmount comparisons/subtractions below would throw. + if (reqBalance.asset() != chanFunds.asset()) + return temBAD_AMOUNT; + if (auto const reqAmt = ctx_.tx[~sfAmount]; reqAmt && reqAmt->asset() != chanFunds.asset()) + return temBAD_AMOUNT; + + if (accountID_ == dst && !ctx_.tx[~sfSignature]) { return ctx_.view().rules().enabled(fixCleanup3_2_0) ? TER{tecNO_PERMISSION} : TER{temBAD_SIGNATURE}; @@ -164,22 +204,56 @@ PaymentChannelClaim::doApply() return tecNO_DST; if (auto err = - verifyDepositPreauth(ctx_.tx, ctx_.view(), txAccount, dst, sled, ctx_.journal); + verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, dst, sled, ctx_.journal); !isTesSuccess(err)) return err; (*slep)[sfBalance] = ctx_.tx[sfBalance]; - XRPAmount const reqDelta = reqBalance - chanBalance; + STAmount const reqDelta = reqBalance - chanBalance; XRPL_ASSERT( reqDelta >= beast::kZero, "xrpl::PaymentChannelClaim::doApply : minimum balance delta"); - (*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta; + + // Transfer amount to destination + if (isXRP(reqDelta)) + { + (*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta; + } + else + { + if (!ctx_.view().rules().enabled(featureTokenPaychan)) + return temDISABLED; + + Rate lockedRate = slep->isFieldPresent(sfTransferRate) + ? xrpl::Rate(slep->getFieldU32(sfTransferRate)) + : kParityRate; + auto const issuer = reqDelta.getIssuer(); + bool const createAsset = dst == accountID_; + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockApplyHelper( + ctx_.view(), + lockedRate, + sled, + preFeeBalance_, + reqDelta, + issuer, + src, + dst, + createAsset, + j_); + }, + reqDelta.asset().value()); + !isTesSuccess(ret)) + return ret; + } + ctx_.view().update(sled); ctx_.view().update(slep); } if (ctx_.tx.isFlag(tfRenew)) { - if (src != txAccount) + if (src != accountID_) return tecNO_PERMISSION; (*slep)[~sfExpiration] = std::nullopt; ctx_.view().update(slep); @@ -188,8 +262,11 @@ PaymentChannelClaim::doApply() if (ctx_.tx.isFlag(tfClose)) { // Channel will close immediately if dry or the receiver closes - if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount]) - return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); + if (dst == accountID_ || (*slep)[sfBalance] == (*slep)[sfAmount]) + { + return closeChannel( + slep, ctx_.view(), k.key, accountID_, ctx_.registry.get().getJournal("View")); + } auto const settleExpiration = saturatingAdd( ctx_.view().rules(), diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp index b17430948a9..2f4025fd0de 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp @@ -1,28 +1,40 @@ #include #include +#include #include #include #include #include #include #include +#include #include +#include +#include +#include #include #include +#include #include #include +#include +#include +#include #include +#include #include #include #include #include #include +#include #include #include #include #include +#include namespace xrpl { @@ -48,17 +60,74 @@ namespace xrpl { //------------------------------------------------------------------------------ +template +static NotTEC +payChanCreatePreflightHelper(PreflightContext const& ctx); + +template <> +NotTEC +payChanCreatePreflightHelper(PreflightContext const& ctx) +{ + STAmount const amount = ctx.tx[sfAmount]; + if (amount.native() || amount <= beast::kZero) + return temBAD_AMOUNT; + + if (badCurrency() == amount.get().currency) + return temBAD_CURRENCY; + + return tesSUCCESS; +} + +template <> +NotTEC +payChanCreatePreflightHelper(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(fixCleanup3_2_0) && !ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + auto const amount = ctx.tx[sfAmount]; + if (amount.native() || amount.mpt() > MPTAmount{kMaxMpTokenAmount} || amount <= beast::kZero) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + TxConsequences PaymentChannelCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::kZero}; +} + +bool +PaymentChannelCreate::checkExtraFeatures(PreflightContext const& ctx) +{ + // Only require featureMPTokensV1 when the channel amount is an MPT and + // fixCleanup3_2_0 is active; XRP/IOU channels are unaffected by this gate. + if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfAmount].holds()) + return ctx.rules.enabled(featureMPTokensV1); + return true; } NotTEC PaymentChannelCreate::preflight(PreflightContext const& ctx) { - if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::kZero)) - return temBAD_AMOUNT; + STAmount const amount{ctx.tx[sfAmount]}; + if (!isXRP(amount)) + { + if (!ctx.rules.enabled(featureTokenPaychan)) + return temBAD_AMOUNT; + + if (auto const ret = std::visit( + [&](T const&) { return payChanCreatePreflightHelper(ctx); }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + else + { + if (amount <= beast::kZero) + return temBAD_AMOUNT; + } if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) return temDST_IS_SRC; @@ -77,6 +146,8 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx) if (!sle) return terNO_ACCOUNT; + STAmount const amount{ctx.tx[sfAmount]}; + // Check reserve and funds availability if (!ctx.view.rules().enabled(featureSponsor)) { @@ -86,15 +157,15 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx) if (balance < reserve) return tecINSUFFICIENT_RESERVE; - if (balance < reserve + ctx.tx[sfAmount]) + if (isXRP(amount) && balance < reserve + ctx.tx[sfAmount]) return tecUNFUNDED; } - auto const dst = ctx.tx[sfDestination]; + auto const dest = ctx.tx[sfDestination]; { // Check destination account - auto const sled = ctx.view.read(keylet::account(dst)); + auto const sled = ctx.view.read(keylet::account(dest)); if (!sled) return tecNO_DST; @@ -115,6 +186,17 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } + if (!isXRP(amount)) + { + if (auto const ret = std::visit( + [&](T const&) { + return escrowLockPreclaimHelper(ctx.view, account, dest, amount, ctx.j); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + return tesSUCCESS; } @@ -122,6 +204,7 @@ TER PaymentChannelCreate::doApply() { auto const account = ctx_.tx[sfAccount]; + STAmount const amount{ctx_.tx[sfAmount]}; auto const sle = ctx_.view().peek(keylet::account(account)); if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE @@ -163,21 +246,21 @@ PaymentChannelCreate::doApply() return tecUNFUNDED; } - auto const dst = ctx_.tx[sfDestination]; + auto const dest = ctx_.tx[sfDestination]; // Create PayChan in ledger. // // Note that we use the value from the sequence or ticket as the // payChan sequence. For more explanation see comments in SeqProxy.h. - Keylet const payChanKeylet = keylet::payChannel(account, dst, ctx_.tx.getSeqValue()); + Keylet const payChanKeylet = keylet::payChannel(account, dest, ctx_.tx.getSeqValue()); auto const slep = std::make_shared(payChanKeylet); // Funds held in this channel - (*slep)[sfAmount] = ctx_.tx[sfAmount]; + (*slep)[sfAmount] = amount; // Amount channel has already paid - (*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed(); + (*slep)[sfBalance] = amount.zeroed(); (*slep)[sfAccount] = account; - (*slep)[sfDestination] = dst; + (*slep)[sfDestination] = dest; (*slep)[sfSettleDelay] = ctx_.tx[sfSettleDelay]; (*slep)[sfPublicKey] = ctx_.tx[sfPublicKey]; (*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter]; @@ -188,6 +271,13 @@ PaymentChannelCreate::doApply() (*slep)[sfSequence] = ctx_.tx.getSeqValue(); } + if (ctx_.view().rules().enabled(featureTokenPaychan) && !isXRP(amount)) + { + auto const xferRate = transferRate(ctx_.view(), amount); + if (xferRate != kParityRate) + (*slep)[sfTransferRate] = xferRate.value; + } + ctx_.view().insert(slep); // Add PayChan to owner directory @@ -202,14 +292,39 @@ PaymentChannelCreate::doApply() // Add PayChan to the recipient's owner directory { auto const page = - ctx_.view().dirInsert(keylet::ownerDir(dst), payChanKeylet, describeOwnerDir(dst)); + ctx_.view().dirInsert(keylet::ownerDir(dest), payChanKeylet, describeOwnerDir(dest)); if (!page) return tecDIR_FULL; // LCOV_EXCL_LINE (*slep)[sfDestinationNode] = *page; } + // Add PayChan to the issuer's owner directory, if applicable + AccountID const issuer = amount.getIssuer(); + if (!isXRP(amount) && issuer != accountID_ && issuer != dest && !amount.holds()) + { + auto page = ctx_.view().dirInsert( + keylet::ownerDir(issuer), payChanKeylet, describeOwnerDir(issuer)); + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*slep)[sfIssuerNode] = *page; + } + // Deduct owner's balance, increment owner count - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + if (isXRP(amount)) + { + (*sle)[sfBalance] = (*sle)[sfBalance] - amount; + } + else + { + if (auto const ret = std::visit( + [&](T const&) { + return escrowLockApplyHelper(ctx_.view(), issuer, accountID_, amount, j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal); addSponsorToLedgerEntry(ctx_.getApplyViewContext(), slep); ctx_.view().update(sle); diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp index 1d1cf07e252..60fd95e3661 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp @@ -5,27 +5,78 @@ #include #include #include +#include #include #include +#include #include #include #include #include +#include +#include +#include #include #include #include #include #include +#include #include #include #include +#include +#include + namespace xrpl { +template +static NotTEC +payChanFundPreflightHelper(PreflightContext const& ctx); + +template <> +NotTEC +payChanFundPreflightHelper(PreflightContext const& ctx) +{ + STAmount const amount = ctx.tx[sfAmount]; + if (amount.native() || amount <= beast::kZero) + return temBAD_AMOUNT; + + if (badCurrency() == amount.get().currency) + return temBAD_CURRENCY; + + return tesSUCCESS; +} + +template <> +NotTEC +payChanFundPreflightHelper(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(fixCleanup3_2_0) && !ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + auto const amount = ctx.tx[sfAmount]; + if (amount.native() || amount.mpt() > MPTAmount{kMaxMpTokenAmount} || amount <= beast::kZero) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + TxConsequences PaymentChannelFund::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::kZero}; +} + +bool +PaymentChannelFund::checkExtraFeatures(PreflightContext const& ctx) +{ + // Only require featureMPTokensV1 when the funding amount is an MPT and + // fixCleanup3_2_0 is active; XRP/IOU channels are unaffected by this gate. + if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfAmount].holds()) + return ctx.rules.enabled(featureMPTokensV1); + return true; } NotTEC @@ -34,8 +85,23 @@ PaymentChannelFund::preflight(PreflightContext const& ctx) if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfChannel] == beast::kZero) return temMALFORMED; - if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::kZero)) - return temBAD_AMOUNT; + STAmount const amount{ctx.tx[sfAmount]}; + if (!isXRP(amount)) + { + if (!ctx.rules.enabled(featureTokenPaychan)) + return temBAD_AMOUNT; + + if (auto const ret = std::visit( + [&](T const&) { return payChanFundPreflightHelper(ctx); }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + else + { + if (amount <= beast::kZero) + return temBAD_AMOUNT; + } return tesSUCCESS; } @@ -49,16 +115,17 @@ PaymentChannelFund::doApply() return tecNO_ENTRY; AccountID const src = (*slep)[sfAccount]; - auto const txAccount = ctx_.tx[sfAccount]; + AccountID const dst = (*slep)[sfDestination]; auto const curExpiration = (*slep)[~sfExpiration]; if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) || isChannelExpired(ctx_.view(), curExpiration)) { - return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); + return closeChannel( + slep, ctx_.view(), k.key, accountID_, ctx_.registry.get().getJournal("View")); } - if (src != txAccount) + if (src != accountID_) { // only the owner can add funds or extend return tecNO_PERMISSION; @@ -82,10 +149,19 @@ PaymentChannelFund::doApply() ctx_.view().update(slep); } - auto const sle = ctx_.view().peek(keylet::account(txAccount)); + auto const sle = ctx_.view().peek(keylet::account(accountID_)); if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE + STAmount const amount{ctx_.tx[sfAmount]}; + STAmount const chanAmt{(*slep)[sfAmount]}; + + // The funded asset must match the channel's asset. Without this check + // STAmount arithmetic below (chanAmt + amount) would throw on + // mismatched issues. + if (amount.asset() != chanAmt.asset()) + return temBAD_AMOUNT; + { // Check reserve and funds availability STAmount const balance = (*sle)[sfBalance]; @@ -99,20 +175,56 @@ PaymentChannelFund::doApply() // when a sponsor is present and would ignore the source's post-lock // balance entirely. Funding an existing channel adds no owned object, // so there is no owner-count delta. - if (balance < accountReserve(ctx_.view(), sle, j_) + ctx_.tx[sfAmount]) + if (isXRP(amount) && balance < accountReserve(ctx_.view(), sle, j_) + amount) return tecUNFUNDED; } // do not allow adding funds if dst does not exist - if (AccountID const dst = (*slep)[sfDestination]; !ctx_.view().read(keylet::account(dst))) + if (!ctx_.view().read(keylet::account(dst))) { return tecNO_DST; } - (*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount]; - ctx_.view().update(slep); + if (!isXRP(amount)) + { + // Funding is subject to the same issuer controls (locking opt-in, + // authorization, freeze/lock, transferability, spendable balance) + // as channel creation. + if (auto const ret = std::visit( + [&](T const&) { + return escrowLockPreclaimHelper(ctx_.view(), accountID_, dst, amount, j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + + // Guard the channel-amount accumulation itself: a small IOU amount + // added to a much larger channel amount would be rounded away below + // after the source had already been debited. MPT amounts are exact + // integers bounded by the outstanding supply, so only IOUs can lose + // precision here. + if (amount.holds() && !canAdd(chanAmt, amount)) + return tecPRECISION_LOSS; + } + + if (isXRP(amount)) + { + (*sle)[sfBalance] = (*sle)[sfBalance] - amount; + } + else + { + AccountID const issuer = amount.getIssuer(); + if (auto const ret = std::visit( + [&](T const&) { + return escrowLockApplyHelper(ctx_.view(), issuer, accountID_, amount, j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + (*slep)[sfAmount] = chanAmt + amount; + ctx_.view().update(slep); ctx_.view().update(sle); return tesSUCCESS; diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index bf0bc5c7d77..7f2984c5eca 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -4194,7 +4195,7 @@ struct AMMMPT_test : public jtx::AMMTest settleDelay, pk, cancelAfter), - Ter(telENV_RPC_FAILED)); + Ter(tecNO_PERMISSION)); env(create(carol_, ammAlice.ammAccount(), XRP(1'000), settleDelay, pk, cancelAfter), Ter(tecNO_PERMISSION)); diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index f19743026c4..74d192293b5 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 7543db73640..ef5676f749e 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1978,35 +1978,6 @@ class MPToken_test : public beast::unit_test::Suite }; ammBid(sfBidMin); ammBid(sfBidMax); - // PaymentChannelCreate - { - json::Value jv; - jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Account] = alice.human(); - jv[jss::Destination] = carol.human(); - jv[jss::SettleDelay] = 1; - jv[sfPublicKey.fieldName] = strHex(alice.pk().slice()); - jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None); - test(jv, jss::Amount.cStr()); - } - // PaymentChannelFund - { - json::Value jv; - jv[jss::TransactionType] = jss::PaymentChannelFund; - jv[jss::Account] = alice.human(); - jv[sfChannel.fieldName] = to_string(uint256{1}); - jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None); - test(jv, jss::Amount.cStr()); - } - // PaymentChannelClaim - { - json::Value jv; - jv[jss::TransactionType] = jss::PaymentChannelClaim; - jv[jss::Account] = alice.human(); - jv[sfChannel.fieldName] = to_string(uint256{1}); - jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None); - test(jv, jss::Amount.cStr()); - } // NFTokenCreateOffer { json::Value jv; diff --git a/src/test/app/PayChanToken_test.cpp b/src/test/app/PayChanToken_test.cpp new file mode 100644 index 00000000000..61ad97cc2ec --- /dev/null +++ b/src/test/app/PayChanToken_test.cpp @@ -0,0 +1,3902 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl::test { +struct PayChanToken_test : public beast::unit_test::Suite +{ + void + testIOUEnablement(FeatureBitset features) + { + testcase("IOU Enablement"); + + using namespace jtx; + using namespace std::chrono; + + for (bool const withTokenPaychan : {false, true}) + { + auto const amend = withTokenPaychan ? features : features - featureTokenPaychan; + Env env{*this, amend}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(10'000), alice, bob); + env.close(); + env(pay(gw, alice, usd(5'000))); + env(pay(gw, bob, usd(5'000))); + env.close(); + + auto const openResult = withTokenPaychan ? Ter(tesSUCCESS) : Ter(temBAD_AMOUNT); + auto const closeResult = withTokenPaychan ? Ter(tesSUCCESS) : Ter(tecNO_TARGET); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk), openResult); + env.close(); + env(paychan::fund(alice, chan, usd(1'000)), openResult); + env.close(); + env(paychan::claim(bob, chan), Txflags(tfClose), closeResult); + env.close(); + } + } + + void + testIOUAllowLockingFlag(FeatureBitset features) + { + testcase("IOU Allow Locking Flag"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(10'000), alice, bob); + env.close(); + env(pay(gw, alice, usd(5'000))); + env(pay(gw, bob, usd(5'000))); + env.close(); + + // Create PayChan + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + // Clear the asfAllowTrustLineLocking flag + env(fclear(gw, asfAllowTrustLineLocking)); + env.close(); + env.require(Nflags(gw, asfAllowTrustLineLocking)); + + // Cannot Create PayChan without asfAllowTrustLineLocking + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk), Ter(tecNO_PERMISSION)); + env.close(); + + // Cannot Fund PayChan without asfAllowTrustLineLocking; funding is + // subject to the same issuer controls as create + env(paychan::fund(alice, chan, usd(1'000)), Ter(tecNO_PERMISSION)); + env.close(); + + // Can claim the paychan created before the flag was cleared + auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, usd(1'000)); + env(paychan::claim(bob, chan, usd(1'000), usd(1'000), Slice(sig), alice.pk()), + Ter(tesSUCCESS)); + env.close(); + } + + void + testIOUCreatePreflight(FeatureBitset features) + { + testcase("IOU Create Preflight"); + using namespace test::jtx; + using namespace std::literals; + + // temBAD_FEE: Exercises invalid preflight1. + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5'000), alice, bob, gw); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, usd(1), settleDelay, pk), + Fee(XRP(-1)), + Ter(temBAD_FEE)); + env.close(); + } + + // temBAD_AMOUNT: amount <= 0 + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5'000), alice, bob, gw); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, usd(-1), settleDelay, pk), Ter(temBAD_AMOUNT)); + env.close(); + } + + // temBAD_CURRENCY: badCurrency() == amount.getCurrency() + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const bad = IOU(gw, badCurrency()); + env.fund(XRP(5'000), alice, bob, gw); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, bad(1), settleDelay, pk), Ter(temBAD_CURRENCY)); + env.close(); + } + } + + void + testIOUCreatePreclaim(FeatureBitset features) + { + testcase("IOU Create Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_PERMISSION: issuer is the same as the account + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + + env(paychan::create(gw, alice, usd(1), 100s, alice.pk()), Ter(tecNO_PERMISSION)); + env.close(); + } + + // tecNO_ISSUER: Issuer does not exist + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5000), alice, bob); + env.close(); + env.memoize(gw); + + env(paychan::create(alice, bob, usd(1), 100s, alice.pk()), Ter(tecNO_ISSUER)); + env.close(); + } + + // tecNO_PERMISSION: asfAllowTrustLineLocking is not set + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(usd(10'000), alice, bob); + env.close(); + env(pay(gw, alice, usd(5000))); + env(pay(gw, bob, usd(5000))); + env.close(); + + env(paychan::create(gw, alice, usd(1), 100s, alice.pk()), Ter(tecNO_PERMISSION)); + env.close(); + } + + // tecNO_LINE: account does not have a trustline to the issuer + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + env(paychan::create(alice, bob, usd(1), 100s, alice.pk()), Ter(tecNO_LINE)); + env.close(); + } + + // tecNO_PERMISSION: Not testable + // tecNO_PERMISSION: Not testable + // tecNO_AUTH: requireAuth + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env.trust(usd(10'000), alice, bob); + env.close(); + + env(paychan::create(alice, bob, usd(1), 100s, alice.pk()), Ter(tecNO_AUTH)); + env.close(); + } + + // tecNO_AUTH: requireAuth + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + auto const aliceUSD = alice["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), Txflags(tfSetfAuth)); + env.trust(usd(10'000), alice, bob); + env.close(); + + env(paychan::create(alice, bob, usd(1), 100s, alice.pk()), Ter(tecNO_AUTH)); + env.close(); + } + + // tecFROZEN: account is frozen + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, usd(100'000))); + env(trust(bob, usd(100'000))); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, usd(10'000), alice, tfSetFreeze)); + env.close(); + + env(paychan::create(alice, bob, usd(1), 100s, alice.pk()), Ter(tecFROZEN)); + env.close(); + } + + // tecFROZEN: dest is frozen + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, usd(100'000))); + env(trust(bob, usd(100'000))); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + // set freeze on bob trustline + env(trust(gw, usd(10'000), bob, tfSetFreeze)); + env.close(); + + env(paychan::create(alice, bob, usd(1), 100s, alice.pk()), Ter(tecFROZEN)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, usd(100'000))); + env(trust(bob, usd(100'000))); + env.close(); + + env(paychan::create(alice, bob, usd(1), 100s, alice.pk()), Ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, usd(100'000))); + env(trust(bob, usd(100'000))); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + env(paychan::create(alice, bob, usd(10'001), 100s, alice.pk()), + Ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecPRECISION_LOSS + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100000000000000000), alice); + env.trust(usd(100000000000000000), bob); + env.close(); + env(pay(gw, alice, usd(10000000000000000))); + env(pay(gw, bob, usd(1))); + env.close(); + + // With a larger mantissa the amounts remain addable + bool const largeMantissa = + features[featureSingleAssetVault] || features[featureLendingProtocol]; + + // alice cannot create paychan for 1/10 iou - precision loss + env(paychan::create(alice, bob, usd(1), 100s, alice.pk()), + Ter(largeMantissa ? (TER)tesSUCCESS : (TER)tecPRECISION_LOSS)); + env.close(); + } + } + + void + testIOUClaimPreclaim(FeatureBitset features) + { + testcase("IOU Claim Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), Txflags(tfSetfAuth)); + env(trust(gw, bobUSD(10'000)), Txflags(tfSetfAuth)); + env.trust(usd(10'000), alice, bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, usd(1), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + env(pay(bob, gw, usd(10'000))); + env(trust(gw, bobUSD(0)), Txflags(tfSetfAuth)); + env(trust(bob, usd(0))); + env.close(); + + env.trust(usd(10'000), bob); + env.close(); + + // bob cannot claim because he is not authorized + auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, usd(1)); + env(paychan::claim(bob, chan, usd(1), usd(1), Slice(sig), alice.pk()), Ter(tecNO_AUTH)); + env.close(); + } + + // tecFROZEN: issuer has deep frozen the dest + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(10'000), alice, bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, usd(1), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, usd(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + + // bob cannot claim because of deep freeze + auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, usd(1)); + env(paychan::claim(bob, chan, usd(1), usd(1), Slice(sig), alice.pk()), Ter(tecFROZEN)); + env.close(); + } + } + + void + testIOUClaimDoApply(FeatureBitset features) + { + testcase("IOU Claim Do Apply"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_LINE_INSUF_RESERVE: insufficient reserve to create line + { + Env env{*this, features}; + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5000), alice, gw); + env.fund(acctReserve + (incReserve - 1), bob); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(10'000), alice); + env.close(); + env(pay(gw, alice, usd(10'000))); + env.close(); + + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, usd(1), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + // bob cannot claim because insufficient reserve to create line + auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, usd(1)); + env(paychan::claim(bob, chan, usd(1), usd(1), Slice(sig), alice.pk()), + Ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + } + + // tecNO_LINE: alice submits; claim IOU not created + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(10'000), alice); + env.close(); + env(pay(gw, alice, usd(10'000))); + env.close(); + + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, usd(1), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + // alice cannot claim because bob does not have a trustline + env(paychan::claim(alice, chan, usd(1), usd(1)), Ter(tecNO_LINE)); + env.close(); + } + + // tecLIMIT_EXCEEDED: alice submits; IOU Limit < balance + amount + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(1000), alice, bob); + env.close(); + env(pay(gw, alice, usd(1000))); + env.close(); + + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, usd(5), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + env.trust(usd(1), bob); + env.close(); + + // alice cannot claim because bobs limit is too low + env(paychan::claim(alice, chan, usd(5), usd(5)), Ter(tecLIMIT_EXCEEDED)); + env.close(); + } + + // tesSUCCESS: bob submits; IOU Limit < balance + amount + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(1000), alice, bob); + env.close(); + env(pay(gw, alice, usd(1000))); + env.close(); + + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, usd(5), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + env.trust(usd(1), bob); + env.close(); + + auto const bobPreLimit = env.limit(bob, usd); + + // bob can claim even if bobs limit is too low; the limit only + // protects against unsolicited holdings and bob consents by + // submitting the claim himself + auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, usd(5)); + env(paychan::claim(bob, chan, usd(5), usd(5), Slice(sig), alice.pk()), Ter(tesSUCCESS)); + env.close(); + + // bob received the claimed amount; his limit is not changed + BEAST_EXPECT(env.balance(bob, usd) == usd(5)); + BEAST_EXPECT(env.limit(bob, usd) == bobPreLimit); + } + } + + void + testIOUBalances(FeatureBitset features) + { + testcase("IOU Balances"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(10'000), alice, bob); + env.close(); + env(pay(gw, alice, usd(5'000))); + env(pay(gw, bob, usd(5'000))); + env.close(); + + auto const outstandingUSD = usd(10'000); + + // Create & Claim (Dest) PayChan + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + { + auto const preAliceUSD = env.balance(alice, usd); + auto const preBobUSD = env.balance(bob, usd); + env(paychan::create(alice, bob, usd(1'000), 1s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, usd) == preAliceUSD - usd(1'000)); + BEAST_EXPECT(env.balance(bob, usd) == preBobUSD); + BEAST_EXPECT(issuerBalance(env, gw, usd) == outstandingUSD - usd(1'000)); + BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(1'000)); + } + { + auto const preAliceUSD = env.balance(alice, usd); + auto const preBobUSD = env.balance(bob, usd); + auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, usd(1'000)); + env(paychan::claim(bob, chan, usd(1'000), usd(1'000), Slice(sig), alice.pk()), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, usd) == preAliceUSD); + BEAST_EXPECT(env.balance(bob, usd) == preBobUSD + usd(1'000)); + BEAST_EXPECT(issuerBalance(env, gw, usd) == outstandingUSD); + BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(0)); + } + + // Create & Claim (Account) PayChan + auto const chan2 = paychan::channel(alice, bob, env.seq(alice)); + { + auto const preAliceUSD = env.balance(alice, usd); + auto const preBobUSD = env.balance(bob, usd); + env(paychan::create(alice, bob, usd(1'000), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, usd) == preAliceUSD - usd(1'000)); + BEAST_EXPECT(env.balance(bob, usd) == preBobUSD); + BEAST_EXPECT(issuerBalance(env, gw, usd) == outstandingUSD - usd(1'000)); + BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(1'000)); + } + { + auto const preAliceUSD = env.balance(alice, usd); + auto const preBobUSD = env.balance(bob, usd); + env(paychan::claim(alice, chan2, usd(1'000), usd(1'000)), + Txflags(tfClose), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, usd) == preAliceUSD); + BEAST_EXPECT(env.balance(bob, usd) == preBobUSD + usd(1'000)); + BEAST_EXPECT(issuerBalance(env, gw, usd) == outstandingUSD); + BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(0)); + } + } + + void + testIOUMetaAndOwnership(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + { + testcase("IOU Metadata to other"); + + Env env{*this, features}; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(10'000), alice, bob, carol); + env.close(); + env(pay(gw, alice, usd(5000))); + env(pay(gw, bob, usd(5000))); + env(pay(gw, carol, usd(5000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + auto const pk = alice.pk(); + auto const pk2 = bob.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); + env.close(); + env(paychan::create(bob, carol, usd(1'000), settleDelay, pk2)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); + env.close(); + + auto const ab = env.le(keylet::payChannel(alice.id(), bob.id(), aseq)); + BEAST_EXPECT(ab); + + auto const bc = env.le(keylet::payChannel(bob.id(), carol.id(), bseq)); + BEAST_EXPECT(bc); + + { + xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) != aod.end()); + + xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) != bod.end()); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) != bod.end()); + + xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(cod.begin(), cod.end(), bc) != cod.end()); + + xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 5); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(iod.begin(), iod.end(), ab) != iod.end()); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(iod.begin(), iod.end(), bc) != iod.end()); + } + + auto const chanAb = paychan::channel(alice, bob, aseq); + env(paychan::claim(alice, chanAb, usd(1'000), usd(1'000)), Txflags(tfClose)); + { + BEAST_EXPECT(!env.le(keylet::payChannel(alice.id(), bob.id(), aseq))); + BEAST_EXPECT(env.le(keylet::payChannel(bob.id(), carol.id(), bseq))); + + xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) == aod.end()); + + xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end()); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) != bod.end()); + + xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + + xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(iod.begin(), iod.end(), ab) == iod.end()); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(iod.begin(), iod.end(), bc) != iod.end()); + } + + env.close(); + auto const chanBc = paychan::channel(bob, carol, bseq); + env(paychan::claim(bob, chanBc, usd(1'000), usd(1'000)), Txflags(tfClose)); + { + BEAST_EXPECT(!env.le(keylet::payChannel(alice.id(), bob.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::payChannel(bob.id(), carol.id(), bseq))); + + xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) == aod.end()); + + xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end()); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) == bod.end()); + + xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(iod.begin(), iod.end(), ab) == iod.end()); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(iod.begin(), iod.end(), bc) == iod.end()); + } + } + + { + testcase("IOU Metadata to issuer"); + + Env env{*this, features}; + env.fund(XRP(5000), alice, carol, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(10'000), alice, carol); + env.close(); + env(pay(gw, alice, usd(5000))); + env(pay(gw, carol, usd(5000))); + env.close(); + auto const aseq = env.seq(alice); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, gw, usd(1'000), settleDelay, pk)); + + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); + env.close(); + env(paychan::create(gw, carol, usd(1'000), settleDelay, alice.pk()), + Ter(tecNO_PERMISSION)); + env.close(); + + auto const ag = env.le(keylet::payChannel(alice.id(), gw.id(), aseq)); + BEAST_EXPECT(ag); + + { + xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ag) != aod.end()); + + xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(iod.begin(), iod.end(), ag) != iod.end()); + } + + auto const chanAg = paychan::channel(alice, gw, aseq); + env(paychan::claim(alice, chanAg, usd(1'000), usd(1'000)), Txflags(tfClose)); + { + BEAST_EXPECT(!env.le(keylet::payChannel(alice.id(), gw.id(), aseq))); + + xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ag) == aod.end()); + + xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 2); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(iod.begin(), iod.end(), ag) == iod.end()); + } + } + } + + void + testIOURippleState(FeatureBitset features) + { + testcase("IOU RippleState"); + using namespace test::jtx; + using namespace std::literals; + + struct TestAccountData + { + Account src; + Account dst; + Account gw; + bool hasTrustline; + bool negative; + }; + + std::array const tests = {{ + // src > dst && src > issuer && dst no trustline + {.src = Account("alice2"), + .dst = Account("bob0"), + .gw = Account{"gw0"}, + .hasTrustline = false, + .negative = true}, + // src < dst && src < issuer && dst no trustline + {.src = Account("carol0"), + .dst = Account("dan1"), + .gw = Account{"gw1"}, + .hasTrustline = false, + .negative = false}, + // dst > src && dst > issuer && dst no trustline + {.src = Account("dan1"), + .dst = Account("alice2"), + .gw = Account{"gw0"}, + .hasTrustline = false, + .negative = true}, + // dst < src && dst < issuer && dst no trustline + {.src = Account("bob0"), + .dst = Account("carol0"), + .gw = Account{"gw1"}, + .hasTrustline = false, + .negative = false}, + // src > dst && src > issuer && dst has trustline + {.src = Account("alice2"), + .dst = Account("bob0"), + .gw = Account{"gw0"}, + .hasTrustline = true, + .negative = true}, + // src < dst && src < issuer && dst has trustline + {.src = Account("carol0"), + .dst = Account("dan1"), + .gw = Account{"gw1"}, + .hasTrustline = true, + .negative = false}, + // dst > src && dst > issuer && dst has trustline + {.src = Account("dan1"), + .dst = Account("alice2"), + .gw = Account{"gw0"}, + .hasTrustline = true, + .negative = true}, + // dst < src && dst < issuer && dst has trustline + {.src = Account("bob0"), + .dst = Account("carol0"), + .gw = Account{"gw1"}, + .hasTrustline = true, + .negative = false}, + }}; + + for (auto const& t : tests) + { + Env env{*this, features}; + auto const usd = t.gw["USD"]; + env.fund(XRP(5000), t.src, t.dst, t.gw); + env(fset(t.gw, asfAllowTrustLineLocking)); + env.close(); + + if (t.hasTrustline) + { + env.trust(usd(100'000), t.src, t.dst); + } + else + { + env.trust(usd(100'000), t.src); + } + env.close(); + + env(pay(t.gw, t.src, usd(10'000))); + if (t.hasTrustline) + env(pay(t.gw, t.dst, usd(10'000))); + env.close(); + + // src can create paychan + auto const seq1 = env.seq(t.src); + auto const delta = usd(1'000); + auto const pk = t.src.pk(); + auto const settleDelay = 100s; + env(paychan::create(t.src, t.dst, delta, settleDelay, pk)); + env.close(); + + // dst can claim paychan + auto const preSrc = env.balance(t.src, usd); + auto const preDst = env.balance(t.dst, usd); + + auto const chan = paychan::channel(t.src, t.dst, seq1); + auto const sig = paychan::signClaimAuth(pk, t.src.sk(), chan, delta); + env(paychan::claim(t.dst, chan, delta, delta, Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(t.src, usd) == preSrc); + BEAST_EXPECT(env.balance(t.dst, usd) == preDst + delta); + } + } + + void + testIOUGateway(FeatureBitset features) + { + testcase("IOU Gateway"); + using namespace test::jtx; + using namespace std::literals; + + // issuer is source + { + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + Env env{*this, features}; + auto const usd = gw["USD"]; + env.fund(XRP(5000), alice, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100'000), alice); + env.close(); + + env(pay(gw, alice, usd(10'000))); + env.close(); + + // issuer cannot create paychan + auto const pk = gw.pk(); + auto const settleDelay = 100s; + env(paychan::create(gw, alice, usd(1'000), settleDelay, pk), Ter(tecNO_PERMISSION)); + env.close(); + } + + struct TestAccountData + { + Account src; + Account dst; + bool hasTrustline; + }; + + std::array const gwDstTests = {{ + // src > dst && src > issuer && dst has trustline + {.src = Account("alice2"), .dst = Account{"gw0"}, .hasTrustline = true}, + // src < dst && src < issuer && dst has trustline + {.src = Account("carol0"), .dst = Account{"gw1"}, .hasTrustline = true}, + // dst > src && dst > issuer && dst has trustline + {.src = Account("dan1"), .dst = Account{"gw0"}, .hasTrustline = true}, + // dst < src && dst < issuer && dst has trustline + {.src = Account("bob0"), .dst = Account{"gw1"}, .hasTrustline = true}, + }}; + + // issuer is destination + for (auto const& t : gwDstTests) + { + Env env{*this, features}; + auto const usd = t.dst["USD"]; + env.fund(XRP(5000), t.dst, t.src); + env(fset(t.dst, asfAllowTrustLineLocking)); + env.close(); + + env.trust(usd(100'000), t.src); + env.close(); + + env(pay(t.dst, t.src, usd(10'000))); + env.close(); + + // issuer can receive paychan + auto const seq1 = env.seq(t.src); + auto const preSrc = env.balance(t.src, usd); + auto const pk = t.src.pk(); + auto const settleDelay = 100s; + env(paychan::create(t.src, t.dst, usd(1'000), settleDelay, pk)); + env.close(); + + // issuer can claim paychan, no dest trustline + auto const chan = paychan::channel(t.src, t.dst, seq1); + auto const sig = paychan::signClaimAuth(pk, t.src.sk(), chan, usd(1'000)); + env(paychan::claim(t.dst, chan, usd(1'000), usd(1'000), Slice(sig), pk)); + env.close(); + auto const preAmount = 10'000; + BEAST_EXPECT(preSrc == usd(preAmount)); + auto const postAmount = 9000; + BEAST_EXPECT(env.balance(t.src, usd) == usd(postAmount)); + BEAST_EXPECT(env.balance(t.dst, usd) == usd(0)); + } + } + + void + testIOULockedRate(FeatureBitset features) + { + testcase("IOU Locked Rate"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + // test locked rate + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, usd); + auto const seq1 = env.seq(alice); + auto const delta = usd(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + auto const transferRate = paychan::rate(env, alice, bob, seq1); + BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // bob can claim paychan + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, usd) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, usd) == usd(10'100)); + } + // test rate change - higher + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, usd); + auto const seq1 = env.seq(alice); + auto const delta = usd(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + auto transferRate = paychan::rate(env, alice, bob, seq1); + BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate higher + env(rate(gw, 1.26)); + env.close(); + + // bob can claim paychan - rate unchanged + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, usd) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, usd) == usd(10'100)); + } + // test rate change - lower + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, usd); + auto const seq1 = env.seq(alice); + auto const delta = usd(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + auto transferRate = paychan::rate(env, alice, bob, seq1); + BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate lower + env(rate(gw, 1.00)); + env.close(); + + // bob can claim paychan - rate changed + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, usd) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, usd) == usd(10125)); + } + + // test claim/close doesnt charge rate + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, usd); + auto const seq1 = env.seq(alice); + auto const delta = usd(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + auto transferRate = paychan::rate(env, alice, bob, seq1); + BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate lower + env(rate(gw, 1.00)); + env.close(); + + // alice can close paychan - rate is not charged + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + BEAST_EXPECT(env.balance(alice, usd) == preAlice); + BEAST_EXPECT(env.balance(bob, usd) == usd(10000)); + } + } + + void + testIOULimitAmount(FeatureBitset features) + { + testcase("IOU Limit"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + // test LimitAmount + { + Env env{*this, features}; + env.fund(XRP(1'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(10'000), alice, bob); + env.close(); + env(pay(gw, alice, usd(1'000))); + env(pay(gw, bob, usd(1'000))); + env.close(); + + // alice can create paychan + auto seq1 = env.seq(alice); + auto const delta = usd(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // bob can claim + auto const preBobLimit = env.limit(bob, usd); + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + auto const postBobLimit = env.limit(bob, usd); + // bobs limit is NOT changed + BEAST_EXPECT(postBobLimit == preBobLimit); + } + } + + void + testIOURequireAuth(FeatureBitset features) + { + testcase("IOU Require Auth"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + + Env env{*this, features}; + env.fund(XRP(1'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), Txflags(tfSetfAuth)); + env(trust(alice, usd(10'000))); + env(trust(bob, usd(10'000))); + env.close(); + env(pay(gw, alice, usd(1'000))); + env.close(); + + // alice cannot create paychan - fails without auth + auto seq1 = env.seq(alice); + auto const delta = usd(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk), Ter(tecNO_AUTH)); + env.close(); + + // set auth on bob + env(trust(gw, bobUSD(10'000)), Txflags(tfSetfAuth)); + env(trust(bob, usd(10'000))); + env.close(); + env(pay(gw, bob, usd(1'000))); + env.close(); + + // alice can create paychan - bob has auth + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // bob can claim + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + } + + void + testIOUFreeze(FeatureBitset features) + { + testcase("IOU Freeze"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + // test Global Freeze + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = usd(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + + // create paychan fails - frozen trustline + env(paychan::create(alice, bob, delta, settleDelay, pk), Ter(tecFROZEN)); + env.close(); + + // clear global freeze + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + // create paychan success + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // set global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // bob claim paychan success regardless of frozen assets + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + + // clear global freeze + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + // create paychan success + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // set global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // alice close paychan success regardless of frozen assets + auto const chan2 = paychan::channel(alice, bob, seq1); + env(paychan::claim(alice, chan2, delta, delta), Txflags(tfClose)); + env.close(); + } + + // test Individual Freeze + { + // Env Setup + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, usd(100'000))); + env(trust(bob, usd(100'000))); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, usd(10'000), alice, tfSetFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = usd(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + + // create paychan fails - frozen trustline + env(paychan::create(alice, bob, delta, settleDelay, pk), Ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust(gw, usd(10'000), alice, tfClearFreeze)); + env.close(); + + // create paychan success + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, usd(10'000), bob, tfSetFreeze)); + env.close(); + + // bob claim paychan success regardless of frozen assets + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + + // reset freeze on bob and alice trustline + env(trust(gw, usd(10'000), alice, tfClearFreeze)); + env(trust(gw, usd(10'000), bob, tfClearFreeze)); + env.close(); + + // create paychan success + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, usd(10'000), bob, tfSetFreeze)); + env.close(); + + // alice close paychan success regardless of frozen assets + auto const chan2 = paychan::channel(alice, bob, seq1); + env(paychan::claim(alice, chan2, delta, delta), Txflags(tfClose)); + env.close(); + } + + // test Deep Freeze + { + // Env Setup + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, usd(100'000))); + env(trust(bob, usd(100'000))); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, usd(10'000), alice, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = usd(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + + // create paychan fails - frozen trustline + env(paychan::create(alice, bob, delta, settleDelay, pk), Ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust(gw, usd(10'000), alice, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // create paychan success + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, usd(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // bob claim paychan fails because of deep frozen assets + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk), Ter(tecFROZEN)); + env.close(); + + // reset freeze on alice and bob trustline + env(trust(gw, usd(10'000), alice, tfClearFreeze | tfClearDeepFreeze)); + env(trust(gw, usd(10'000), bob, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // create paychan success + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, usd(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // bob close paychan success regardless of deep frozen assets + auto const chan2 = paychan::channel(alice, bob, seq1); + env(paychan::claim(bob, chan2), Txflags(tfClose)); + env.close(); + } + } + + void + testIOUInsf(FeatureBitset features) + { + testcase("IOU Insufficient Funds"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + { + // test tecPATH_PARTIAL + // ie. has 10'000, paychan 1'000 then try to pay 10'000 + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + // create paychan success + auto const delta = usd(1'000); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + env(pay(alice, gw, usd(10'000)), Ter(tecPATH_PARTIAL)); + } + { + // test tecINSUFFICIENT_FUNDS + // ie. has 10'000 paychan 1'000 then try to paychan 10'000 + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + auto const delta = usd(1'000); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + env(paychan::create(alice, bob, usd(10'000), settleDelay, pk), + Ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + } + + void + testIOUPrecisionLoss(FeatureBitset features) + { + testcase("IOU Precision Loss"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + // test min create precision loss + { + Env env(*this, features); + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100000000000000000), alice); + env.trust(usd(100000000000000000), bob); + env.close(); + env(pay(gw, alice, usd(10000000000000000))); + env(pay(gw, bob, usd(1))); + env.close(); + + // With a larger mantissa the amounts remain addable + bool const largeMantissa = + features[featureSingleAssetVault] || features[featureLendingProtocol]; + + // alice cannot create paychan for 1/10 iou - precision loss + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, usd(1), settleDelay, pk), + Ter(largeMantissa ? (TER)tesSUCCESS : (TER)tecPRECISION_LOSS)); + env.close(); + + auto const seq1 = env.seq(alice); + // alice can create paychan for 1'000 iou + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk)); + env.close(); + + // bob claim paychan success + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, usd(1'000)); + env(paychan::claim(bob, chan, usd(1'000), usd(1'000), Slice(sig), pk)); + env.close(); + } + } + + void + testMPTEnablement(FeatureBitset features) + { + testcase("MPT Enablement"); + + using namespace jtx; + using namespace std::chrono; + + for (bool const withTokenPaychan : {false, true}) + { + auto const amend = withTokenPaychan ? features : features - featureTokenPaychan; + Env env{*this, amend}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(5000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + auto const openResult = withTokenPaychan ? Ter(tesSUCCESS) : Ter(temBAD_AMOUNT); + auto const closeResult = withTokenPaychan ? Ter(tesSUCCESS) : Ter(tecNO_TARGET); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, mpt(1'000), settleDelay, pk), openResult); + env.close(); + env(paychan::fund(alice, chan, mpt(1'000)), openResult); + env.close(); + env(paychan::claim(bob, chan), Txflags(tfClose), closeResult); + env.close(); + } + } + + void + testMPTCreatePreflight(FeatureBitset features) + { + testcase("MPT Create Preflight"); + using namespace test::jtx; + using namespace std::literals; + + for (bool const withMPT : {true, false}) + { + auto const amend = withMPT ? features : features - featureMPTokensV1; + Env env{*this, amend}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(1'000), alice, bob, gw); + + json::Value jv = paychan::create(alice, bob, XRP(1), 100s, alice.pk()); + jv.removeMember(jss::Amount); + jv[jss::Amount][jss::mpt_issuance_id] = + "00000004A407AF5856CCF3C42619DAA925813FC955C72983"; + jv[jss::Amount][jss::value] = "-1"; + + auto const result = withMPT ? Ter(temBAD_AMOUNT) : Ter(temDISABLED); + env(jv, result); + env.close(); + } + + // temBAD_AMOUNT: amount < 0 + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(-1), settleDelay, pk), Ter(temBAD_AMOUNT)); + env.close(); + } + } + + void + testMPTCreatePreclaim(FeatureBitset features) + { + testcase("MPT Create Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_PERMISSION: issuer is the same as the account + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + auto const pk = gw.pk(); + auto const settleDelay = 100s; + env(paychan::create(gw, alice, mpt(1), settleDelay, pk), Ter(tecNO_PERMISSION)); + env.close(); + } + + // tecOBJECT_NOT_FOUND: mpt does not exist + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), alice, bob, gw); + env.close(); + + auto const mpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice)); + json::Value jv = paychan::create(alice, bob, mpt(2), 100s, alice.pk()); + jv[jss::Amount][jss::mpt_issuance_id] = + "00000004A407AF5856CCF3C42619DAA925813FC955C72983"; + env(jv, Ter(tecOBJECT_NOT_FOUND)); + env.close(); + } + + // tecNO_PERMISSION: tfMPTCanEscrow is not enabled + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(3), settleDelay, pk), Ter(tecNO_PERMISSION)); + env.close(); + } + + // tecOBJECT_NOT_FOUND: account does not have the mpt + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + auto const mpt = mptGw["MPT"]; + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(4), settleDelay, pk), Ter(tecOBJECT_NOT_FOUND)); + env.close(); + } + + // tecNO_AUTH: requireAuth set: account not authorized + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + // unauthorize account + mptGw.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize}); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(5), settleDelay, pk), Ter(tecNO_AUTH)); + env.close(); + } + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + // unauthorize dest + mptGw.authorize({.account = gw, .holder = bob, .flags = tfMPTUnauthorize}); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(6), settleDelay, pk), Ter(tecNO_AUTH)); + env.close(); + } + + // tecLOCKED: issuer has locked the account + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + // lock account + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock}); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(7), settleDelay, pk), Ter(tecLOCKED)); + env.close(); + } + + // tecLOCKED: issuer has locked the dest + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + // lock dest + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(8), settleDelay, pk), Ter(tecLOCKED)); + env.close(); + } + + // tecNO_AUTH: mpt cannot be transferred + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(9), settleDelay, pk), Ter(tecNO_AUTH)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS: spendable amount is zero + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, bob, mpt(10))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(11), settleDelay, pk), Ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS: spendable amount is less than the amount + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10))); + env(pay(gw, bob, mpt(10))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(11), settleDelay, pk), Ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + } + + void + testMPTClaimPreclaim(FeatureBitset features) + { + testcase("MPT Claim Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, mpt(10), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + // unauthorize dest + mptGw.authorize({.account = gw, .holder = bob, .flags = tfMPTUnauthorize}); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(10)); + env(paychan::claim(bob, chan, mpt(10), mpt(10), Slice(sig), pk), Ter(tecNO_AUTH)); + env.close(); + } + + // tecLOCKED: issuer has locked the dest + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, mpt(8), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + // lock dest + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(8)); + env(paychan::claim(bob, chan, mpt(8), mpt(8), Slice(sig), pk), Ter(tecLOCKED)); + env.close(); + } + } + + void + testMPTClaimDoApply(FeatureBitset features) + { + testcase("MPT Claim Do Apply"); + using namespace test::jtx; + using namespace std::literals; + + // tecINSUFFICIENT_RESERVE: insufficient reserve to create MPT + { + Env env{*this, features}; + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(acctReserve + (incReserve - 1), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, mpt(10), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(10)); + env(paychan::claim(bob, chan, mpt(10), mpt(10), Slice(sig), pk), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + + // tesSUCCESS: bob submits; claim MPT created + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, mpt(10), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(10)); + env(paychan::claim(bob, chan, mpt(10), mpt(10), Slice(sig), pk), Ter(tesSUCCESS)); + env.close(); + } + + // tecNO_PERMISSION: alice submits; claim MPT not created + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, mpt(10), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + env(paychan::claim(alice, chan, mpt(10), mpt(10)), Ter(tecNO_PERMISSION)); + env.close(); + } + } + + void + testMPTBalances(FeatureBitset features) + { + testcase("MPT Balances"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + env.fund(XRP(5000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice, carol}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = carol}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, carol, mpt(10'000))); + env.close(); + + auto outstandingMPT = env.balance(gw, mpt); + + // Create & Claim (Dest) PayChan + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + { + auto const preAliceMPT = env.balance(alice, mpt); + auto const preBobMPT = env.balance(bob, mpt); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(1'000), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 1'000); + BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0); + BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 1'000); + } + { + auto const preAliceMPT = env.balance(alice, mpt); + auto const preBobMPT = env.balance(bob, mpt); + auto const pk = alice.pk(); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(1'000)); + env(paychan::claim(bob, chan, mpt(1'000), mpt(1'000), Slice(sig), pk), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0); + BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT + mpt(1'000)); + BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0); + BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0); + } + + // Create & Claim (Account) PayChan + auto const chan2 = paychan::channel(alice, bob, env.seq(alice)); + { + auto const preAliceMPT = env.balance(alice, mpt); + auto const preBobMPT = env.balance(bob, mpt); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(1'000), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 1'000); + BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0); + BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 1'000); + } + { + auto const preAliceMPT = env.balance(alice, mpt); + auto const preBobMPT = env.balance(bob, mpt); + env(paychan::claim(alice, chan2, mpt(1'000), mpt(1'000)), + Txflags(tfClose), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0); + BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT + mpt(1'000)); + BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0); + BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0); + } + + // Multiple PayChans + { + auto const preAliceMPT = env.balance(alice, mpt); + auto const preBobMPT = env.balance(bob, mpt); + auto const preCarolMPT = env.balance(carol, mpt); + auto const pk = alice.pk(); + auto const pk2 = carol.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(1'000), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + env(paychan::create(carol, bob, mpt(1'000), settleDelay, pk2), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 1'000); + BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0); + BEAST_EXPECT(env.balance(carol, mpt) == preCarolMPT - mpt(1'000)); + BEAST_EXPECT(mptEscrowed(env, carol, mpt) == 1'000); + BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 2'000); + } + + // Max MPT Amount Issued (PayChan 1 MPT) + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(kMaxMpTokenAmount))); + env.close(); + + auto const preAliceMPT = env.balance(alice, mpt); + auto const preBobMPT = env.balance(bob, mpt); + auto const outstandingMPT = env.balance(gw, mpt); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, mpt(1), settleDelay, pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 1); + BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0); + BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 1); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(1)); + env(paychan::claim(bob, chan, mpt(1), mpt(1), Slice(sig), pk), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0); + BEAST_EXPECT( + !env.le(keylet::mptoken(mpt.mpt(), alice))->isFieldPresent(sfLockedAmount)); + BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT + mpt(1)); + BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0); + BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0); + BEAST_EXPECT( + !env.le(keylet::mptokenIssuance(mpt.mpt()))->isFieldPresent(sfLockedAmount)); + } + + // Max MPT Amount Issued (PayChan Max MPT) + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(kMaxMpTokenAmount))); + env.close(); + + auto const preAliceMPT = env.balance(alice, mpt); + auto const preBobMPT = env.balance(bob, mpt); + auto const outstandingMPT = env.balance(gw, mpt); + + // PayChan Max MPT - 10 + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan1 = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, mpt(kMaxMpTokenAmount - 10), settleDelay, pk)); + env.close(); + + // PayChan 10 MPT + auto const chan2 = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, mpt(10), settleDelay, pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(kMaxMpTokenAmount)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == kMaxMpTokenAmount); + BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0); + BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == kMaxMpTokenAmount); + + auto const sig1 = + paychan::signClaimAuth(pk, alice.sk(), chan1, mpt(kMaxMpTokenAmount - 10)); + env(paychan::claim( + bob, + chan1, + mpt(kMaxMpTokenAmount - 10), + mpt(kMaxMpTokenAmount - 10), + Slice(sig1), + pk), + Ter(tesSUCCESS)); + env.close(); + + auto const sig2 = paychan::signClaimAuth(pk, alice.sk(), chan2, mpt(10)); + env(paychan::claim(bob, chan2, mpt(10), mpt(10), Slice(sig2), pk), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(kMaxMpTokenAmount)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0); + BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT + mpt(kMaxMpTokenAmount)); + BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0); + BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0); + } + } + + void + testMPTMetaAndOwnership(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + { + testcase("MPT Metadata to other"); + + Env env{*this, features}; + MPTTester mptGw(env, gw, {.holders = {alice, bob, carol}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = carol}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env(pay(gw, carol, mpt(10'000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + auto const pk = alice.pk(); + auto const pk2 = bob.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(1'000), settleDelay, pk)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); + env.close(); + env(paychan::create(bob, carol, mpt(1'000), settleDelay, pk2)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); + env.close(); + + auto const ab = env.le(keylet::payChannel(alice.id(), bob.id(), aseq)); + BEAST_EXPECT(ab); + + auto const bc = env.le(keylet::payChannel(bob.id(), carol.id(), bseq)); + BEAST_EXPECT(bc); + + { + xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) != aod.end()); + + xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) != bod.end()); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) != bod.end()); + + xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(cod.begin(), cod.end(), bc) != cod.end()); + } + + auto const chanAb = paychan::channel(alice, bob, aseq); + env(paychan::claim(alice, chanAb, mpt(1'000), mpt(1'000)), Txflags(tfClose)); + { + BEAST_EXPECT(!env.le(keylet::payChannel(alice.id(), bob.id(), aseq))); + BEAST_EXPECT(env.le(keylet::payChannel(bob.id(), carol.id(), bseq))); + + xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) == aod.end()); + + xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end()); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) != bod.end()); + + xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + } + + env.close(); + auto const chanBc = paychan::channel(bob, carol, bseq); + env(paychan::claim(bob, chanBc, mpt(1'000), mpt(1'000)), Txflags(tfClose)); + { + BEAST_EXPECT(!env.le(keylet::payChannel(alice.id(), bob.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::payChannel(bob.id(), carol.id(), bseq))); + + xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) == aod.end()); + + xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end()); + // NOLINTNEXTLINE(modernize-use-ranges) + BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) == bod.end()); + + xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + } + } + } + + void + testMPTGateway(FeatureBitset features) + { + testcase("MPT Gateway Balances"); + using namespace test::jtx; + using namespace std::literals; + + // issuer is source + { + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + Env env{*this, features}; + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + // issuer cannot create paychan + auto const pk = gw.pk(); + auto const settleDelay = 100s; + env(paychan::create(gw, alice, mpt(1'000), settleDelay, pk), Ter(tecNO_PERMISSION)); + env.close(); + } + + // issuer is dest; alice w/ authorization + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + // issuer can be destination + auto const preAliceMPT = env.balance(alice, mpt); + auto const preOutstanding = env.balance(gw, mpt); + auto const preEscrowed = issuerMPTEscrowed(env, mpt); + BEAST_EXPECT(preOutstanding == mpt(-10'000)); + BEAST_EXPECT(preEscrowed == 0); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, gw, env.seq(alice)); + env(paychan::create(alice, gw, mpt(1'000), settleDelay, pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 1'000); + BEAST_EXPECT(env.balance(gw, mpt) == preOutstanding); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == preEscrowed + 1'000); + + // issuer (dest) can claim paychan + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(1'000)); + env(paychan::claim(gw, chan, mpt(1'000), mpt(1'000), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0); + BEAST_EXPECT(env.balance(gw, mpt) == preOutstanding + mpt(1'000)); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == preEscrowed); + } + } + + void + testMPTLockedRate(FeatureBitset features) + { + testcase("MPT Locked Rate"); + using namespace test::jtx; + using namespace std::literals; + + // test locked rate: claim + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.transferFee = 25000, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, mpt); + auto const seq1 = env.seq(alice); + auto const delta = mpt(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(125), settleDelay, pk)); + env.close(); + auto const transferRate = paychan::rate(env, alice, bob, seq1); + BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // bob can claim paychan + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, mpt) == mpt(10'100)); + } + + // test locked rate: close + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.transferFee = 25000, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, mpt); + auto const preBob = env.balance(bob, mpt); + auto const seq1 = env.seq(alice); + auto const delta = mpt(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(125), settleDelay, pk)); + env.close(); + auto const transferRate = paychan::rate(env, alice, bob, seq1); + BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // bob can close paychan + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAlice); + BEAST_EXPECT(env.balance(bob, mpt) == preBob); + } + } + + // void + // testMPTRequireAuth(FeatureBitset features) + // { + // testcase("MPT Require Auth"); + // using namespace test::jtx; + // using namespace std::literals; + + // Env env{*this, features}; + // auto const baseFee = env.current()->fees().base; + // auto const alice = Account("alice"); + // auto const bob = Account("bob"); + // auto const gw = Account("gw"); + + // MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + // mptGw.create( + // {.ownerCount = 1, + // .holderCount = 0, + // .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + // mptGw.authorize({.account = alice}); + // mptGw.authorize({.account = gw, .holder = alice}); + // mptGw.authorize({.account = bob}); + // mptGw.authorize({.account = gw, .holder = bob}); + // auto const MPT = mptGw["MPT"]; + // env(pay(gw, alice, MPT(10'000))); + // env.close(); + + // auto seq = env.seq(alice); + // auto const delta = MPT(125); + // // alice can create escrow - is authorized + // env(escrow::create(alice, bob, MPT(100)), + // escrow::condition(escrow::cb1), + // escrow::finish_time(env.now() + 1s), + // Fee(baseFee * 150)); + // env.close(); + + // // bob can finish escrow - is authorized + // env(escrow::finish(bob, alice, seq), + // escrow::condition(escrow::cb1), + // escrow::fulfillment(escrow::fb1), + // Fee(baseFee * 150)); + // env.close(); + // } + + void + testMPTLock(FeatureBitset features) + { + testcase("MPT Lock"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + // alice create paychan + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, mpt(100), settleDelay, pk)); + env.close(); + + // lock account & dest + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock}); + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + // bob cannot claim + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(100)); + env(paychan::claim(bob, chan, mpt(100), mpt(100), Slice(sig), pk), Ter(tecLOCKED)); + env.close(); + + // bob can claim/close + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + } + + void + testMPTCanTransfer(FeatureBitset features) + { + testcase("MPT Can Transfer"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + // alice cannot create paychan to non issuer + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(100), settleDelay, pk), Ter(tecNO_AUTH)); + env.close(); + + // PayChan Create & Claim + { + // alice can create paychan to issuer + auto const chan = paychan::channel(alice, gw, env.seq(alice)); + env(paychan::create(alice, gw, mpt(100), settleDelay, pk)); + env.close(); + + // gw can claim + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(100)); + env(paychan::claim(gw, chan, mpt(100), mpt(100), Slice(sig), pk)); + env.close(); + } + + // PayChan Create & Close + { + // alice can create paychan to issuer + auto const chan = paychan::channel(alice, gw, env.seq(alice)); + env(paychan::create(alice, gw, mpt(100), settleDelay, pk)); + env.close(); + + // gw can claim/close + env(paychan::claim(gw, chan), Txflags(tfClose)); + env.close(); + } + } + + void + testMPTDestroy(FeatureBitset features) + { + testcase("MPT Destroy"); + using namespace test::jtx; + using namespace std::literals; + + // tecHAS_OBLIGATIONS: issuer cannot destroy issuance + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, mpt(10), settleDelay, pk)); + env.close(); + + env(pay(alice, gw, mpt(10'000)), Ter(tecPATH_PARTIAL)); + env(pay(alice, gw, mpt(9'990))); + env(pay(bob, gw, mpt(10'000))); + BEAST_EXPECT(env.balance(alice, mpt) == mpt(0)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 10); + BEAST_EXPECT(env.balance(bob, mpt) == mpt(0)); + BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0); + BEAST_EXPECT(env.balance(gw, mpt) == mpt(-10)); + mptGw.authorize({.account = bob, .flags = tfMPTUnauthorize}); + mptGw.destroy({.id = mptGw.issuanceID(), .ownerCount = 1, .err = tecHAS_OBLIGATIONS}); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(10)); + env(paychan::claim(bob, chan, mpt(10), mpt(10), Slice(sig), pk), Ter(tesSUCCESS)); + env.close(); + + env(pay(bob, gw, mpt(10))); + mptGw.destroy({.id = mptGw.issuanceID(), .ownerCount = 0}); + } + + // tecHAS_OBLIGATIONS: holder cannot destroy mptoken + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, mpt(10), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + env(pay(alice, gw, mpt(9'990))); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == mpt(0)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 10); + mptGw.authorize( + {.account = alice, .flags = tfMPTUnauthorize, .err = tecHAS_OBLIGATIONS}); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(10)); + env(paychan::claim(bob, chan, mpt(10), mpt(10), Slice(sig), pk), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == mpt(0)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0); + mptGw.authorize({.account = alice, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(mpt.mpt(), alice))); + } + } + + void + testIOUClawbackInteraction(FeatureBitset features) + { + testcase("IOU Clawback Interaction"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + // Attempt to shelter funds from clawback by locking in channel + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(5'000))); + env(pay(gw, bob, usd(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, usd(4'000), settleDelay, pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, usd) == usd(1'000)); + BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(4'000)); + + env(claw(gw, alice["USD"](1'000))); + env.close(); + BEAST_EXPECT(env.balance(alice, usd) == usd(0)); + + auto const chan = paychan::channel(alice, bob, seq1); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == usd(4'000)); + BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(4'000)); + + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + // Alice recovered 4000 USD that survived the clawback + BEAST_EXPECT(env.balance(alice, usd) == usd(4'000)); + BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(0)); + } + + // Clawback from dest with active channel (claim after clawback) + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(5'000))); + env(pay(gw, bob, usd(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk)); + env.close(); + + env(claw(gw, bob["USD"](5'000))); + env.close(); + BEAST_EXPECT(env.balance(bob, usd) == usd(0)); + + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, usd(1'000)); + env(paychan::claim(bob, chan, usd(1'000), usd(1'000), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(bob, usd) == usd(1'000)); + } + } + + void + testIOUFundAfterFreeze(FeatureBitset features) + { + testcase("IOU Fund After Freeze"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + // Fund channel after destination is frozen + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk)); + env.close(); + + env(trust(gw, usd(100'000), bob, tfSetFreeze)); + env.close(); + + // Cannot fund a channel whose destination is frozen; funding is + // subject to the same issuer controls as create + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::fund(alice, chan, usd(1'000)), Ter(tecFROZEN)); + env.close(); + + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == usd(1'000)); + + // The destination can still claim funds locked before the freeze + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, usd(500)); + env(paychan::claim(bob, chan, usd(500), usd(500), Slice(sig), pk), Ter(tesSUCCESS)); + env.close(); + } + + // Fund channel after sender frozen + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk)); + env.close(); + + env(trust(gw, usd(100'000), alice, tfSetFreeze)); + env.close(); + + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::fund(alice, chan, usd(1'000)), Ter(tecFROZEN)); + env.close(); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == usd(1'000)); + } + + // Close channel refund with deep frozen sender + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk)); + env.close(); + auto const preAlice = env.balance(alice, usd); + + env(trust(gw, usd(100'000), alice, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + BEAST_EXPECT(env.balance(alice, usd) == preAlice + usd(1'000)); + BEAST_EXPECT(!paychan::channelExists(*env.current(), chan)); + } + } + + void + testIOUFundIssuerControls(FeatureBitset features) + { + testcase("IOU Fund Issuer Controls"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + // tecINSUFFICIENT_FUNDS: cannot fund more than the spendable balance + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(2'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk)); + env.close(); + + // alice pays away the rest of her spendable balance + env(pay(alice, bob, usd(900))); + env.close(); + + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::fund(alice, chan, usd(1'000)), Ter(tecINSUFFICIENT_FUNDS)); + env.close(); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == usd(1'000)); + + // funding the spendable balance still works + env(paychan::fund(alice, chan, usd(100)), Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == usd(1'100)); + } + + // tecPRECISION_LOSS: cannot fund an amount the channel amount cannot + // absorb, even when the source's spendable balance can. Without the + // channel-amount canAdd guard the source would be debited while the + // channel amount rounds back unchanged. + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100000000000000000), alice); + env.trust(usd(100000000000000000), bob); + env.close(); + env(pay(gw, alice, usd(10000000000000000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, usd(1000000000000000), settleDelay, pk)); + env.close(); + + // alice pays away most of her balance so her small spendable + // amount passes the helper's canAdd(spendable, amount) check + env(pay(alice, bob, usd(8999999999999995))); + env.close(); + BEAST_EXPECT(env.balance(alice, usd) == usd(5)); + + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::fund(alice, chan, usd(0.001)), Ter(tecPRECISION_LOSS)); + env.close(); + + // neither the channel amount nor alice's balance changed + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == usd(1000000000000000)); + BEAST_EXPECT(env.balance(alice, usd) == usd(5)); + } + } + + void + testMPTFundIssuerControls(FeatureBitset features) + { + testcase("MPT Fund Issuer Controls"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: cannot fund after the issuer revokes authorization + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, mpt(100), settleDelay, pk)); + env.close(); + + // unauthorize the source account + mptGw.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize}); + + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::fund(alice, chan, mpt(100)), Ter(tecNO_AUTH)); + env.close(); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == mpt(100)); + + // re-authorize the source, unauthorize the destination + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = gw, .holder = bob, .flags = tfMPTUnauthorize}); + + env(paychan::fund(alice, chan, mpt(100)), Ter(tecNO_AUTH)); + env.close(); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == mpt(100)); + + // funding works again once both parties are authorized + mptGw.authorize({.account = gw, .holder = bob}); + env(paychan::fund(alice, chan, mpt(100)), Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == mpt(200)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 200); + } + + // tecLOCKED: cannot fund while the source or destination is locked + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, mpt(100), settleDelay, pk)); + env.close(); + + // lock the source account + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock}); + + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::fund(alice, chan, mpt(100)), Ter(tecLOCKED)); + env.close(); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == mpt(100)); + + // unlock the source, lock the destination + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTUnlock}); + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + env(paychan::fund(alice, chan, mpt(100)), Ter(tecLOCKED)); + env.close(); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == mpt(100)); + + // funding works again once both parties are unlocked + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTUnlock}); + env(paychan::fund(alice, chan, mpt(100)), Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == mpt(200)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 200); + } + } + + void + testIOUDeepFreezeAfterCreate(FeatureBitset features) + { + testcase("IOU Deep Freeze After Create"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + // Create channel, deep freeze sender, bob claims (sender freeze + // doesn't block claim since funds already locked) + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk)); + env.close(); + + // Deep freeze alice's trust line + env(trust(gw, usd(100'000), alice, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // Bob claims - only dest freeze is checked, not sender + auto const chan = paychan::channel(alice, bob, seq1); + auto const preBob = env.balance(bob, usd); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, usd(500)); + env(paychan::claim(bob, chan, usd(500), usd(500), Slice(sig), pk), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(bob, usd) == preBob + usd(500)); + } + } + + void + testIOUMultiChannelDrain(FeatureBitset features) + { + testcase("IOU Multi Channel Drain"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, carol, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100'000), alice, bob, carol); + env.close(); + env(pay(gw, alice, usd(5'000))); + env(pay(gw, bob, usd(5'000))); + env(pay(gw, carol, usd(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, usd(3'000), settleDelay, pk)); + env.close(); + BEAST_EXPECT(env.balance(alice, usd) == usd(2'000)); + + auto const seq2 = env.seq(alice); + env(paychan::create(alice, carol, usd(2'000), settleDelay, pk)); + env.close(); + BEAST_EXPECT(env.balance(alice, usd) == usd(0)); + + env(paychan::create(alice, bob, usd(1), settleDelay, pk), Ter(tecINSUFFICIENT_FUNDS)); + env.close(); + + auto const chan1 = paychan::channel(alice, bob, seq1); + auto sig = paychan::signClaimAuth(pk, alice.sk(), chan1, usd(3'000)); + env(paychan::claim(bob, chan1, usd(3'000), usd(3'000), Slice(sig), pk)); + env.close(); + BEAST_EXPECT(env.balance(bob, usd) == usd(8'000)); + + auto const chan2 = paychan::channel(alice, carol, seq2); + sig = paychan::signClaimAuth(pk, alice.sk(), chan2, usd(2'000)); + env(paychan::claim(carol, chan2, usd(2'000), usd(2'000), Slice(sig), pk)); + env.close(); + BEAST_EXPECT(env.balance(carol, usd) == usd(7'000)); + BEAST_EXPECT(env.balance(alice, usd) == usd(0)); + } + } + + void + testIOUTransferRatePartialClaims(FeatureBitset features) + { + testcase("IOU Transfer Rate Partial Claims"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + // Partial claim at high rate, rate drops, second claim at lower rate + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk)); + env.close(); + + auto const preBob = env.balance(bob, usd); + auto const chan = paychan::channel(alice, bob, seq1); + + auto sig = paychan::signClaimAuth(pk, alice.sk(), chan, usd(500)); + env(paychan::claim(bob, chan, usd(500), usd(500), Slice(sig), pk)); + env.close(); + BEAST_EXPECT(env.balance(bob, usd) == preBob + usd(400)); + + env(rate(gw, 1.0)); + env.close(); + + sig = paychan::signClaimAuth(pk, alice.sk(), chan, usd(1'000)); + env(paychan::claim(bob, chan, usd(1'000), usd(1'000), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(bob, usd) == preBob + usd(900)); + } + + // Create at parity, issuer raises rate, claim uses locked parity + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk)); + env.close(); + + env(rate(gw, 2.0)); + env.close(); + + auto const preBob = env.balance(bob, usd); + auto const chan = paychan::channel(alice, bob, seq1); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, usd(1'000)); + env(paychan::claim(bob, chan, usd(1'000), usd(1'000), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(bob, usd) == preBob + usd(1'000)); + } + } + + void + testIOUTrustLineLimitClaim(FeatureBitset features) + { + testcase("IOU Trust Line Limit Claim"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + // Claim that would exceed bob's trust line limit + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, usd(100'000))); + env(trust(bob, usd(500))); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(200))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk)); + env.close(); + + auto const chan = paychan::channel(alice, bob, seq1); + + // Bob claims 250 for self — succeeds (200 + 250 = 450 < 500) + auto sig = paychan::signClaimAuth(pk, alice.sk(), chan, usd(250)); + env(paychan::claim(bob, chan, usd(250), usd(250), Slice(sig), pk)); + env.close(); + BEAST_EXPECT(env.balance(bob, usd) == usd(450)); + + // Bob claims past his limit — succeeds; the limit only protects + // against unsolicited holdings and bob consents by submitting + // the claim himself (450 + 350 > 500) + sig = paychan::signClaimAuth(pk, alice.sk(), chan, usd(600)); + env(paychan::claim(bob, chan, usd(600), usd(600), Slice(sig), pk)); + env.close(); + BEAST_EXPECT(env.balance(bob, usd) == usd(800)); + + // Alice claiming on bob's behalf is blocked by bob's limit + env(paychan::claim(alice, chan, usd(900), usd(900)), + Txflags(tfClose), + Ter(tecLIMIT_EXCEEDED)); + env.close(); + BEAST_EXPECT(env.balance(bob, usd) == usd(800)); + } + } + + void + testIOUAllowLockingClearedClaim(FeatureBitset features) + { + testcase("IOU Allow Locking Cleared Claim"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(usd(100'000), alice); + env.trust(usd(100'000), bob); + env.close(); + env(pay(gw, alice, usd(10'000))); + env(pay(gw, bob, usd(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk)); + env.close(); + + env(fclear(gw, asfAllowTrustLineLocking)); + env.close(); + + auto const preBob = env.balance(bob, usd); + + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, usd(1'000)); + env(paychan::claim(bob, chan, usd(1'000), usd(1'000), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(bob, usd) == preBob + usd(1'000)); + + env(paychan::create(alice, bob, usd(1'000), settleDelay, pk), Ter(tecNO_PERMISSION)); + env.close(); + } + } + + void + testMPTClawbackInteraction(FeatureBitset features) + { + testcase("MPT Clawback Interaction"); + using namespace test::jtx; + using namespace std::literals; + + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanClawback}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(5'000))); + env(pay(gw, bob, mpt(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, mpt(4'000), settleDelay, pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == mpt(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 4'000); + + mptGw.claw(gw, alice, 1'000); + BEAST_EXPECT(env.balance(alice, mpt) == mpt(0)); + + auto const chan = paychan::channel(alice, bob, seq1); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 4'000); + + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == mpt(4'000)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0); + } + } + + void + testMPTClaimAutoCreate(FeatureBitset features) + { + testcase("MPT Claim Auto Create"); + using namespace test::jtx; + using namespace std::literals; + + // Claim auto-creates MPToken for receiver without authorize + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + BEAST_EXPECT(!env.le(keylet::mptoken(mpt.mpt(), bob))); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, mpt(1'000), settleDelay, pk)); + env.close(); + + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(500)); + env(paychan::claim(bob, chan, mpt(500), mpt(500), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.le(keylet::mptoken(mpt.mpt(), bob))); + BEAST_EXPECT(env.balance(bob, mpt) == mpt(500)); + } + + // requireAuth blocks claim even with auto-create + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(1'000), settleDelay, pk), Ter(tecNO_AUTH)); + env.close(); + } + } + + void + testMPTFreezeClaimClose(FeatureBitset features) + { + testcase("MPT Freeze Claim Close"); + using namespace test::jtx; + using namespace std::literals; + + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, mpt(1'000), settleDelay, pk)); + env.close(); + + auto const preAlice = env.balance(alice, mpt); + + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock}); + + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(500)); + + env(paychan::claim(bob, chan, mpt(500), mpt(500), Slice(sig), pk), Ter(tesSUCCESS)); + env.close(); + + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + auto const sig2 = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(1'000)); + env(paychan::claim(bob, chan, mpt(1'000), mpt(1'000), Slice(sig2), pk), Ter(tecLOCKED)); + env.close(); + + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + BEAST_EXPECT(env.balance(alice, mpt) == preAlice + mpt(500)); + BEAST_EXPECT(!paychan::channelExists(*env.current(), chan)); + } + } + + void + testMPTCanEscrowRequired(FeatureBitset features) + { + testcase("MPT CanEscrow Required"); + using namespace test::jtx; + using namespace std::literals; + + // Without canEscrow flag, channel creation fails + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, mpt(1'000), settleDelay, pk), Ter(tecNO_PERMISSION)); + env.close(); + } + + // With canEscrow flag, channel works and claim succeeds + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env(pay(gw, bob, mpt(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, mpt(1'000), settleDelay, pk)); + env.close(); + + auto const chan = paychan::channel(alice, bob, seq1); + auto const preBob = env.balance(bob, mpt); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, mpt(1'000)); + env(paychan::claim(bob, chan, mpt(1'000), mpt(1'000), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(bob, mpt) == preBob + mpt(1'000)); + } + } + + void + testIOUWithFeats(FeatureBitset features) + { + testIOUEnablement(features); + testIOUAllowLockingFlag(features); + testIOUCreatePreflight(features); + testIOUCreatePreclaim(features); + testIOUClaimPreclaim(features); + testIOUClaimDoApply(features); + // testIOUClaimClosePreclaim(features); + testIOUBalances(features); + testIOUMetaAndOwnership(features); + testIOURippleState(features); + testIOUGateway(features); + testIOULockedRate(features); + testIOULimitAmount(features); + testIOURequireAuth(features); + testIOUFreeze(features); + testIOUInsf(features); + testIOUPrecisionLoss(features); + testIOUClawbackInteraction(features); + testIOUFundAfterFreeze(features); + testIOUFundIssuerControls(features); + testIOUDeepFreezeAfterCreate(features); + testIOUMultiChannelDrain(features); + testIOUTransferRatePartialClaims(features); + testIOUTrustLineLimitClaim(features); + testIOUAllowLockingClearedClaim(features); + } + + void + testMPTWithFeats(FeatureBitset features) + { + testMPTEnablement(features); + testMPTCreatePreflight(features); + testMPTCreatePreclaim(features); + testMPTClaimPreclaim(features); + testMPTClaimDoApply(features); + // testMPTClaimClosePreclaim(features); + testMPTBalances(features); + testMPTMetaAndOwnership(features); + testMPTGateway(features); + testMPTLockedRate(features); + // testMPTRequireAuth(features); + testMPTLock(features); + testMPTCanTransfer(features); + testMPTDestroy(features); + testMPTClawbackInteraction(features); + testMPTClaimAutoCreate(features); + testMPTFreezeClaimClose(features); + testMPTCanEscrowRequired(features); + testMPTFundIssuerControls(features); + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{testableAmendments()}; + testIOUWithFeats(all); + testMPTWithFeats(all); + } +}; + +BEAST_DEFINE_TESTSUITE(PayChanToken, app, xrpl); +} // namespace xrpl::test diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index 50684721354..0e94acc6f79 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -1,7 +1,6 @@ #include #include -#include #include #include #include // IWYU pragma: keep @@ -9,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -78,15 +78,6 @@ struct PayChan_test : public beast::unit_test::Suite return sign(pk, sk, msg.slice()); } - static STAmount - channelAmount(ReadView const& view, uint256 const& chan) - { - auto const slep = view.read({ltPAYCHAN, chan}); - if (!slep) - return XRPAmount{-1}; - return (*slep)[sfAmount]; - } - static std::optional channelExpiration(ReadView const& view, uint256 const& chan) { @@ -111,26 +102,30 @@ struct PayChan_test : public beast::unit_test::Suite env.fund(XRP(10000), alice, bob); auto const pk = alice.pk(); auto const settleDelay = 100s; - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, XRP(1000), settleDelay, pk)); - BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); - BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), settleDelay, pk)); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0)); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000)); { auto const preAlice = env.balance(alice); - env(fund(alice, chan, XRP(1000))); + env(paychan::fund(alice, chan, XRP(1000))); auto const feeDrops = env.current()->fees().base; BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops); } - auto chanBal = channelBalance(*env.current(), chan); - auto chanAmt = channelAmount(*env.current(), chan); + auto chanBal = paychan::channelBalance(*env.current(), chan); + auto chanAmt = paychan::channelAmount(*env.current(), chan); BEAST_EXPECT(chanBal == XRP(0)); BEAST_EXPECT(chanAmt == XRP(2000)); { // bad amounts (non-xrp, negative amounts) - env(create(alice, bob, usda(1000), settleDelay, pk), Ter(temBAD_AMOUNT)); + // With TokenPaychan enabled the IOU amount passes preflight but + // fails preclaim: alice is the issuer and has not enabled + // asfAllowTrustLineLocking, either of which is tecNO_PERMISSION. + env(create(alice, bob, usda(1000), settleDelay, pk), + Ter(features[featureTokenPaychan] ? TER(tecNO_PERMISSION) : TER(temBAD_AMOUNT))); env(fund(alice, chan, usda(1000)), Ter(temBAD_AMOUNT)); env(create(alice, bob, XRP(-1000), settleDelay, pk), Ter(temBAD_AMOUNT)); env(fund(alice, chan, XRP(-1000)), Ter(temBAD_AMOUNT)); @@ -174,9 +169,9 @@ struct PayChan_test : public beast::unit_test::Suite auto const reqBal = chanBal + delta; auto const authAmt = reqBal + XRP(100); assert(reqBal <= chanAmt); - env(claim(alice, chan, reqBal, authAmt)); - BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); - BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + env(paychan::claim(alice, chan, reqBal, authAmt)); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob + delta); chanBal = reqBal; } @@ -336,14 +331,14 @@ struct PayChan_test : public beast::unit_test::Suite NetClock::time_point const cancelAfter = env.current()->header().parentCloseTime + 3600s; auto const channelFunds = XRP(1000); - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); - BEAST_EXPECT(channelExists(*env.current(), chan)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); env.close(cancelAfter); { // dst cannot claim after cancelAfter - auto const chanBal = channelBalance(*env.current(), chan); - auto const chanAmt = channelAmount(*env.current(), chan); + auto const chanBal = paychan::channelBalance(*env.current(), chan); + auto const chanAmt = paychan::channelAmount(*env.current(), chan); auto preAlice = env.balance(alice); auto preBob = env.balance(bob); auto const delta = XRP(500); @@ -353,7 +348,7 @@ struct PayChan_test : public beast::unit_test::Suite auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); auto const feeDrops = env.current()->fees().base; - BEAST_EXPECT(!channelExists(*env.current(), chan)); + BEAST_EXPECT(!paychan::channelExists(*env.current(), chan)); BEAST_EXPECT(env.balance(bob) == preBob - feeDrops); BEAST_EXPECT(env.balance(alice) == preAlice + channelFunds); } @@ -367,9 +362,9 @@ struct PayChan_test : public beast::unit_test::Suite NetClock::time_point const cancelAfter = env.current()->header().parentCloseTime + 3600s; auto const channelFunds = XRP(1000); - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); - BEAST_EXPECT(channelExists(*env.current(), chan)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); // third party close before cancelAfter env(claim(carol, chan), Txflags(tfClose), Ter(tecNO_PERMISSION)); BEAST_EXPECT(channelExists(*env.current(), chan)); @@ -436,9 +431,9 @@ struct PayChan_test : public beast::unit_test::Suite auto const minExpiration = closeTime + settleDelay; NetClock::time_point const cancelAfter = closeTime + 7200s; auto const channelFunds = XRP(1000); - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); - BEAST_EXPECT(channelExists(*env.current(), chan)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); BEAST_EXPECT(!channelExpiration(*env.current(), chan)); // Owner closes, will close after settleDelay env(claim(alice, chan), Txflags(tfClose)); @@ -467,7 +462,7 @@ struct PayChan_test : public beast::unit_test::Suite env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration - 50s}), Ter(temBAD_EXPIRATION)); BEAST_EXPECT(!channelExpiration(*env.current(), chan)); - env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration})); + env(paychan::fund(alice, chan, XRP(1), NetClock::time_point{minExpiration})); env.close(minExpiration); // Try to extend the expiration after the expiration has already passed env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration + 1000s})); @@ -489,17 +484,17 @@ struct PayChan_test : public beast::unit_test::Suite NetClock::time_point const settleTimepoint = env.current()->header().parentCloseTime + settleDelay; auto const channelFunds = XRP(1000); - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, channelFunds, settleDelay, pk)); - BEAST_EXPECT(channelExists(*env.current(), chan)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); // Owner closes, will close after settleDelay env(claim(alice, chan), Txflags(tfClose)); BEAST_EXPECT(channelExists(*env.current(), chan)); env.close(settleTimepoint - settleDelay / 2); { // receiver can still claim - auto const chanBal = channelBalance(*env.current(), chan); - auto const chanAmt = channelAmount(*env.current(), chan); + auto const chanBal = paychan::channelBalance(*env.current(), chan); + auto const chanAmt = paychan::channelAmount(*env.current(), chan); auto preBob = env.balance(bob); auto const delta = XRP(500); auto const reqBal = chanBal + delta; @@ -515,8 +510,8 @@ struct PayChan_test : public beast::unit_test::Suite env.close(settleTimepoint); { // past settleTime, channel will close - auto const chanBal = channelBalance(*env.current(), chan); - auto const chanAmt = channelAmount(*env.current(), chan); + auto const chanBal = paychan::channelBalance(*env.current(), chan); + auto const chanAmt = paychan::channelAmount(*env.current(), chan); auto const preAlice = env.balance(alice); auto preBob = env.balance(bob); auto const delta = XRP(500); @@ -545,17 +540,17 @@ struct PayChan_test : public beast::unit_test::Suite auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, channelFunds, settleDelay, pk)); - BEAST_EXPECT(channelExists(*env.current(), chan)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); // Owner tries to close channel, but it will remain open (settle delay) env(claim(alice, chan), Txflags(tfClose)); BEAST_EXPECT(channelExists(*env.current(), chan)); { // claim the entire amount auto const preBob = env.balance(bob); - env(claim(alice, chan, channelFunds.value(), channelFunds.value())); - BEAST_EXPECT(channelBalance(*env.current(), chan) == channelFunds); + env(paychan::claim(alice, chan, channelFunds.value(), channelFunds.value())); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == channelFunds); BEAST_EXPECT(env.balance(bob) == preBob + channelFunds); } auto const preAlice = env.balance(alice); @@ -580,15 +575,15 @@ struct PayChan_test : public beast::unit_test::Suite auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, channelFunds, settleDelay, pk)); - BEAST_EXPECT(channelExists(*env.current(), chan)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); // Owner tries to close channel, but it will remain open (settle delay) env(claim(alice, chan), Txflags(tfClose)); BEAST_EXPECT(channelExists(*env.current(), chan)); { - auto chanBal = channelBalance(*env.current(), chan); - auto chanAmt = channelAmount(*env.current(), chan); + auto chanBal = paychan::channelBalance(*env.current(), chan); + auto chanAmt = paychan::channelAmount(*env.current(), chan); auto const preBob = env.balance(bob); auto const delta = XRP(500); @@ -603,8 +598,8 @@ struct PayChan_test : public beast::unit_test::Suite } { // Claim again - auto chanBal = channelBalance(*env.current(), chan); - auto chanAmt = channelAmount(*env.current(), chan); + auto chanBal = paychan::channelBalance(*env.current(), chan); + auto chanAmt = paychan::channelAmount(*env.current(), chan); auto const preBob = env.balance(bob); auto const delta = XRP(500); @@ -635,9 +630,9 @@ struct PayChan_test : public beast::unit_test::Suite Env env{*this, features}; env.fund(XRP(10000), alice, bob); env(fset(bob, asfDisallowXRP)); - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, XRP(1000), 3600s, alice.pk())); - BEAST_EXPECT(channelExists(*env.current(), chan)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), 3600s, alice.pk())); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); } { @@ -646,13 +641,13 @@ struct PayChan_test : public beast::unit_test::Suite // since it is just advisory. Env env{*this, features}; env.fund(XRP(10000), alice, bob); - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, XRP(1000), 3600s, alice.pk())); - BEAST_EXPECT(channelExists(*env.current(), chan)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), 3600s, alice.pk())); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); env(fset(bob, asfDisallowXRP)); auto const reqBal = XRP(500).value(); - env(claim(alice, chan, reqBal, reqBal)); + env(paychan::claim(alice, chan, reqBal, reqBal)); } } @@ -704,16 +699,16 @@ struct PayChan_test : public beast::unit_test::Suite auto const pk = alice.pk(); auto const settleDelay = 100s; - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, XRP(1000), settleDelay, pk)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); - BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); - BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0)); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000)); // alice can add more funds to the channel even though bob has // asfDepositAuth set. - env(fund(alice, chan, XRP(1000))); + env(paychan::fund(alice, chan, XRP(1000))); env.close(); // alice claims. Fails because bob's lsfDepositAuth flag is set. @@ -741,7 +736,7 @@ struct PayChan_test : public beast::unit_test::Suite // bob claims with signature. Succeeds even though bob's // lsfDepositAuth flag is set since bob submitted the // transaction. - env(claim(bob, chan, delta, delta, Slice(sig), pk)); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); env.close(); BEAST_EXPECT(env.balance(bob) == preBob + delta - baseFee); } @@ -772,7 +767,7 @@ struct PayChan_test : public beast::unit_test::Suite env(deposit::auth(bob, alice)); env.close(); - env(claim(alice, chan, delta, delta, Slice(sig), pk)); + env(paychan::claim(alice, chan, delta, delta, Slice(sig), pk)); env.close(); BEAST_EXPECT(env.balance(bob) == preBob + delta - (3 * baseFee)); @@ -794,7 +789,7 @@ struct PayChan_test : public beast::unit_test::Suite env.close(); // alice claims successfully. - env(claim(alice, chan, delta, delta)); + env(paychan::claim(alice, chan, delta, delta)); env.close(); BEAST_EXPECT(env.balance(bob) == preBob + XRP(800) - (5 * baseFee)); } @@ -822,12 +817,12 @@ struct PayChan_test : public beast::unit_test::Suite auto const pk = alice.pk(); auto const settleDelay = 100s; - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, XRP(1000), settleDelay, pk)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); // alice add funds to the channel - env(fund(alice, chan, XRP(1000))); + env(paychan::fund(alice, chan, XRP(1000))); env.close(); std::string const credBadIdx = @@ -918,12 +913,12 @@ struct PayChan_test : public beast::unit_test::Suite auto const pk = alice.pk(); auto const settleDelay = 100s; - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, XRP(1000), settleDelay, pk)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); // alice add funds to the channel - env(fund(alice, chan, XRP(1000))); + env(paychan::fund(alice, chan, XRP(1000))); env.close(); auto const delta = XRP(500).value(); @@ -951,11 +946,11 @@ struct PayChan_test : public beast::unit_test::Suite auto const pk = alice.pk(); auto const settleDelay = 100s; - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, XRP(1000), settleDelay, pk)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); - env(fund(alice, chan, XRP(1000))); + env(paychan::fund(alice, chan, XRP(1000))); env.close(); std::string const credIdx = "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" @@ -987,12 +982,12 @@ struct PayChan_test : public beast::unit_test::Suite auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); - auto const chan1 = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, channelFunds, settleDelay, pk)); - BEAST_EXPECT(channelExists(*env.current(), chan1)); - auto const chan2 = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, channelFunds, settleDelay, pk)); - BEAST_EXPECT(channelExists(*env.current(), chan2)); + auto const chan1 = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan1)); + auto const chan2 = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan2)); BEAST_EXPECT(chan1 != chan2); } @@ -1011,8 +1006,8 @@ struct PayChan_test : public beast::unit_test::Suite auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); - auto const chan1Str = to_string(channel(alice, bob, env.seq(alice))); - env(create(alice, bob, channelFunds, settleDelay, pk)); + auto const chan1Str = to_string(paychan::channel(alice, bob, env.seq(alice))); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); env.close(); { // test account non-string @@ -1068,8 +1063,8 @@ struct PayChan_test : public beast::unit_test::Suite BEAST_EXPECT(r[jss::result][jss::channels].size() == 0); BEAST_EXPECT(r[jss::result][jss::validated]); } - auto const chan2Str = to_string(channel(alice, bob, env.seq(alice))); - env(create(alice, bob, channelFunds, settleDelay, pk)); + auto const chan2Str = to_string(paychan::channel(alice, bob, env.seq(alice))); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); env.close(); { auto const r = env.rpc("account_channels", alice.human(), bob.human()); @@ -1119,7 +1114,7 @@ struct PayChan_test : public beast::unit_test::Suite auto const channelFunds = XRP(1); for (auto const& b : bobs) { - env(create(alice, b, channelFunds, settleDelay, alice.pk())); + env(paychan::create(alice, b, channelFunds, settleDelay, alice.pk())); } } @@ -1218,8 +1213,8 @@ struct PayChan_test : public beast::unit_test::Suite // channels where alice is the source, not the destination auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); - env(create(alice, bob, channelFunds, settleDelay, alice.pk())); - env(create(bob, alice, channelFunds, settleDelay, bob.pk())); + env(paychan::create(alice, bob, channelFunds, settleDelay, alice.pk())); + env(paychan::create(bob, alice, channelFunds, settleDelay, bob.pk())); auto const r = [&] { json::Value jvc; @@ -1246,8 +1241,8 @@ struct PayChan_test : public beast::unit_test::Suite auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); - auto const chan1Str = to_string(channel(alice, bob, env.seq(alice))); - env(create(alice, bob, channelFunds, settleDelay, pk)); + auto const chan1Str = to_string(paychan::channel(alice, bob, env.seq(alice))); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); env.close(); json::Value args{json::ValueType::Object}; @@ -1281,8 +1276,8 @@ struct PayChan_test : public beast::unit_test::Suite auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); - auto const chan1Str = to_string(channel(alice, bob, env.seq(alice))); - env(create(alice, bob, channelFunds, settleDelay, pk)); + auto const chan1Str = to_string(paychan::channel(alice, bob, env.seq(alice))); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); env.close(); std::string chan1PkStr; { @@ -1304,8 +1299,8 @@ struct PayChan_test : public beast::unit_test::Suite BEAST_EXPECT(r[jss::result][jss::channels].size() == 0); BEAST_EXPECT(r[jss::result][jss::validated]); } - auto const chan2Str = to_string(channel(alice, bob, env.seq(alice))); - env(create(alice, bob, channelFunds, settleDelay, pk)); + auto const chan2Str = to_string(paychan::channel(alice, bob, env.seq(alice))); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); env.close(); { auto const r = env.rpc("account_channels", alice.human(), bob.human()); @@ -1601,8 +1596,8 @@ struct PayChan_test : public beast::unit_test::Suite auto const pk = alice.pk(); auto const settleDelay = 100s; - auto const chan = channel(alice, bob, env.seq(alice)); - auto jv = create(alice, bob, XRP(1000), settleDelay, pk); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + auto jv = paychan::create(alice, bob, XRP(1000), settleDelay, pk); auto const pkHex = strHex(pk.slice()); jv["PublicKey"] = pkHex.substr(2, pkHex.size() - 2); env(jv, Ter(temMALFORMED)); @@ -1680,7 +1675,7 @@ struct PayChan_test : public beast::unit_test::Suite // Test with adding the paychan to the recipient's owner directory Env env{*this, features}; env.fund(XRP(10000), alice, bob); - env(create(alice, bob, XRP(1000), settleDelay, pk)); + env(paychan::create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); auto const [chan, chanSle] = channelKeyAndSle(*env.current(), alice, bob); BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle)); @@ -1702,7 +1697,7 @@ struct PayChan_test : public beast::unit_test::Suite Env env(*this, features); env.fund(XRP(10000), alice, bob); // create the channel before the amendment activates - env(create(alice, bob, XRP(1000), settleDelay, pk)); + env(paychan::create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); auto const [chan, chanSle] = channelKeyAndSle(*env.current(), alice, bob); BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle)); @@ -1755,18 +1750,18 @@ struct PayChan_test : public beast::unit_test::Suite // Create a channel from alice to bob auto const pk = alice.pk(); auto const settleDelay = 100s; - auto const chan = channel(alice, bob, env.seq(alice)); - env(create(alice, bob, XRP(1000), settleDelay, pk)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); - BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); - BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0)); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000)); rmAccount(env, alice, carol, tecHAS_OBLIGATIONS); rmAccount(env, bob, carol, TER(tecHAS_OBLIGATIONS)); auto const feeDrops = env.current()->fees().base; - auto chanBal = channelBalance(*env.current(), chan); - auto chanAmt = channelAmount(*env.current(), chan); + auto chanBal = paychan::channelBalance(*env.current(), chan); + auto chanAmt = paychan::channelAmount(*env.current(), chan); BEAST_EXPECT(chanBal == XRP(0)); BEAST_EXPECT(chanAmt == XRP(1000)); @@ -1830,15 +1825,15 @@ struct PayChan_test : public beast::unit_test::Suite auto const pk = alice.pk(); auto const settleDelay = 100s; - auto const chan = channel(alice, bob, aliceTicketSeq); + auto const chan = paychan::channel(alice, bob, aliceTicketSeq); env(create(alice, bob, XRP(1000), settleDelay, pk), ticket::Use(aliceTicketSeq++)); env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); BEAST_EXPECT(env.seq(alice) == aliceSeq); - BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); - BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0)); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000)); { auto const preAlice = env.balance(alice); @@ -1851,8 +1846,8 @@ struct PayChan_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops); } - auto chanBal = channelBalance(*env.current(), chan); - auto chanAmt = channelAmount(*env.current(), chan); + auto chanBal = paychan::channelBalance(*env.current(), chan); + auto chanAmt = paychan::channelAmount(*env.current(), chan); BEAST_EXPECT(chanBal == XRP(0)); BEAST_EXPECT(chanAmt == XRP(2000)); @@ -1868,8 +1863,8 @@ struct PayChan_test : public beast::unit_test::Suite env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); BEAST_EXPECT(env.seq(alice) == aliceSeq); - BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); - BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob + delta); chanBal = reqBal; } @@ -1887,8 +1882,8 @@ struct PayChan_test : public beast::unit_test::Suite env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); BEAST_EXPECT(env.seq(bob) == bobSeq); - BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); - BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt); auto const feeDrops = env.current()->fees().base; BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops); chanBal = reqBal; @@ -1903,8 +1898,8 @@ struct PayChan_test : public beast::unit_test::Suite env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); BEAST_EXPECT(env.seq(bob) == bobSeq); - BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); - BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob - feeDrops); } { @@ -1923,8 +1918,8 @@ struct PayChan_test : public beast::unit_test::Suite env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); BEAST_EXPECT(env.seq(bob) == bobSeq); - BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); - BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob); } @@ -1934,8 +1929,8 @@ struct PayChan_test : public beast::unit_test::Suite env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); BEAST_EXPECT(env.seq(bob) == bobSeq); - BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); - BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt); { // Dst closes channel @@ -1946,7 +1941,7 @@ struct PayChan_test : public beast::unit_test::Suite env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); BEAST_EXPECT(env.seq(bob) == bobSeq); - BEAST_EXPECT(!channelExists(*env.current(), chan)); + BEAST_EXPECT(!paychan::channelExists(*env.current(), chan)); auto const feeDrops = env.current()->fees().base; auto const delta = chanAmt - chanBal; assert(delta > beast::kZero); @@ -1995,6 +1990,7 @@ struct PayChan_test : public beast::unit_test::Suite // pre-amendment behavior, so run it with the amendment disabled. FeatureBitset const all{testableAmendments() - fixCleanup3_2_0}; testWithFeats(all); + testWithFeats(all - featureTokenPaychan); testDepositAuthCreds(); testMetaAndOwnership(all - fixIncludeKeyletFields); } diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 4c18d7343c3..9c77b8d7170 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -19,8 +19,8 @@ #include #include #include +#include #include -#include #include #include #include @@ -379,6 +379,20 @@ checkArraySize(json::Value const& val, unsigned int size); std::uint32_t ownerCount(test::jtx::Env const& env, test::jtx::Account const& account); +/* Token (IOU/MPT) Locking */ +/******************************************************************************/ +uint64_t +mptEscrowed(jtx::Env const& env, jtx::Account const& account, jtx::MPT const& mpt); + +uint64_t +issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt); + +jtx::PrettyAmount +issuerBalance(jtx::Env& env, jtx::Account const& account, Issue const& issue); + +jtx::PrettyAmount +issuerEscrowed(jtx::Env& env, jtx::Account const& account, Issue const& issue); + // Helper function that returns the sponsored owner count on an account. std::uint32_t sponsoredOwnerCount(test::jtx::Env const& env, test::jtx::Account const& account); @@ -599,66 +613,6 @@ accountBalance(Env& env, Account const& acct); [[nodiscard]] bool expectLedgerEntryRoot(Env& env, Account const& acct, STAmount const& expectedValue); -/* Payment Channel */ -/******************************************************************************/ -namespace paychan { - -json::Value -create( - AccountID const& account, - AccountID const& to, - STAmount const& amount, - NetClock::duration const& settleDelay, - PublicKey const& pk, - std::optional const& cancelAfter = std::nullopt, - std::optional const& dstTag = std::nullopt); - -inline json::Value -create( - Account const& account, - Account const& to, - STAmount const& amount, - NetClock::duration const& settleDelay, - PublicKey const& pk, - std::optional const& cancelAfter = std::nullopt, - std::optional const& dstTag = std::nullopt) -{ - return create(account.id(), to.id(), amount, settleDelay, pk, cancelAfter, dstTag); -} - -json::Value -fund( - AccountID const& account, - uint256 const& channel, - STAmount const& amount, - std::optional const& expiration = std::nullopt); - -json::Value -claim( - AccountID const& account, - uint256 const& channel, - std::optional const& balance = std::nullopt, - std::optional const& amount = std::nullopt, - std::optional const& signature = std::nullopt, - std::optional const& pk = std::nullopt); - -uint256 -channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue); - -inline uint256 -channel(Account const& account, Account const& dst, std::uint32_t seqProxyValue) -{ - return channel(account.id(), dst.id(), seqProxyValue); -} - -STAmount -channelBalance(ReadView const& view, uint256 const& chan); - -bool -channelExists(ReadView const& view, uint256 const& chan); - -} // namespace paychan - /* Crossing Limits */ /******************************************************************************/ diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 4d3869b4f91..fcc0fd73886 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -16,18 +16,15 @@ #include #include -#include #include #include #include -#include #include #include #include #include #include #include -#include #include #include #include @@ -37,7 +34,6 @@ #include #include #include -#include #include #include #include @@ -52,7 +48,6 @@ #include #include -#include #include #include #include @@ -94,6 +89,54 @@ ownerCount(Env const& env, Account const& account) return env.ownerCount(account); } +/* Token (IOU/MPT) Locking */ +/******************************************************************************/ +uint64_t +mptEscrowed(jtx::Env const& env, jtx::Account const& account, jtx::MPT const& mpt) +{ + auto const sle = env.le(keylet::mptoken(mpt.mpt(), account)); + if (sle && sle->isFieldPresent(sfLockedAmount)) + return (*sle)[sfLockedAmount]; + return 0; +} + +uint64_t +issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt) +{ + auto const sle = env.le(keylet::mptokenIssuance(mpt.mpt())); + if (sle && sle->isFieldPresent(sfLockedAmount)) + return (*sle)[sfLockedAmount]; + return 0; +} + +jtx::PrettyAmount +issuerBalance(jtx::Env& env, jtx::Account const& account, Issue const& issue) +{ + json::Value params; + params[jss::account] = account.human(); + auto jrr = env.rpc("json", "gateway_balances", to_string(params)); + auto const result = jrr[jss::result]; + auto const obligations = result[jss::obligations][to_string(issue.currency)]; + if (obligations.isNull()) + return {STAmount(issue, 0), account.name()}; + STAmount const amount = amountFromString(issue, obligations.asString()); + return {amount, account.name()}; +} + +jtx::PrettyAmount +issuerEscrowed(jtx::Env& env, jtx::Account const& account, Issue const& issue) +{ + json::Value params; + params[jss::account] = account.human(); + auto jrr = env.rpc("json", "gateway_balances", to_string(params)); + auto const result = jrr[jss::result]; + auto const locked = result[jss::locked][to_string(issue.currency)]; + if (locked.isNull()) + return {STAmount(issue, 0), account.name()}; + STAmount const amount = amountFromString(issue, locked.asString()); + return {amount, account.name()}; +} + std::uint32_t sponsoredOwnerCount(Env const& env, Account const& account) { @@ -499,100 +542,6 @@ expectLedgerEntryRoot(Env& env, Account const& acct, STAmount const& expectedVal return accountBalance(env, acct) == to_string(expectedValue.xrp()); } -/* Payment Channel */ -/******************************************************************************/ -namespace paychan { - -json::Value -create( - AccountID const& account, - AccountID const& to, - STAmount const& amount, - NetClock::duration const& settleDelay, - PublicKey const& pk, - std::optional const& cancelAfter, - std::optional const& dstTag) -{ - json::Value jv; - jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Account] = to_string(account); - jv[jss::Destination] = to_string(to); - jv[jss::Amount] = amount.getJson(JsonOptions::Values::None); - jv[jss::SettleDelay] = settleDelay.count(); - jv[sfPublicKey.fieldName] = strHex(pk.slice()); - if (cancelAfter) - jv[sfCancelAfter.fieldName] = cancelAfter->time_since_epoch().count(); - if (dstTag) - jv[sfDestinationTag.fieldName] = *dstTag; - return jv; -} - -json::Value -fund( - AccountID const& account, - uint256 const& channel, - STAmount const& amount, - std::optional const& expiration) -{ - json::Value jv; - jv[jss::TransactionType] = jss::PaymentChannelFund; - jv[jss::Account] = to_string(account); - jv[sfChannel.fieldName] = to_string(channel); - jv[jss::Amount] = amount.getJson(JsonOptions::Values::None); - if (expiration) - jv[sfExpiration.fieldName] = expiration->time_since_epoch().count(); - return jv; -} - -json::Value -claim( - AccountID const& account, - uint256 const& channel, - std::optional const& balance, - std::optional const& amount, - std::optional const& signature, - std::optional const& pk) -{ - json::Value jv; - jv[jss::TransactionType] = jss::PaymentChannelClaim; - jv[jss::Account] = to_string(account); - jv["Channel"] = to_string(channel); - if (amount) - jv[jss::Amount] = amount->getJson(JsonOptions::Values::None); - if (balance) - jv["Balance"] = balance->getJson(JsonOptions::Values::None); - if (signature) - jv["Signature"] = strHex(*signature); - if (pk) - jv["PublicKey"] = strHex(pk->slice()); - return jv; -} - -uint256 -channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue) -{ - auto const k = keylet::payChannel(account, dst, seqProxyValue); - return k.key; -} - -STAmount -channelBalance(ReadView const& view, uint256 const& chan) -{ - auto const slep = view.read({ltPAYCHAN, chan}); - if (!slep) - return XRPAmount{-1}; - return (*slep)[sfBalance]; -} - -bool -channelExists(ReadView const& view, uint256 const& chan) -{ - auto const slep = view.read({ltPAYCHAN, chan}); - return bool(slep); -} - -} // namespace paychan - /* Crossing Limits */ /******************************************************************************/ diff --git a/src/test/jtx/impl/paychan.cpp b/src/test/jtx/impl/paychan.cpp new file mode 100644 index 00000000000..dde37ac7f71 --- /dev/null +++ b/src/test/jtx/impl/paychan.cpp @@ -0,0 +1,153 @@ +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/** Paychan operations. */ +namespace xrpl::test::jtx::paychan { + +json::Value +create( + AccountID const& account, + AccountID const& to, + STAmount const& amount, + NetClock::duration const& settleDelay, + PublicKey const& pk, + std::optional const& cancelAfter, + std::optional const& dstTag) +{ + json::Value jv; + jv[jss::TransactionType] = jss::PaymentChannelCreate; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv[jss::Destination] = to_string(to); + jv[jss::Amount] = amount.getJson(JsonOptions::Values::None); + jv[jss::SettleDelay] = settleDelay.count(); + jv[sfPublicKey.fieldName] = strHex(pk.slice()); + if (cancelAfter) + jv[sfCancelAfter.fieldName] = cancelAfter->time_since_epoch().count(); + if (dstTag) + jv[sfDestinationTag.fieldName] = *dstTag; + return jv; +} + +json::Value +fund( + AccountID const& account, + uint256 const& channel, + STAmount const& amount, + std::optional const& expiration) +{ + json::Value jv; + jv[jss::TransactionType] = jss::PaymentChannelFund; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv[sfChannel.fieldName] = to_string(channel); + jv[jss::Amount] = amount.getJson(JsonOptions::Values::None); + if (expiration) + jv[sfExpiration.fieldName] = expiration->time_since_epoch().count(); + return jv; +} + +json::Value +claim( + AccountID const& account, + uint256 const& channel, + std::optional const& balance, + std::optional const& amount, + std::optional const& signature, + std::optional const& pk) +{ + json::Value jv; + jv[jss::TransactionType] = jss::PaymentChannelClaim; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv["Channel"] = to_string(channel); + if (amount) + jv[jss::Amount] = amount->getJson(JsonOptions::Values::None); + if (balance) + jv["Balance"] = balance->getJson(JsonOptions::Values::None); + if (signature) + jv["Signature"] = strHex(*signature); + if (pk) + jv["PublicKey"] = strHex(pk->slice()); + return jv; +} + +uint256 +channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue) +{ + auto const k = keylet::payChannel(account, dst, seqProxyValue); + return k.key; +} + +STAmount +channelBalance(ReadView const& view, uint256 const& chan) +{ + auto const slep = view.read({ltPAYCHAN, chan}); + if (!slep) + return XRPAmount{-1}; + return (*slep)[sfBalance]; +} + +STAmount +channelAmount(ReadView const& view, uint256 const& chan) +{ + auto const slep = view.read({ltPAYCHAN, chan}); + if (!slep) + return XRPAmount{-1}; + return (*slep)[sfAmount]; +} + +bool +channelExists(ReadView const& view, uint256 const& chan) +{ + auto const slep = view.read({ltPAYCHAN, chan}); + return bool(slep); +} + +Buffer +signClaimAuth( + PublicKey const& pk, + SecretKey const& sk, + uint256 const& channel, + STAmount const& authAmt) +{ + Serializer msg; + serializePayChanAuthorization(msg, channel, authAmt); + return sign(pk, sk, msg.slice()); +} + +Rate +rate(Env& env, Account const& account, Account const& dest, std::uint32_t const& seq) +{ + auto const sle = env.le(keylet::payChannel(account.id(), dest.id(), seq)); + if (sle->isFieldPresent(sfTransferRate)) + return xrpl::Rate((*sle)[sfTransferRate]); + return Rate{0}; +} + +} // namespace xrpl::test::jtx::paychan diff --git a/src/test/jtx/paychan.h b/src/test/jtx/paychan.h new file mode 100644 index 00000000000..d5e8778336a --- /dev/null +++ b/src/test/jtx/paychan.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/** Paychan operations. */ +namespace xrpl::test::jtx::paychan { + +json::Value +create( + AccountID const& account, + AccountID const& to, + STAmount const& amount, + NetClock::duration const& settleDelay, + PublicKey const& pk, + std::optional const& cancelAfter = std::nullopt, + std::optional const& dstTag = std::nullopt); + +inline json::Value +create( + Account const& account, + Account const& to, + STAmount const& amount, + NetClock::duration const& settleDelay, + PublicKey const& pk, + std::optional const& cancelAfter = std::nullopt, + std::optional const& dstTag = std::nullopt) +{ + return create(account.id(), to.id(), amount, settleDelay, pk, cancelAfter, dstTag); +} + +json::Value +fund( + AccountID const& account, + uint256 const& channel, + STAmount const& amount, + std::optional const& expiration = std::nullopt); + +json::Value +claim( + AccountID const& account, + uint256 const& channel, + std::optional const& balance = std::nullopt, + std::optional const& amount = std::nullopt, + std::optional const& signature = std::nullopt, + std::optional const& pk = std::nullopt); + +uint256 +channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue); + +inline uint256 +channel(Account const& account, Account const& dst, std::uint32_t seqProxyValue) +{ + return channel(account.id(), dst.id(), seqProxyValue); +} + +STAmount +channelBalance(ReadView const& view, uint256 const& chan); + +STAmount +channelAmount(ReadView const& view, uint256 const& chan); + +bool +channelExists(ReadView const& view, uint256 const& chan); + +Buffer +signClaimAuth( + PublicKey const& pk, + SecretKey const& sk, + uint256 const& channel, + STAmount const& authAmt); + +Rate +rate(Env& env, Account const& account, Account const& dest, std::uint32_t const& seq); + +} // namespace xrpl::test::jtx::paychan diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/PayChannelTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/PayChannelTests.cpp index 7c312523698..e14e1dcd1a8 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/PayChannelTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/PayChannelTests.cpp @@ -35,6 +35,8 @@ TEST(PayChannelTests, BuilderSettersRoundTrip) auto const previousTxnIDValue = canonical_UINT256(); auto const previousTxnLgrSeqValue = canonical_UINT32(); auto const destinationNodeValue = canonical_UINT64(); + auto const transferRateValue = canonical_UINT32(); + auto const issuerNodeValue = canonical_UINT64(); PayChannelBuilder builder{ accountValue, @@ -54,6 +56,8 @@ TEST(PayChannelTests, BuilderSettersRoundTrip) builder.setSourceTag(sourceTagValue); builder.setDestinationTag(destinationTagValue); builder.setDestinationNode(destinationNodeValue); + builder.setTransferRate(transferRateValue); + builder.setIssuerNode(issuerNodeValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -166,6 +170,22 @@ TEST(PayChannelTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasDestinationNode()); } + { + auto const& expected = transferRateValue; + auto const actualOpt = entry.getTransferRate(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfTransferRate"); + EXPECT_TRUE(entry.hasTransferRate()); + } + + { + auto const& expected = issuerNodeValue; + auto const actualOpt = entry.getIssuerNode(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfIssuerNode"); + EXPECT_TRUE(entry.hasIssuerNode()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -194,6 +214,8 @@ TEST(PayChannelTests, BuilderFromSleRoundTrip) auto const previousTxnIDValue = canonical_UINT256(); auto const previousTxnLgrSeqValue = canonical_UINT32(); auto const destinationNodeValue = canonical_UINT64(); + auto const transferRateValue = canonical_UINT32(); + auto const issuerNodeValue = canonical_UINT64(); auto sle = std::make_shared(PayChannel::entryType, index); @@ -212,6 +234,8 @@ TEST(PayChannelTests, BuilderFromSleRoundTrip) sle->at(sfPreviousTxnID) = previousTxnIDValue; sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue; sle->at(sfDestinationNode) = destinationNodeValue; + sle->at(sfTransferRate) = transferRateValue; + sle->at(sfIssuerNode) = issuerNodeValue; PayChannelBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -390,6 +414,32 @@ TEST(PayChannelTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfDestinationNode"); } + { + auto const& expected = transferRateValue; + + auto const fromSleOpt = entryFromSle.getTransferRate(); + auto const fromBuilderOpt = entryFromBuilder.getTransferRate(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfTransferRate"); + expectEqualField(expected, *fromBuilderOpt, "sfTransferRate"); + } + + { + auto const& expected = issuerNodeValue; + + auto const fromSleOpt = entryFromSle.getIssuerNode(); + auto const fromBuilderOpt = entryFromBuilder.getIssuerNode(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfIssuerNode"); + expectEqualField(expected, *fromBuilderOpt, "sfIssuerNode"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -472,5 +522,9 @@ TEST(PayChannelTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getDestinationTag().has_value()); EXPECT_FALSE(entry.hasDestinationNode()); EXPECT_FALSE(entry.getDestinationNode().has_value()); + EXPECT_FALSE(entry.hasTransferRate()); + EXPECT_FALSE(entry.getTransferRate().has_value()); + EXPECT_FALSE(entry.hasIssuerNode()); + EXPECT_FALSE(entry.getIssuerNode().has_value()); } } diff --git a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp index a6730c8e2b6..dbf8b177171 100644 --- a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp +++ b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp @@ -175,6 +175,37 @@ doGatewayBalances(RPC::JsonContext& context) } } + if (sle->getType() == ltPAYCHAN) + { + auto const& amount = sle->getFieldAmount(sfAmount); + if (amount.native() || amount.holds()) + return; + + auto const& balance = sle->getFieldAmount(sfBalance); + auto const& netAmount = amount - balance; + auto& bal = locked[netAmount.get().currency]; + if (bal == beast::kZero) + { + // This is needed to set the currency code correctly + bal = netAmount; + } + else + { + try + { + bal += netAmount; + } + catch (std::runtime_error const&) + { + // Presumably the exception was caused by overflow. + // On overflow return the largest valid STAmount. + // Very large sums of STAmount are approximations + // anyway. + bal = STAmount(bal.get(), STAmount::kMaxValue, STAmount::kMaxOffset); + } + } + } + auto rs = PathFindTrustLine::makeItem(accountID, sle); if (!rs)