diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index 859981cf059..e55fd4ecfd0 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -6,13 +6,132 @@ #include #include #include +#include #include #include +#include #include +#include #include namespace xrpl { +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::mptIssuance(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( @@ -46,6 +165,7 @@ escrowUnlockApplyHelper( bool const recvLow = issuer > receiver; bool const senderIssuer = issuer == sender; bool const receiverIssuer = issuer == receiver; + bool const lineExisted = view.exists(trustLineKey); if (senderIssuer) return tecINTERNAL; // LCOV_EXCL_LINE @@ -53,7 +173,7 @@ escrowUnlockApplyHelper( if (receiverIssuer) return tesSUCCESS; - if (!view.exists(trustLineKey) && createAsset) + if (!lineExisted && createAsset) { // Can the account cover the trust line's reserve? if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; diff --git a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h index 24838f1331e..2eba3a0dfb6 100644 --- a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h +++ b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -12,6 +13,7 @@ closeChannel( std::shared_ptr const& slep, ApplyView& view, uint256 const& key, + AccountID const& txAccount, beast::Journal j); } // namespace xrpl diff --git a/include/xrpl/protocol/PayChan.h b/include/xrpl/protocol/PayChan.h index d8f4e0f5273..7194ce8bdeb 100644 --- a/include/xrpl/protocol/PayChan.h +++ b/include/xrpl/protocol/PayChan.h @@ -1,8 +1,14 @@ #pragma once #include +#include #include +#include +#include +#include +#include #include +#include #include namespace xrpl { @@ -15,4 +21,68 @@ 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 + msg.add64( + 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 fd62b74d596..423ebcb0381 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_FIX (Cleanup3_2_0, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_1_3, Supported::Yes, VoteBehavior::DefaultYes) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 632038a9c5b..3f3e36b42e5 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -364,6 +364,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 450e2558cce..a8efbb694bc 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/jss.h b/include/xrpl/protocol/jss.h index 8a2a1125427..58a5a84a0d9 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -243,6 +243,7 @@ JSS(errored); // JSS(error_code); // out: error JSS(error_exception); // out: Submit JSS(error_message); // out: error +JSS(escrowed); // out: escrowed JSS(expand); // in: handler/Ledger JSS(expected_date); // out: any (warnings) JSS(expected_date_UTC); // out: any (warnings) diff --git a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp index 31c206d85b6..f6eb6c6e393 100644 --- a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp +++ b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp @@ -3,16 +3,23 @@ #include #include #include +#include #include #include #include +#include #include +#include +#include #include +#include #include +#include #include #include #include +#include namespace xrpl { @@ -21,6 +28,7 @@ closeChannel( std::shared_ptr const& slep, ApplyView& view, uint256 const& key, + AccountID const& txAccount, beast::Journal j) { AccountID const src = (*slep)[sfAccount]; @@ -37,9 +45,9 @@ closeChannel( } // 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 @@ -56,7 +64,63 @@ closeChannel( XRPL_ASSERT( (*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount"); - (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]; + + 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); diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index 3e51b6f8773..9584b8e6560 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -130,7 +130,8 @@ XRPNotCreated::visitEntry( 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])) @@ -149,7 +150,7 @@ XRPNotCreated::visitEntry( 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 635af25b616..5646a583b72 100644 --- a/src/libxrpl/tx/invariants/MPTInvariant.cpp +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -341,6 +341,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 123f83a1a67..c55b45ab82a 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 @@ -34,60 +29,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::mptIssuance(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) { @@ -105,7 +46,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 0b1db125f66..8e8b8a50029 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 @@ -186,18 +187,15 @@ escrowCreatePreclaimHelper( { Issue 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::line(account, issuer, issue.currency)); if (!sleRippleState) return tecNO_LINE; @@ -212,19 +210,15 @@ escrowCreatePreclaimHelper( 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; @@ -235,12 +229,9 @@ escrowCreatePreclaimHelper( 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; @@ -256,51 +247,38 @@ escrowCreatePreclaimHelper( 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::mptIssuance(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; @@ -316,8 +294,6 @@ escrowCreatePreclaimHelper( 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; @@ -358,54 +334,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 13bd4b16826..3898dcc238a 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 @@ -130,68 +125,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::mptIssuance(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) { @@ -216,7 +149,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 cc99b8f62de..7501229f3d5 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 @@ -25,6 +28,7 @@ #include #include #include +#include namespace xrpl { @@ -44,11 +48,16 @@ NotTEC PaymentChannelClaim::preflight(PreflightContext const& ctx) { 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; + + // 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) @@ -68,8 +77,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; @@ -80,7 +89,7 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx) PublicKey const pk(ctx.tx[sfPublicKey]); Serializer msg; - serializePayChanAuthorization(msg, k.key, authAmt); + serializePayChanAuthorization(msg, k.key, *authAmt); if (!verify(pk, msg.slice(), *sig)) return temBAD_SIGNATURE; } @@ -101,6 +110,27 @@ PaymentChannelClaim::preclaim(PreclaimContext const& ctx) !isTesSuccess(err)) return err; + 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 (!ctx.view.rules().enabled(featureTokenPaychan)) + return temDISABLED; + + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockPreclaimHelper(ctx.view, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + return tesSUCCESS; } @@ -114,7 +144,6 @@ PaymentChannelClaim::doApply() AccountID const src = (*slep)[sfAccount]; AccountID const dst = (*slep)[sfDestination]; - AccountID const txAccount = ctx_.tx[sfAccount]; auto const curExpiration = (*slep)[~sfExpiration]; { @@ -122,19 +151,29 @@ PaymentChannelClaim::doApply() auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count(); if ((cancelAfter && closeTime >= *cancelAfter) || (curExpiration && closeTime >= *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 temBAD_SIGNATURE; if (ctx_.tx[~sfSignature]) @@ -158,22 +197,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); @@ -182,8 +255,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 = ctx_.view().header().parentCloseTime.time_since_epoch().count() + diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp index 7ce25b6d15d..943f9a7476e 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp @@ -7,21 +7,33 @@ #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 { @@ -47,17 +59,268 @@ 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(featureMPTokensV1)) + return temDISABLED; + + auto const amount = ctx.tx[sfAmount]; + if (amount.native() || amount.mpt() > MPTAmount{kMaxMpTokenAmount} || amount <= beast::kZero) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +template +static TER +payChanCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount); + +template <> +TER +payChanCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount) +{ + Issue 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::line(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 +payChanCreatePreclaimHelper( + 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::mptIssuance(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; +} + +template +static TER +payChanLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal); + +template <> +TER +payChanLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create a payment channel + 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 +payChanLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create a payment channel + 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; +} + 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}; } 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; @@ -76,6 +339,8 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx) if (!sle) return terNO_ACCOUNT; + STAmount const amount{ctx.tx[sfAmount]}; + // Check reserve and funds availability { auto const balance = (*sle)[sfBalance]; @@ -84,15 +349,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; @@ -113,6 +378,17 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } + if (!isXRP(amount)) + { + if (auto const ret = std::visit( + [&](T const&) { + return payChanCreatePreclaimHelper(ctx, account, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + return tesSUCCESS; } @@ -120,6 +396,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 @@ -131,21 +408,21 @@ PaymentChannelCreate::doApply() return tecEXPIRED; } - 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::payChan(account, dst, ctx_.tx.getSeqValue()); + Keylet const payChanKeylet = keylet::payChan(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]; @@ -156,6 +433,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 @@ -170,14 +454,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 payChanLockApplyHelper(ctx_.view(), issuer, accountID_, amount, j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); 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 41906aa3dac..64841c513ee 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp @@ -4,35 +4,138 @@ #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 { +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(featureMPTokensV1)) + return temDISABLED; + + auto const amount = ctx.tx[sfAmount]; + if (amount.native() || amount.mpt() > MPTAmount{kMaxMpTokenAmount} || amount <= beast::kZero) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +template +static TER +payChanLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal); + +template <> +TER +payChanLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + 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 +payChanLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + 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; +} + 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}; } NotTEC PaymentChannelFund::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 payChanFundPreflightHelper(ctx); }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + else + { + if (amount <= beast::kZero) + return temBAD_AMOUNT; + } return tesSUCCESS; } @@ -46,17 +149,20 @@ PaymentChannelFund::doApply() return tecNO_ENTRY; AccountID const src = (*slep)[sfAccount]; - auto const txAccount = ctx_.tx[sfAccount]; + AccountID const dst = (*slep)[sfDestination]; auto const expiration = (*slep)[~sfExpiration]; { auto const cancelAfter = (*slep)[~sfCancelAfter]; auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count(); if ((cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration)) - 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; @@ -75,10 +181,18 @@ 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]}; + + // The funded asset must match the channel's asset. Without this check + // STAmount arithmetic below (e.g. (*slep)[sfAmount] + amount) would + // throw on mismatched issues. + if (amount.asset() != STAmount{(*slep)[sfAmount]}.asset()) + return temBAD_AMOUNT; + { // Check reserve and funds availability auto const balance = (*sle)[sfBalance]; @@ -87,20 +201,57 @@ PaymentChannelFund::doApply() if (balance < reserve) return tecINSUFFICIENT_RESERVE; - if (balance < reserve + ctx_.tx[sfAmount]) + if (isXRP(amount) && balance < reserve + 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)) + { + if (auto const ret = std::visit( + [&](T const&) -> TER { + auto const& iss = amount.get(); + if (isFrozen(ctx_.view(), accountID_, iss)) + { + if constexpr (std::is_same_v) + { + return tecFROZEN; + } + else + { + return tecLOCKED; + } + } + return TER{tesSUCCESS}; + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + if (isXRP(amount)) + { + (*sle)[sfBalance] = (*sle)[sfBalance] - amount; + } + else + { + AccountID const issuer = amount.getIssuer(); + if (auto const ret = std::visit( + [&](T const&) { + return payChanLockApplyHelper(ctx_.view(), issuer, accountID_, amount, j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + + (*slep)[sfAmount] = (*slep)[sfAmount] + amount; + ctx_.view().update(slep); ctx_.view().update(sle); return tesSUCCESS; diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index 5bb1303dba3..c4e6aaf5be5 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -1677,7 +1677,6 @@ struct EscrowToken_test : public beast::unit_test::Suite 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}; diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 6906c4aba02..876b64c6320 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1979,8 +1979,67 @@ class MPToken_test : public beast::unit_test::Suite setMPTFields(field, jv); test(jv, field.fieldName); }; - ammBid(sfBidMin); - ammBid(sfBidMax); + for (SField const& field : + {std::cref(static_cast(sfBidMin)), + std::cref(static_cast(sfBidMax)), + std::cref(static_cast(sfAsset)), + std::cref(static_cast(sfAsset2))}) + ammBid(field); + // AMMClawback + auto ammClawback = [&](SField const& field) { + json::Value jv; + jv[jss::TransactionType] = jss::AMMClawback; + jv[jss::Account] = alice.human(); + jv[jss::Holder] = carol.human(); + setMPTFields(field, jv); + test(jv, field.fieldName); + }; + for (SField const& field : + {std::cref(static_cast(sfAmount)), + std::cref(static_cast(sfAsset)), + std::cref(static_cast(sfAsset2))}) + ammClawback(field); + // AMMDelete + auto ammDelete = [&](SField const& field) { + json::Value jv; + jv[jss::TransactionType] = jss::AMMDelete; + jv[jss::Account] = alice.human(); + setMPTFields(field, jv, false); + test(jv, field.fieldName); + }; + ammDelete(sfAsset); + ammDelete(sfAsset2); + // AMMVote + auto ammVote = [&](SField const& field) { + json::Value jv; + jv[jss::TransactionType] = jss::AMMVote; + jv[jss::Account] = alice.human(); + jv[jss::TradingFee] = 100; + setMPTFields(field, jv, false); + test(jv, field.fieldName); + }; + ammVote(sfAsset); + ammVote(sfAsset2); + // CheckCash + auto checkCash = [&](SField const& field) { + json::Value jv; + jv[jss::TransactionType] = jss::CheckCash; + jv[jss::Account] = alice.human(); + jv[sfCheckID.fieldName] = to_string(uint256{1}); + jv[field.fieldName] = mpt.getJson(JsonOptions::Values::None); + test(jv, field.fieldName); + }; + checkCash(sfAmount); + checkCash(sfDeliverMin); + // CheckCreate + { + json::Value jv; + jv[jss::TransactionType] = jss::CheckCreate; + jv[jss::Account] = alice.human(); + jv[jss::Destination] = carol.human(); + jv[jss::SendMax] = mpt.getJson(JsonOptions::Values::None); + test(jv, jss::SendMax.cStr()); + } // PaymentChannelCreate { json::Value jv; @@ -2010,6 +2069,13 @@ class MPToken_test : public beast::unit_test::Suite jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None); test(jv, jss::Amount.cStr()); } + // OfferCreate + { + json::Value jv = offer(alice, usd(100), mpt); + test(jv, jss::TakerPays.cStr()); + jv = offer(alice, mpt, usd(100)); + test(jv, jss::TakerGets.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..ba7907762d3 --- /dev/null +++ b/src/test/app/PayChanToken_test.cpp @@ -0,0 +1,3681 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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(); + + // Can Fund PayChan without asfAllowTrustLineLocking + env(paychan::fund(alice, chan, usd(1'000)), Ter(tesSUCCESS)); + 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(); + + // alice cannot create paychan for 1/10 iou - precision loss + env(paychan::create(alice, bob, usd(1), 100s, alice.pk()), 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 cannot claim if bobs limit is too low + 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(tecLIMIT_EXCEEDED)); + env.close(); + + // bobs limit is not changed + 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::payChan(alice.id(), bob.id(), aseq)); + BEAST_EXPECT(ab); + + auto const bc = env.le(keylet::payChan(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); + 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); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) != bod.end()); + 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); + 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); + BEAST_EXPECT(std::find(iod.begin(), iod.end(), ab) != iod.end()); + 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::payChan(alice.id(), bob.id(), aseq))); + BEAST_EXPECT(env.le(keylet::payChan(bob.id(), carol.id(), bseq))); + + xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + 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); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end()); + 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); + BEAST_EXPECT(std::find(iod.begin(), iod.end(), ab) == iod.end()); + 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::payChan(alice.id(), bob.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::payChan(bob.id(), carol.id(), bseq))); + + xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + 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); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end()); + 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); + BEAST_EXPECT(std::find(iod.begin(), iod.end(), ab) == iod.end()); + 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::payChan(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); + 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); + 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::payChan(alice.id(), gw.id(), aseq))); + + xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + 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); + 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(); + + // 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(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::mptIssuance(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::payChan(alice.id(), bob.id(), aseq)); + BEAST_EXPECT(ab); + + auto const bc = env.le(keylet::payChan(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); + 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); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) != bod.end()); + 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); + 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::payChan(alice.id(), bob.id(), aseq))); + BEAST_EXPECT(env.le(keylet::payChan(bob.id(), carol.id(), bseq))); + + xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + 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); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end()); + 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::payChan(alice.id(), bob.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::payChan(bob.id(), carol.id(), bseq))); + + xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + 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); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end()); + 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(); + + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::fund(alice, chan, usd(1'000)), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == usd(2'000)); + + 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 + 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 more — fails, would exceed limit (450 + 350 > 500) + sig = paychan::signClaimAuth(pk, alice.sk(), chan, usd(600)); + env(paychan::claim(bob, chan, usd(600), usd(600), Slice(sig), pk), + Ter(tecLIMIT_EXCEEDED)); + env.close(); + BEAST_EXPECT(env.balance(bob, usd) == usd(450)); + + // Alice closing on bob's behalf also blocked by limit + env(paychan::claim(alice, chan, usd(600), usd(600)), + Txflags(tfClose), + Ter(tecLIMIT_EXCEEDED)); + env.close(); + } + } + + 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); + 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); + } + +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 b81afa830e9..40afc0dd084 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 @@ -79,15 +79,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) { @@ -112,20 +103,20 @@ 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)); @@ -175,9 +166,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; } @@ -337,14 +328,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); @@ -354,7 +345,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); } @@ -368,9 +359,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)); @@ -437,9 +428,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)); @@ -468,7 +459,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})); @@ -490,17 +481,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; @@ -516,8 +507,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); @@ -546,17 +537,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); @@ -581,15 +572,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); @@ -604,8 +595,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); @@ -636,9 +627,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)); } { @@ -647,13 +638,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)); } } @@ -705,16 +696,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. @@ -742,7 +733,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); } @@ -773,7 +764,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)); @@ -795,7 +786,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)); } @@ -823,12 +814,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 = @@ -877,7 +868,7 @@ struct PayChan_test : public beast::unit_test::Suite credentials::Ids({credIdx}), Ter(tecBAD_CREDENTIALS)); - // Fails because bob's lsfDepositAuth flag is set. + // Fails because bob’s lsfDepositAuth flag is set. env(claim(alice, chan, delta, delta), Ter(tecNO_PERMISSION)); // Fail, bad credentials index. @@ -919,12 +910,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(); @@ -952,11 +943,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" @@ -988,12 +979,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); } @@ -1012,8 +1003,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 @@ -1069,8 +1060,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()); @@ -1120,7 +1111,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())); } } @@ -1219,8 +1210,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; @@ -1247,8 +1238,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}; @@ -1282,8 +1273,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; { @@ -1305,8 +1296,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()); @@ -1602,8 +1593,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)); @@ -1682,7 +1673,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)); @@ -1704,7 +1695,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)); @@ -1712,6 +1703,7 @@ struct PayChan_test : public beast::unit_test::Suite BEAST_EXPECT(inOwnerDir(*env.current(), bob, chanSle)); BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); + // close the channel after the amendment activates env(claim(bob, chan), Txflags(tfClose)); BEAST_EXPECT(!channelExists(*env.current(), chan)); BEAST_EXPECT(!inOwnerDir(*env.current(), alice, chanSle)); @@ -1757,18 +1749,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)); @@ -1832,15 +1824,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); @@ -1853,8 +1845,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)); @@ -1870,8 +1862,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; } @@ -1889,8 +1881,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; @@ -1905,8 +1897,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); } { @@ -1916,7 +1908,7 @@ struct PayChan_test : public beast::unit_test::Suite STAmount const reqAmt = authAmt + drops(1); assert(reqAmt <= chanAmt); // Note that since claim() returns a tem (neither tec nor tes), - // the ticket is not consumed. So we don't kIncrement bobTicket. + // the ticket is not consumed. So we don't increment bobTicket. auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); env(claim(bob, chan, reqAmt, authAmt, Slice(sig), alice.pk()), ticket::Use(bobTicketSeq), @@ -1925,8 +1917,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); } @@ -1936,8 +1928,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 @@ -1948,7 +1940,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); @@ -1994,6 +1986,7 @@ struct PayChan_test : public beast::unit_test::Suite using namespace test::jtx; FeatureBitset const all{testableAmendments()}; testWithFeats(all); + testWithFeats(all - featureTokenEscrow); testDepositAuthCreds(); testMetaAndOwnership(all - fixIncludeKeyletFields); } diff --git a/src/test/jtx.h b/src/test/jtx.h index d4b88b0b9ef..f84dd396030 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 011ac2e58d7..96b1f60e729 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include @@ -345,6 +346,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); + [[nodiscard]] inline bool checkVL(Slice const& result, std::string const& expected) @@ -552,66 +567,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 c784c074def..358d8a7f752 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::mptIssuance(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()}; +} + /* Path finding */ /******************************************************************************/ void @@ -481,100 +524,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::payChan(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..50243e335b7 --- /dev/null +++ b/src/test/jtx/impl/paychan.cpp @@ -0,0 +1,172 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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::payChan(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::payChan(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..880bcb3a935 --- /dev/null +++ b/src/test/jtx/paychan.h @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#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/xrpld/rpc/handlers/account/GatewayBalances.cpp b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp index 146b9ead5ca..17204e60697 100644 --- a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp +++ b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp @@ -147,22 +147,52 @@ doGatewayBalances(RPC::JsonContext& context) forEachItem(*ledger, accountID, [&](std::shared_ptr const& sle) { if (sle->getType() == ltESCROW) { - auto const& escrow = sle->getFieldAmount(sfAmount); - // Gateway Balance should not include MPTs - if (escrow.holds()) + auto const& amount = sle->getFieldAmount(sfAmount); + if (amount.native() || amount.holds()) return; - auto& bal = locked[escrow.get().currency]; + auto& bal = locked[amount.get().currency]; if (bal == beast::kZero) { // This is needed to set the currency code correctly - bal = escrow; + bal = amount; } else { try { - bal += escrow; + bal += amount; + } + 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); + } + } + } + + 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&) {